Skip to content

Commit

Permalink
Add passwordless login/registration to auth and web (#10632)
Browse files Browse the repository at this point in the history
Wire passwordless registration and authorization into Auth and Proxy APIs, thus
making passwordless logins possible.

API changes are described by RFD 52: Passwordless [1].

#9160

[1] https://github.com/gravitational/teleport/blob/master/rfd/0052-passwordless.md#authentication-api-changes

* Add passwordless settings to Auth protos
* Update generated protos
* Register: Apply DeviceUsage in lib/auth
* Register: Apply DeviceUsage in lib/web
* Login: Generate passwordless challenge
* Login: Allow passwordless authentication
* Wire passwordless in lib/web endpoints
* Make mocku2f passwordless setup a bit nicer
  • Loading branch information
codingllama authored Mar 4, 2022
1 parent 61fce15 commit 5023235
Show file tree
Hide file tree
Showing 17 changed files with 1,921 additions and 791 deletions.
1,759 changes: 1,150 additions & 609 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

35 changes: 33 additions & 2 deletions api/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -818,6 +818,19 @@ enum DeviceType {
DEVICE_TYPE_WEBAUTHN = 3;
}

enum DeviceUsage {
DEVICE_USAGE_UNSPECIFIED = 0;

// Device intended for MFA use, but not for passwordless.
// Allows both FIDO and FIDO2 devices.
// Resident keys not required.
DEVICE_USAGE_MFA = 1;

// Device intended for both MFA and passwordless.
// Requires a FIDO2 device and takes a resident key slot.
DEVICE_USAGE_PASSWORDLESS = 2;
}

// MFAAuthenticateChallenge is a challenge for all MFA devices registered for a
// user.
message MFAAuthenticateChallenge {
Expand Down Expand Up @@ -928,7 +941,11 @@ message AddMFADeviceResponse {
// AddMFADeviceRequestInit describes the new MFA device.
message AddMFADeviceRequestInit {
string DeviceName = 1;
reserved 2; // LegacyDeviceType LegacyType
DeviceType DeviceType = 3;
// DeviceUsage is the requested usage for the device.
// Defaults to DEVICE_USAGE_MFA.
DeviceUsage DeviceUsage = 4 [ (gogoproto.jsontag) = "device_usage,omitempty" ];
}

// AddMFADeviceResponseAck is a confirmation of successful device registration.
Expand Down Expand Up @@ -1374,12 +1391,17 @@ message UserCredentials {
bytes Password = 2 [ (gogoproto.jsontag) = "password" ];
}

// ContextUser marks requests that rely in the currently authenticated user.
message ContextUser {}

// Passwordless marks requests for passwordless challenges.
message Passwordless {}

// CreateAuthenticateChallengeRequest is a request for creating MFA authentication challenges for a
// users mfa devices.
message CreateAuthenticateChallengeRequest {
// Request defines how the request will be verified before creating challenges.
// This field can be empty, which implies the request is to create challenges for the
// user in context (logged in user).
// An empty Request is equivalent to context_user being set.
oneof Request {
// UserCredentials verifies request with username and password. Used with logins or
// when the logged in user wants to change their password.
Expand All @@ -1389,6 +1411,12 @@ message CreateAuthenticateChallengeRequest {
// VerifyAccountRecovery (step 2 of the recovery process after RPC StartAccountRecovery).
string RecoveryStartTokenID = 2
[ (gogoproto.jsontag) = "recovery_start_token_id,omitempty" ];
// ContextUser issues a challenge for the currently-authenticated user.
// Default option if no other is provided.
ContextUser ContextUser = 3 [ (gogoproto.jsontag) = "context_user,omitempty" ];
// Passwordless issues a passwordless challenge (authenticated user not
// required).
Passwordless Passwordless = 4 [ (gogoproto.jsontag) = "passwordless,omitempty" ];
}
}

Expand All @@ -1412,6 +1440,9 @@ message CreateRegisterChallengeRequest {
string TokenID = 1 [ (gogoproto.jsontag) = "token_id" ];
// DeviceType is the type of MFA device to make a register challenge for.
DeviceType DeviceType = 2 [ (gogoproto.jsontag) = "device_type" ];
// DeviceUsage is the requested usage for the device.
// Defaults to DEVICE_USAGE_MFA.
DeviceUsage DeviceUsage = 3 [ (gogoproto.jsontag) = "device_usage,omitempty" ];
}

// PaginatedResource represents one of the supported resources.
Expand Down
3 changes: 2 additions & 1 deletion lib/auth/accountrecovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -260,7 +260,8 @@ func (s *Server) VerifyAccountRecovery(ctx context.Context, req *proto.VerifyAcc
}

if err := s.verifyAuthnWithRecoveryLock(ctx, startToken, func() error {
_, err := s.validateMFAAuthResponse(ctx, startToken.GetUser(), req.GetMFAAuthenticateResponse())
_, _, err := s.validateMFAAuthResponse(
ctx, req.GetMFAAuthenticateResponse(), startToken.GetUser(), false /* passwordless */)
return err
}); err != nil {
return nil, trace.Wrap(err)
Expand Down
2 changes: 1 addition & 1 deletion lib/auth/accountrecovery_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1291,7 +1291,7 @@ func TestGetAccountRecoveryCodes(t *testing.T) {

func triggerLoginLock(t *testing.T, srv *Server, username string) {
for i := 1; i <= defaults.MaxLoginAttempts; i++ {
_, err := srv.authenticateUser(context.Background(), AuthenticateUserRequest{
_, _, err := srv.authenticateUser(context.Background(), AuthenticateUserRequest{
Username: username,
OTP: &OTPCreds{},
})
Expand Down
114 changes: 89 additions & 25 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -1307,6 +1307,7 @@ func (a *Server) PreAuthenticatedSignIn(user string, identity tlsca.Identity) (t
// CreateAuthenticateChallenge implements AuthService.CreateAuthenticateChallenge.
func (a *Server) CreateAuthenticateChallenge(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) {
var username string
var passwordless bool

switch req.GetRequest().(type) {
case *proto.CreateAuthenticateChallengeRequest_UserCredentials:
Expand All @@ -1331,15 +1332,18 @@ func (a *Server) CreateAuthenticateChallenge(ctx context.Context, req *proto.Cre

username = token.GetUser()

default:
case *proto.CreateAuthenticateChallengeRequest_Passwordless:
passwordless = true // Allows empty username.

default: // unset or CreateAuthenticateChallengeRequest_ContextUser.
var err error
username, err = GetClientUsername(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
}

challenges, err := a.mfaAuthChallenge(ctx, username)
challenges, err := a.mfaAuthChallenge(ctx, username, passwordless)
if err != nil {
log.Error(trace.DebugReport(err))
return nil, trace.AccessDenied("unable to create MFA challenges")
Expand Down Expand Up @@ -1368,17 +1372,19 @@ func (a *Server) CreateRegisterChallenge(ctx context.Context, req *proto.CreateR
}

regChal, err := a.createRegisterChallenge(ctx, &newRegisterChallengeRequest{
username: token.GetUser(),
token: token,
deviceType: req.GetDeviceType(),
username: token.GetUser(),
token: token,
deviceType: req.GetDeviceType(),
deviceUsage: req.GetDeviceUsage(),
})

return regChal, trace.Wrap(err)
}

type newRegisterChallengeRequest struct {
username string
deviceType proto.DeviceType
username string
deviceType proto.DeviceType
deviceUsage proto.DeviceUsage

// token is a user token resource.
// It is used as following:
Expand Down Expand Up @@ -1447,7 +1453,8 @@ func (a *Server) createRegisterChallenge(ctx context.Context, req *newRegisterCh
Identity: identity,
}

credentialCreation, err := webRegistration.Begin(ctx, req.username, false /* passwordless */)
passwordless := req.deviceUsage == proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS
credentialCreation, err := webRegistration.Begin(ctx, req.username, passwordless)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -3184,7 +3191,7 @@ func (a *Server) isMFARequired(ctx context.Context, checker services.AccessCheck

// mfaAuthChallenge constructs an MFAAuthenticateChallenge for all MFA devices
// registered by the user.
func (a *Server) mfaAuthChallenge(ctx context.Context, user string) (*proto.MFAAuthenticateChallenge, error) {
func (a *Server) mfaAuthChallenge(ctx context.Context, user string, passwordless bool) (*proto.MFAAuthenticateChallenge, error) {
// Check what kind of MFA is enabled.
apref, err := a.GetAuthPreference(ctx)
if err != nil {
Expand Down Expand Up @@ -3212,16 +3219,42 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user string) (*proto.MFAA
webConfig = val
}

devs, err := a.Identity.GetMFADevices(ctx, user, true)
// Handle passwordless separately, it works differently from MFA.
if passwordless {
if !enableWebauthn {
return nil, trace.BadParameter("passwordless requires WebAuthn")
}
webLogin := &wanlib.PasswordlessFlow{
Webauthn: webConfig,
Identity: a.Identity,
}
assertion, err := webLogin.Begin(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
return &proto.MFAAuthenticateChallenge{
WebauthnChallenge: wanlib.CredentialAssertionToProto(assertion),
}, nil
}

// User required for non-passwordless.
if user == "" {
return nil, trace.BadParameter("user required")
}

devs, err := a.Identity.GetMFADevices(ctx, user, true /* withSecrets */)
if err != nil {
return nil, trace.Wrap(err)
}

groupedDevs := groupByDeviceType(devs, enableWebauthn)
challenge := &proto.MFAAuthenticateChallenge{}

// TOTP challenge.
if enableTOTP && groupedDevs.TOTP {
challenge.TOTP = &proto.TOTPChallenge{}
}

// WebAuthn challenge.
if len(groupedDevs.Webauthn) > 0 {
webLogin := &wanlib.LoginFlow{
U2F: u2fPref,
Expand Down Expand Up @@ -3264,32 +3297,63 @@ func groupByDeviceType(devs []*types.MFADevice, groupWebauthn bool) devicesByTyp
return res
}

func (a *Server) validateMFAAuthResponse(ctx context.Context, user string, resp *proto.MFAAuthenticateResponse) (*types.MFADevice, error) {
// validateMFAAuthResponse validates an MFA or passwordless challenge.
// Returns the device used to solve the challenge (if applicable) and the
// username.
func (a *Server) validateMFAAuthResponse(
ctx context.Context,
resp *proto.MFAAuthenticateResponse, user string, passwordless bool) (*types.MFADevice, string, error) {
// Sanity check user/passwordless.
if user == "" && !passwordless {
return nil, "", trace.BadParameter("user required")
}

switch res := resp.Response.(type) {
case *proto.MFAAuthenticateResponse_TOTP:
return a.checkOTP(user, res.TOTP.Code)
// cases in order of preference
case *proto.MFAAuthenticateResponse_Webauthn:
// Read necessary configurations.
cap, err := a.GetAuthPreference(ctx)
if err != nil {
return nil, trace.Wrap(err)
return nil, "", trace.Wrap(err)
}
u2f, err := cap.GetU2F()
switch {
case trace.IsNotFound(err): // OK, may happen.
case err != nil: // Unexpected.
return nil, "", trace.Wrap(err)
}
u2f, _ := cap.GetU2F()
webConfig, err := cap.GetWebauthn()
if err != nil {
return nil, trace.Wrap(err)
return nil, "", trace.Wrap(err)
}
webLogin := &wanlib.LoginFlow{
U2F: u2f,
Webauthn: webConfig,
Identity: a.Identity,

assertionResp := wanlib.CredentialAssertionResponseFromProto(res.Webauthn)
var dev *types.MFADevice
if passwordless {
webLogin := &wanlib.PasswordlessFlow{
Webauthn: webConfig,
Identity: a.Identity,
}
dev, user, err = webLogin.Finish(ctx, assertionResp)
} else {
webLogin := &wanlib.LoginFlow{
U2F: u2f,
Webauthn: webConfig,
Identity: a.Identity,
}
dev, err = webLogin.Finish(ctx, user, wanlib.CredentialAssertionResponseFromProto(res.Webauthn))
}
dev, err := webLogin.Finish(ctx, user, wanlib.CredentialAssertionResponseFromProto(res.Webauthn))
if err != nil {
return nil, trace.AccessDenied("MFA response validation failed: %v", err)
return nil, "", trace.AccessDenied("MFA response validation failed: %v", err)
}
return dev, nil
return dev, user, nil

case *proto.MFAAuthenticateResponse_TOTP:
dev, err := a.checkOTP(user, res.TOTP.Code)
return dev, user, trace.Wrap(err)

default:
return nil, trace.BadParameter("unknown or missing MFAAuthenticateResponse type %T", resp.Response)
return nil, "", trace.BadParameter("unknown or missing MFAAuthenticateResponse type %T", resp.Response)
}
}

Expand Down
Loading

0 comments on commit 5023235

Please sign in to comment.