diff --git a/api/constants/constants.go b/api/constants/constants.go index f76acb95138..e1abc8ebb72 100644 --- a/api/constants/constants.go +++ b/api/constants/constants.go @@ -40,15 +40,6 @@ const ( // are captured. EnhancedRecordingNetwork = "network" - // OTP means One-time Password Algorithm for Two-Factor Authentication. - OTP = "otp" - - // U2F means Universal 2nd Factor.for Two-Factor Authentication. - U2F = "u2f" - - // OFF means no second factor.for Two-Factor Authentication. - OFF = "off" - // Local means authentication will happen locally within the Teleport cluster. Local = "local" @@ -89,3 +80,23 @@ const ( // KeepAliveDatabase is the keep alive type for database server. KeepAliveDatabase = "db" ) + +// SecondFactorType is the type of 2FA authentication. +type SecondFactorType string + +const ( + // SecondFactorOff means no second factor. + SecondFactorOff = SecondFactorType("off") + // SecondFactorOTP means that only OTP is supported for 2FA and 2FA is + // required for all users. + SecondFactorOTP = SecondFactorType("otp") + // SecondFactorU2F means that only U2F is supported for 2FA and 2FA is + // required for all users. + SecondFactorU2F = SecondFactorType("u2f") + // SecondFactorOn means that all 2FA protocols are supported and 2FA is + // required for all users. + SecondFactorOn = SecondFactorType("on") + // SecondFactorOptional means that all 2FA protocols are supported and 2FA + // is required only for users that have MFA devices registered. + SecondFactorOptional = SecondFactorType("optional") +) diff --git a/api/types/authentication.go b/api/types/authentication.go index e2bbee39ed7..11a6b0e4a69 100644 --- a/api/types/authentication.go +++ b/api/types/authentication.go @@ -43,9 +43,9 @@ type AuthPreference interface { SetType(string) // GetSecondFactor gets the type of second factor: off, otp or u2f. - GetSecondFactor() string + GetSecondFactor() constants.SecondFactorType // SetSecondFactor sets the type of second factor: off, otp, or u2f. - SetSecondFactor(string) + SetSecondFactor(constants.SecondFactorType) // GetConnectorName gets the name of the OIDC or SAML connector to use. If // this value is empty, we fall back to the first connector in the backend. @@ -96,7 +96,7 @@ func DefaultAuthPreference() AuthPreference { }, Spec: AuthPreferenceSpecV2{ Type: constants.Local, - SecondFactor: constants.OTP, + SecondFactor: constants.SecondFactorOTP, }, } } @@ -192,12 +192,12 @@ func (c *AuthPreferenceV2) SetType(s string) { } // GetSecondFactor returns the type of second factor. -func (c *AuthPreferenceV2) GetSecondFactor() string { +func (c *AuthPreferenceV2) GetSecondFactor() constants.SecondFactorType { return c.Spec.SecondFactor } // SetSecondFactor sets the type of second factor. -func (c *AuthPreferenceV2) SetSecondFactor(s string) { +func (c *AuthPreferenceV2) SetSecondFactor(s constants.SecondFactorType) { c.Spec.SecondFactor = s } @@ -239,7 +239,7 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { c.Spec.Type = constants.Local } if c.Spec.SecondFactor == "" { - c.Spec.SecondFactor = constants.OTP + c.Spec.SecondFactor = constants.SecondFactorOTP } // make sure type makes sense @@ -251,7 +251,14 @@ func (c *AuthPreferenceV2) CheckAndSetDefaults() error { // make sure second factor makes sense switch c.Spec.SecondFactor { - case constants.OFF, constants.OTP, constants.U2F: + case constants.SecondFactorOff, constants.SecondFactorOTP: + case constants.SecondFactorU2F, constants.SecondFactorOn, constants.SecondFactorOptional: + if c.Spec.U2F == nil { + return trace.BadParameter("missing required U2F configuration for second factor type %q", c.Spec.SecondFactor) + } + if err := c.Spec.U2F.Check(); err != nil { + return trace.Wrap(err) + } default: return trace.BadParameter("second factor type %q not supported", c.Spec.SecondFactor) } @@ -270,7 +277,7 @@ type AuthPreferenceSpecV2 struct { Type string `json:"type"` // SecondFactor is the type of second factor. - SecondFactor string `json:"second_factor,omitempty"` + SecondFactor constants.SecondFactorType `json:"second_factor,omitempty"` // ConnectorName is the name of the OIDC or SAML connector. If this value is // not set the first connector in the backend will be used. @@ -289,6 +296,16 @@ type U2F struct { Facets []string `json:"facets,omitempty"` } +func (u *U2F) Check() error { + if u.AppID == "" { + return trace.BadParameter("u2f configuration missing app_id") + } + if len(u.Facets) == 0 { + return trace.BadParameter("u2f configuration missing facets") + } + return nil +} + // NewMFADevice creates a new MFADevice with the given name. Caller must set // the Device field in the returned MFADevice. func NewMFADevice(name string, addedAt time.Time) *MFADevice { diff --git a/constants.go b/constants.go index 3ee404b05fd..6746fadbb94 100644 --- a/constants.go +++ b/constants.go @@ -26,9 +26,6 @@ import ( // The following constants have been moved to /api/constants/constants.go, and are now // imported here for backwards compatibility. DELETE IN 7.0.0 const ( - OTP = constants.OTP - U2F = constants.U2F - OFF = constants.OFF Local = constants.Local OIDC = constants.OIDC SAML = constants.SAML @@ -296,12 +293,6 @@ const ( // the proxy is recording sessions or not. RecordingProxyReqType = "recording-proxy@teleport.com" - // TOTP means Time-based One-time Password Algorithm. for Two-Factor Authentication. - TOTP = "totp" - - // HOTP means HMAC-based One-time Password Algorithm.for Two-Factor Authentication. - HOTP = "hotp" - // JSON means JSON serialization format JSON = "json" @@ -394,6 +385,16 @@ const ( MinimumEtcdVersion = "3.3.0" ) +// OTPType is the type of the One-time Password Algorithm. +type OTPType string + +const ( + // TOTP means Time-based One-time Password Algorithm (for Two-Factor Authentication) + TOTP = OTPType("totp") + // HOTP means HMAC-based One-time Password Algorithm (for Two-Factor Authentication) + HOTP = OTPType("hotp") +) + const ( // These values are from https://openid.net/specs/openid-connect-core-1_0.html#AuthRequest diff --git a/lib/auth/apiserver.go b/lib/auth/apiserver.go index 8a05faef5c3..069d4855530 100644 --- a/lib/auth/apiserver.go +++ b/lib/auth/apiserver.go @@ -744,7 +744,7 @@ func (s *APIServer) u2fSignRequest(auth ClientI, w http.ResponseWriter, r *http. } user := p.ByName("user") pass := []byte(req.Password) - u2fSignReq, err := auth.GetU2FSignRequest(user, pass) + u2fSignReq, err := auth.GetMFAAuthenticateChallenge(user, pass) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/auth/auth.go b/lib/auth/auth.go index f5dd314fa1b..cae83af5df3 100644 --- a/lib/auth/auth.go +++ b/lib/auth/auth.go @@ -38,6 +38,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/wrappers" "github.com/gravitational/teleport/lib/auth/u2f" @@ -841,63 +842,53 @@ func (a *Server) PreAuthenticatedSignIn(user string, identity tlsca.Identity) (s return sess.WithoutSecrets(), nil } -// U2FAuthenticateChallenge is a U2F authentication challenge sent on user +// MFAAuthenticateChallenge is a U2F authentication challenge sent on user // login. -type U2FAuthenticateChallenge struct { +type MFAAuthenticateChallenge struct { // Before 6.0 teleport would only send 1 U2F challenge. Embed the old // challenge for compatibility with older clients. All new clients should // ignore this and read Challenges instead. *u2f.AuthenticateChallenge - // The list of U2F challenges, one for each registered device. - Challenges []u2f.AuthenticateChallenge `json:"challenges"` + + // U2FChallenges is a list of U2F challenges, one for each registered + // device. + U2FChallenges []u2f.AuthenticateChallenge `json:"u2f_challenges"` + // TOTPChallenge specifies whether TOTP is supported for this user. + TOTPChallenge bool `json:"totp_challenge"` } -func (a *Server) U2FSignRequest(user string, password []byte) (*U2FAuthenticateChallenge, error) { +func (a *Server) GetMFAAuthenticateChallenge(user string, password []byte) (*MFAAuthenticateChallenge, error) { ctx := context.TODO() - cap, err := a.GetAuthPreference() - if err != nil { - return nil, trace.Wrap(err) - } - u2fConfig, err := cap.GetU2F() - if err != nil { - return nil, trace.Wrap(err) - } - - err = a.WithUserLock(user, func() error { + err := a.WithUserLock(user, func() error { return a.CheckPasswordWOToken(user, password) }) if err != nil { return nil, trace.Wrap(err) } - devs, err := a.GetMFADevices(ctx, user) + protoChal, err := a.mfaAuthChallenge(ctx, user, a.Identity) if err != nil { return nil, trace.Wrap(err) } - res := new(U2FAuthenticateChallenge) - for _, dev := range devs { - if dev.GetU2F() == nil { - continue - } - ch, err := u2f.AuthenticateInit(ctx, u2f.AuthenticateInitParams{ - Dev: dev, - AppConfig: *u2fConfig, - StorageKey: user, - Storage: a.Identity, - }) - if err != nil { - return nil, trace.Wrap(err) - } - res.Challenges = append(res.Challenges, *ch) - if res.AuthenticateChallenge == nil { - res.AuthenticateChallenge = ch - } + + // Convert from proto to JSON format. + chal := &MFAAuthenticateChallenge{ + TOTPChallenge: protoChal.TOTP != nil, } - if len(res.Challenges) == 0 { - return nil, trace.NotFound("no U2F devices found for user %q", user) + for _, u2fChal := range protoChal.U2F { + ch := u2f.AuthenticateChallenge{ + Challenge: u2fChal.Challenge, + KeyHandle: u2fChal.KeyHandle, + AppID: u2fChal.AppID, + } + if chal.AuthenticateChallenge == nil { + chal.AuthenticateChallenge = &ch + } + chal.U2FChallenges = append(chal.U2FChallenges, ch) } - return res, nil + + return chal, nil } func (a *Server) CheckU2FSignResponse(ctx context.Context, user string, response *u2f.AuthenticateChallengeResponse) error { @@ -1961,10 +1952,10 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user string, u2fStorage u return nil, trace.Wrap(err) } var enableTOTP, enableU2F bool - switch apref.GetType() { - case teleport.TOTP: + switch apref.GetSecondFactor() { + case constants.SecondFactorOTP: enableTOTP, enableU2F = true, false - case teleport.U2F: + case constants.SecondFactorU2F: enableTOTP, enableU2F = false, true default: // Other AuthPreference types don't restrict us to a single MFA type, diff --git a/lib/auth/auth_test.go b/lib/auth/auth_test.go index af612d6712c..7fafebbab03 100644 --- a/lib/auth/auth_test.go +++ b/lib/auth/auth_test.go @@ -30,6 +30,7 @@ import ( "github.com/google/go-cmp/cmp" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/testauthority" authority "github.com/gravitational/teleport/lib/auth/testauthority" @@ -102,7 +103,7 @@ func (s *AuthSuite) SetUpTest(c *C) { authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.OFF, + SecondFactor: constants.SecondFactorOff, }) c.Assert(err, IsNil) @@ -1044,11 +1045,11 @@ func TestU2FSignChallengeCompat(t *testing.T) { // New format is U2FAuthenticateChallenge as JSON. // Old format was u2f.AuthenticateChallenge as JSON. t.Run("old client, new server", func(t *testing.T) { - newChallenge := &U2FAuthenticateChallenge{ + newChallenge := &MFAAuthenticateChallenge{ AuthenticateChallenge: &u2f.AuthenticateChallenge{ Challenge: "c1", }, - Challenges: []u2f.AuthenticateChallenge{ + U2FChallenges: []u2f.AuthenticateChallenge{ {Challenge: "c1"}, {Challenge: "c2"}, {Challenge: "c3"}, @@ -1070,11 +1071,11 @@ func TestU2FSignChallengeCompat(t *testing.T) { wire, err := json.Marshal(oldChallenge) require.NoError(t, err) - var newChallenge U2FAuthenticateChallenge + var newChallenge MFAAuthenticateChallenge err = json.Unmarshal(wire, &newChallenge) require.NoError(t, err) - require.Empty(t, cmp.Diff(newChallenge, U2FAuthenticateChallenge{AuthenticateChallenge: oldChallenge})) + require.Empty(t, cmp.Diff(newChallenge, MFAAuthenticateChallenge{AuthenticateChallenge: oldChallenge})) }) } diff --git a/lib/auth/auth_with_roles.go b/lib/auth/auth_with_roles.go index 55acf5fd039..8e9a6b157a4 100644 --- a/lib/auth/auth_with_roles.go +++ b/lib/auth/auth_with_roles.go @@ -776,10 +776,10 @@ func (a *ServerWithRoles) PreAuthenticatedSignIn(user string) (services.WebSessi return a.authServer.PreAuthenticatedSignIn(user, a.context.Identity.GetIdentity()) } -func (a *ServerWithRoles) GetU2FSignRequest(user string, password []byte) (*U2FAuthenticateChallenge, error) { +func (a *ServerWithRoles) GetMFAAuthenticateChallenge(user string, password []byte) (*MFAAuthenticateChallenge, error) { // we are already checking password here, no need to extra permission check // anyone who has user's password can generate sign request - return a.authServer.U2FSignRequest(user, password) + return a.authServer.GetMFAAuthenticateChallenge(user, password) } // CreateWebSession creates a new web session for the specified user diff --git a/lib/auth/clt.go b/lib/auth/clt.go index 85fdf01faa6..441ac9cd620 100644 --- a/lib/auth/clt.go +++ b/lib/auth/clt.go @@ -1074,8 +1074,8 @@ func (c *Client) CheckPassword(user string, password []byte, otpToken string) er return trace.Wrap(err) } -// GetU2FSignRequest generates request for user trying to authenticate with U2F token -func (c *Client) GetU2FSignRequest(user string, password []byte) (*U2FAuthenticateChallenge, error) { +// GetMFAAuthenticateChallenge generates request for user trying to authenticate with U2F token +func (c *Client) GetMFAAuthenticateChallenge(user string, password []byte) (*MFAAuthenticateChallenge, error) { out, err := c.PostJSON( c.Endpoint("u2f", "users", user, "sign"), signInReq{ @@ -1085,7 +1085,7 @@ func (c *Client) GetU2FSignRequest(user string, password []byte) (*U2FAuthentica if err != nil { return nil, trace.Wrap(err) } - var signRequest *U2FAuthenticateChallenge + var signRequest *MFAAuthenticateChallenge if err := json.Unmarshal(out.Bytes(), &signRequest); err != nil { return nil, err } @@ -2225,8 +2225,8 @@ type IdentityService interface { // ValidateGithubAuthCallback validates Github auth callback ValidateGithubAuthCallback(q url.Values) (*GithubAuthResponse, error) - // GetU2FSignRequest generates request for user trying to authenticate with U2F token - GetU2FSignRequest(user string, password []byte) (*U2FAuthenticateChallenge, error) + // GetMFAAuthenticateChallenge generates request for user trying to authenticate with U2F token + GetMFAAuthenticateChallenge(user string, password []byte) (*MFAAuthenticateChallenge, error) // GetSignupU2FRegisterRequest generates sign request for user trying to sign up with invite token GetSignupU2FRegisterRequest(token string) (*u2f.RegisterChallenge, error) diff --git a/lib/auth/grpcserver_test.go b/lib/auth/grpcserver_test.go index e033b98695f..52a0b730740 100644 --- a/lib/auth/grpcserver_test.go +++ b/lib/auth/grpcserver_test.go @@ -35,6 +35,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/mocku2f" "github.com/gravitational/teleport/lib/auth/u2f" @@ -54,7 +55,8 @@ func TestMFADeviceManagement(t *testing.T) { require.NoError(t, err) // Enable U2F support. authPref, err := services.NewAuthPreference(types.AuthPreferenceSpecV2{ - Type: teleport.Local, + Type: teleport.Local, + SecondFactor: constants.SecondFactorOn, U2F: &types.U2F{ AppID: "teleport", Facets: []string{"teleport"}, @@ -554,7 +556,8 @@ func TestGenerateUserSingleUseCert(t *testing.T) { // Enable U2F support. authPref, err := services.NewAuthPreference(types.AuthPreferenceSpecV2{ - Type: teleport.Local, + Type: teleport.Local, + SecondFactor: constants.SecondFactorOn, U2F: &types.U2F{ AppID: "teleport", Facets: []string{"teleport"}, diff --git a/lib/auth/helpers.go b/lib/auth/helpers.go index 43520c24de1..ed24c2a54ce 100644 --- a/lib/auth/helpers.go +++ b/lib/auth/helpers.go @@ -27,6 +27,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client" + "github.com/gravitational/teleport/api/constants" authority "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/backend/memory" @@ -183,7 +184,7 @@ func NewTestAuthServer(cfg TestAuthServerConfig) (*TestAuthServer, error) { } authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.OFF, + SecondFactor: constants.SecondFactorOff, }) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/auth/init_test.go b/lib/auth/init_test.go index 4113c153c4d..a1907fecc50 100644 --- a/lib/auth/init_test.go +++ b/lib/auth/init_test.go @@ -32,6 +32,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/auth/u2f" @@ -220,7 +221,7 @@ func (s *AuthInitSuite) TestAuthPreference(c *C) { cap, err := as.GetAuthPreference() c.Assert(err, IsNil) c.Assert(cap.GetType(), Equals, "local") - c.Assert(cap.GetSecondFactor(), Equals, "u2f") + c.Assert(cap.GetSecondFactor(), Equals, constants.SecondFactorU2F) u, err := cap.GetU2F() c.Assert(err, IsNil) c.Assert(u.AppID, Equals, "foo") diff --git a/lib/auth/methods.go b/lib/auth/methods.go index 7cc34c6a614..e6e07bc9f4e 100644 --- a/lib/auth/methods.go +++ b/lib/auth/methods.go @@ -22,7 +22,7 @@ import ( "golang.org/x/crypto/ssh" - "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/u2f" "github.com/gravitational/teleport/lib/events" @@ -121,23 +121,6 @@ func (s *Server) authenticateUser(ctx context.Context, req AuthenticateUserReque } switch { - case req.Pass != nil: - // authenticate using password only, make sure - // that auth preference does not require second factor - // otherwise users can bypass the second factor - if authPreference.GetSecondFactor() != teleport.OFF { - return trace.AccessDenied("missing second factor") - } - err := s.WithUserLock(req.Username, func() error { - return s.CheckPasswordWOToken(req.Username, req.Pass.Password) - }) - if err != nil { - // provide obscure message on purpose, while logging the real - // error server side - log.Debugf("Failed to authenticate: %v.", err) - return trace.AccessDenied("invalid username or password") - } - return nil case req.U2F != nil: // authenticate using U2F - code checks challenge response // signed by U2F device of the user @@ -162,6 +145,41 @@ func (s *Server) authenticateUser(ctx context.Context, req AuthenticateUserReque return trace.AccessDenied("invalid username, password or second factor") } return nil + case req.Pass != nil: + // authenticate using password only, make sure + // that auth preference does not require second factor + // otherwise users can bypass the second factor + switch authPreference.GetSecondFactor() { + case constants.SecondFactorOff: + // No 2FA required, check password only. + case constants.SecondFactorOptional: + // 2FA is optional. Make sure that a user does not have MFA devices + // registered. + devs, err := s.GetMFADevices(ctx, req.Username) + if err != nil && !trace.IsNotFound(err) { + return trace.Wrap(err) + } + if len(devs) != 0 { + log.Warningf("MFA bypass attempt by user %q, access denied.", req.Username) + return trace.AccessDenied("missing second factor authentication") + } + default: + // Some form of MFA is required but none provided. Either client is + // buggy (didn't send MFA response) or someone is trying to bypass + // MFA. + log.Warningf("MFA bypass attempt by user %q, access denied.", req.Username) + return trace.AccessDenied("missing second factor") + } + err := s.WithUserLock(req.Username, func() error { + return s.CheckPasswordWOToken(req.Username, req.Pass.Password) + }) + if err != nil { + // provide obscure message on purpose, while logging the real + // error server side + log.Debugf("Failed to authenticate: %v.", err) + return trace.AccessDenied("invalid username or password") + } + return nil default: return trace.AccessDenied("unsupported authentication method") } diff --git a/lib/auth/password.go b/lib/auth/password.go index 803f8e39451..a3e1190d344 100644 --- a/lib/auth/password.go +++ b/lib/auth/password.go @@ -9,6 +9,7 @@ import ( "github.com/gravitational/trace" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/u2f" "github.com/gravitational/teleport/lib/defaults" @@ -32,7 +33,7 @@ type ChangePasswordWithTokenRequest struct { // Password is user password Password []byte `json:"password"` // U2FRegisterResponse is U2F registration challenge response. - U2FRegisterResponse u2f.RegisterChallengeResponse `json:"u2f_register_response"` + U2FRegisterResponse *u2f.RegisterChallengeResponse `json:"u2f_register_response,omitempty"` } // ChangePasswordWithToken changes password with token @@ -90,16 +91,42 @@ func (s *Server) ChangePassword(req services.ChangePasswordReq) error { fn := func() error { secondFactor := authPreference.GetSecondFactor() switch secondFactor { - case teleport.OFF: + case constants.SecondFactorOff: return s.CheckPasswordWOToken(userID, req.OldPassword) - case teleport.OTP: + case constants.SecondFactorOTP: return s.CheckPassword(userID, req.OldPassword, req.SecondFactorToken) - case teleport.U2F: + case constants.SecondFactorU2F: if req.U2FSignResponse == nil { - return trace.BadParameter("missing U2F sign response") + return trace.AccessDenied("missing U2F sign response") } return s.CheckU2FSignResponse(ctx, userID, req.U2FSignResponse) + case constants.SecondFactorOn: + if req.SecondFactorToken != "" { + return s.CheckPassword(userID, req.OldPassword, req.SecondFactorToken) + } + if req.U2FSignResponse != nil { + return s.CheckU2FSignResponse(ctx, userID, req.U2FSignResponse) + } + return trace.AccessDenied("missing second factor authentication") + case constants.SecondFactorOptional: + if req.SecondFactorToken != "" { + return s.CheckPassword(userID, req.OldPassword, req.SecondFactorToken) + } + if req.U2FSignResponse != nil { + return s.CheckU2FSignResponse(ctx, userID, req.U2FSignResponse) + } + // Check that a user has no MFA devices registered. + devs, err := s.GetMFADevices(ctx, userID) + if err != nil && !trace.IsNotFound(err) { + return trace.Wrap(err) + } + if len(devs) != 0 { + // MFA devices registered but no MFA fields set in request. + log.Warningf("MFA bypass attempt by user %q, access denied.", userID) + return trace.AccessDenied("missing second factor authentication") + } + return nil } return trace.BadParameter("unsupported second factor method: %q", secondFactor) @@ -295,7 +322,7 @@ func (s *Server) CreateSignupU2FRegisterRequest(tokenID string) (*u2f.RegisterCh // getOTPType returns the type of OTP token used, HOTP or TOTP. // Deprecated: Remove this method once HOTP support has been removed from Gravity. -func (s *Server) getOTPType(user string) (string, error) { +func (s *Server) getOTPType(user string) (teleport.OTPType, error) { _, err := s.GetHOTP(user) if err != nil { if trace.IsNotFound(err) { @@ -358,18 +385,22 @@ func (s *Server) changePasswordWithToken(ctx context.Context, req ChangePassword return user, nil } -func (s *Server) changeUserSecondFactor(req ChangePasswordWithTokenRequest, ResetPasswordToken services.ResetPasswordToken) error { - username := ResetPasswordToken.GetUser() +func (s *Server) changeUserSecondFactor(req ChangePasswordWithTokenRequest, token services.ResetPasswordToken) error { + username := token.GetUser() cap, err := s.GetAuthPreference() if err != nil { return trace.Wrap(err) } ctx := context.TODO() - switch cap.GetSecondFactor() { - case teleport.OFF: + secondFactor := cap.GetSecondFactor() + if secondFactor == constants.SecondFactorOff { return nil - case teleport.OTP, teleport.TOTP, teleport.HOTP: + } + if req.SecondFactorToken != "" { + if secondFactor == constants.SecondFactorU2F { + return trace.BadParameter("user %q sent an OTP token during password reset but cluster only allows U2F for second factor", username) + } secrets, err := s.Identity.GetResetPasswordTokenSecrets(ctx, req.TokenID) if err != nil { return trace.Wrap(err) @@ -387,7 +418,11 @@ func (s *Server) changeUserSecondFactor(req ChangePasswordWithTokenRequest, Rese } return nil - case teleport.U2F: + } + if req.U2FRegisterResponse != nil { + if secondFactor == constants.SecondFactorOTP { + return trace.BadParameter("user %q sent a U2F registration during password reset but cluster only allows OTP for second factor", username) + } _, err = cap.GetU2F() if err != nil { return trace.Wrap(err) @@ -397,12 +432,15 @@ func (s *Server) changeUserSecondFactor(req ChangePasswordWithTokenRequest, Rese DevName: "u2f", ChallengeStorageKey: req.TokenID, RegistrationStorageKey: username, - Resp: req.U2FRegisterResponse, + Resp: *req.U2FRegisterResponse, Storage: s.Identity, Clock: s.GetClock(), }) return trace.Wrap(err) - default: - return trace.BadParameter("unknown second factor type %q", cap.GetSecondFactor()) } + + if secondFactor != constants.SecondFactorOptional { + return trace.BadParameter("no second factor sent during user %q password reset", username) + } + return nil } diff --git a/lib/auth/password_test.go b/lib/auth/password_test.go index f4d2ecdcb1d..5f0497a9564 100644 --- a/lib/auth/password_test.go +++ b/lib/auth/password_test.go @@ -25,6 +25,7 @@ import ( "time" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" authority "github.com/gravitational/teleport/lib/auth/testauthority" "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/backend/lite" @@ -179,7 +180,7 @@ func (s *PasswordSuite) TestUserNotFound(c *C) { } func (s *PasswordSuite) TestChangePassword(c *C) { - req, err := s.prepareForPasswordChange("user1", []byte("abc123"), teleport.OFF) + req, err := s.prepareForPasswordChange("user1", []byte("abc123"), constants.SecondFactorOff) c.Assert(err, IsNil) fakeClock := clockwork.NewFakeClock() @@ -202,7 +203,7 @@ func (s *PasswordSuite) TestChangePassword(c *C) { } func (s *PasswordSuite) TestChangePasswordWithOTP(c *C) { - req, err := s.prepareForPasswordChange("user2", []byte("abc123"), teleport.OTP) + req, err := s.prepareForPasswordChange("user2", []byte("abc123"), constants.SecondFactorOTP) c.Assert(err, IsNil) fakeClock := clockwork.NewFakeClock() @@ -240,7 +241,7 @@ func (s *PasswordSuite) TestChangePasswordWithOTP(c *C) { func (s *PasswordSuite) TestChangePasswordWithToken(c *C) { authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.OFF, + SecondFactor: constants.SecondFactorOff, }) c.Assert(err, IsNil) @@ -271,7 +272,7 @@ func (s *PasswordSuite) TestChangePasswordWithToken(c *C) { func (s *PasswordSuite) TestChangePasswordWithTokenOTP(c *C) { authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.OTP, + SecondFactor: constants.SecondFactorOTP, }) c.Assert(err, IsNil) @@ -308,7 +309,7 @@ func (s *PasswordSuite) TestChangePasswordWithTokenOTP(c *C) { func (s *PasswordSuite) TestChangePasswordWithTokenErrors(c *C) { authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.OTP, + SecondFactor: constants.SecondFactorOTP, }) c.Assert(err, IsNil) @@ -326,13 +327,13 @@ func (s *PasswordSuite) TestChangePasswordWithTokenErrors(c *C) { type testCase struct { desc string - secondFactor string + secondFactor constants.SecondFactorType req ChangePasswordWithTokenRequest } testCases := []testCase{ { - secondFactor: teleport.OFF, + secondFactor: constants.SecondFactorOff, desc: "invalid tokenID value", req: ChangePasswordWithTokenRequest{ TokenID: "what_token", @@ -340,7 +341,7 @@ func (s *PasswordSuite) TestChangePasswordWithTokenErrors(c *C) { }, }, { - secondFactor: teleport.OFF, + secondFactor: constants.SecondFactorOff, desc: "invalid password", req: ChangePasswordWithTokenRequest{ TokenID: validTokenID, @@ -348,7 +349,7 @@ func (s *PasswordSuite) TestChangePasswordWithTokenErrors(c *C) { }, }, { - secondFactor: teleport.OTP, + secondFactor: constants.SecondFactorOTP, desc: "missing second factor", req: ChangePasswordWithTokenRequest{ TokenID: validTokenID, @@ -356,7 +357,7 @@ func (s *PasswordSuite) TestChangePasswordWithTokenErrors(c *C) { }, }, { - secondFactor: teleport.OTP, + secondFactor: constants.SecondFactorOTP, desc: "invalid OTP value", req: ChangePasswordWithTokenRequest{ TokenID: validTokenID, @@ -376,7 +377,7 @@ func (s *PasswordSuite) TestChangePasswordWithTokenErrors(c *C) { c.Assert(err, NotNil, Commentf("test case %q", tc.desc)) } - authPreference.SetSecondFactor(teleport.OFF) + authPreference.SetSecondFactor(constants.SecondFactorOff) err = s.a.SetAuthPreference(authPreference) c.Assert(err, IsNil) @@ -408,7 +409,7 @@ func (s *PasswordSuite) shouldLockAfterFailedAttempts(c *C, req services.ChangeP c.Assert(trace.IsAccessDenied(err), Equals, true) } -func (s *PasswordSuite) prepareForPasswordChange(user string, pass []byte, secondFactorType string) (services.ChangePasswordReq, error) { +func (s *PasswordSuite) prepareForPasswordChange(user string, pass []byte, secondFactorType constants.SecondFactorType) (services.ChangePasswordReq, error) { req := services.ChangePasswordReq{ User: user, OldPassword: pass, diff --git a/lib/auth/tls_test.go b/lib/auth/tls_test.go index 73f48152f63..608b72394ab 100644 --- a/lib/auth/tls_test.go +++ b/lib/auth/tls_test.go @@ -37,6 +37,7 @@ import ( "github.com/gravitational/teleport" "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/events" @@ -2207,7 +2208,7 @@ func (s *TLSSuite) TestAuthenticateWebUserOTP(c *check.C) { authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.OTP, + SecondFactor: constants.SecondFactorOTP, }) c.Assert(err, check.IsNil) err = s.server.Auth().SetAuthPreference(authPreference) @@ -2311,7 +2312,7 @@ func (s *TLSSuite) TestChangePasswordWithToken(c *check.C) { authPreference, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.OTP, + SecondFactor: constants.SecondFactorOTP, }) c.Assert(err, check.IsNil) diff --git a/lib/client/api.go b/lib/client/api.go index 81c48a3afc3..3187ea9b700 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -45,6 +45,7 @@ import ( "golang.org/x/crypto/ssh/terminal" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/types/wrappers" "github.com/gravitational/teleport/lib/auth" @@ -2238,18 +2239,21 @@ func (tc *TeleportClient) applyProxySettings(proxySettings ProxySettings) error return nil } -func (tc *TeleportClient) localLogin(ctx context.Context, secondFactor string, pub []byte) (*auth.SSHLoginResponse, error) { +func (tc *TeleportClient) localLogin(ctx context.Context, secondFactor constants.SecondFactorType, pub []byte) (*auth.SSHLoginResponse, error) { var err error var response *auth.SSHLoginResponse + // TODO(awly): mfa: ideally, clients should always go through mfaLocalLogin + // (with a nop MFA challenge if no 2nd factor is required). That way we can + // deprecate the direct login endpoint. switch secondFactor { - case teleport.OFF, teleport.OTP, teleport.TOTP, teleport.HOTP: + case constants.SecondFactorOff, constants.SecondFactorOTP: response, err = tc.directLogin(ctx, secondFactor, pub) if err != nil { return nil, trace.Wrap(err) } - case teleport.U2F: - response, err = tc.u2fLogin(ctx, pub) + case constants.SecondFactorU2F, constants.SecondFactorOn, constants.SecondFactorOptional: + response, err = tc.mfaLocalLogin(ctx, pub) if err != nil { return nil, trace.Wrap(err) } @@ -2291,7 +2295,7 @@ func (tc *TeleportClient) AddKey(host string, key *Key) (*agent.AddedKey, error) } // directLogin asks for a password + HOTP token, makes a request to CA via proxy -func (tc *TeleportClient) directLogin(ctx context.Context, secondFactorType string, pub []byte) (*auth.SSHLoginResponse, error) { +func (tc *TeleportClient) directLogin(ctx context.Context, secondFactorType constants.SecondFactorType, pub []byte) (*auth.SSHLoginResponse, error) { var err error var password string @@ -2303,7 +2307,7 @@ func (tc *TeleportClient) directLogin(ctx context.Context, secondFactorType stri } // only ask for a second factor if it's enabled - if secondFactorType != teleport.OFF { + if secondFactorType == constants.SecondFactorOTP { otpToken, err = tc.AskOTP() if err != nil { return nil, trace.Wrap(err) @@ -2359,14 +2363,14 @@ func (tc *TeleportClient) ssoLogin(ctx context.Context, connectorID string, pub return response, trace.Wrap(err) } -// directLogin asks for a password and performs the challenge-response authentication -func (tc *TeleportClient) u2fLogin(ctx context.Context, pub []byte) (*auth.SSHLoginResponse, error) { +// mfaLocalLogin asks for a password and performs the challenge-response authentication +func (tc *TeleportClient) mfaLocalLogin(ctx context.Context, pub []byte) (*auth.SSHLoginResponse, error) { password, err := tc.AskPassword() if err != nil { return nil, trace.Wrap(err) } - response, err := SSHAgentU2FLogin(ctx, SSHLoginU2F{ + response, err := SSHAgentMFALogin(ctx, SSHLoginMFA{ SSHLogin: SSHLogin{ ProxyAddr: tc.WebProxyAddr, PubKey: pub, diff --git a/lib/client/mfa.go b/lib/client/mfa.go new file mode 100644 index 00000000000..c8a8061e47a --- /dev/null +++ b/lib/client/mfa.go @@ -0,0 +1,140 @@ +/* +Copyright 2021 Gravitational, Inc. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package client + +import ( + "context" + "fmt" + "os" + "strings" + + "github.com/gravitational/trace" + + "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/lib/auth/u2f" + "github.com/gravitational/teleport/lib/utils/prompt" +) + +// PromptMFAChallenge prompts the user to complete MFA authentication +// challenges. +// +// If promptDevicePrefix is set, it will be printed in prompts before "security +// key" or "device". This is used to emphasize between different kinds of +// devices, like registered vs new. +func PromptMFAChallenge(ctx context.Context, proxyAddr string, c *proto.MFAAuthenticateChallenge, promptDevicePrefix string) (*proto.MFAAuthenticateResponse, error) { + switch { + // No challenge. + case c.TOTP == nil && len(c.U2F) == 0: + return &proto.MFAAuthenticateResponse{}, nil + // TOTP only. + case c.TOTP != nil && len(c.U2F) == 0: + totpCode, err := prompt.Input(os.Stdout, os.Stdin, fmt.Sprintf("Enter an OTP code from a %sdevice", promptDevicePrefix)) + if err != nil { + return nil, trace.Wrap(err) + } + return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_TOTP{ + TOTP: &proto.TOTPResponse{Code: totpCode}, + }}, nil + // U2F only. + case c.TOTP == nil && len(c.U2F) > 0: + fmt.Printf("Tap any %ssecurity key\n", promptDevicePrefix) + + return promptU2FChallenges(ctx, proxyAddr, c.U2F) + // Both TOTP and U2F. + case c.TOTP != nil && len(c.U2F) > 0: + ctx, cancel := context.WithCancel(ctx) + defer cancel() + + type response struct { + kind string + resp *proto.MFAAuthenticateResponse + err error + } + resCh := make(chan response, 1) + + go func() { + resp, err := promptU2FChallenges(ctx, proxyAddr, c.U2F) + select { + case resCh <- response{kind: "U2F", resp: resp, err: err}: + case <-ctx.Done(): + } + }() + + go func() { + totpCode, err := prompt.Input(os.Stdout, os.Stdin, fmt.Sprintf("Tap any %[1]ssecurity key or enter a code from a %[1]sOTP device", promptDevicePrefix, promptDevicePrefix)) + res := response{kind: "TOTP", err: err} + if err == nil { + res.resp = &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_TOTP{ + TOTP: &proto.TOTPResponse{Code: totpCode}, + }} + } + + select { + case resCh <- res: + case <-ctx.Done(): + } + }() + + for i := 0; i < 2; i++ { + select { + case res := <-resCh: + if res.err != nil { + log.WithError(res.err).Debugf("%s authentication failed", res.kind) + continue + } + + // Print a newline after the TOTP prompt, so that any future + // output doesn't print on the prompt line. + fmt.Println() + + return res.resp, nil + case <-ctx.Done(): + return nil, trace.Wrap(ctx.Err()) + } + } + return nil, trace.Errorf("failed to authenticate using all U2F and TOTP devices, rerun the command with '-d' to see error details for each device") + default: + return nil, trace.BadParameter("bug: non-exhaustive switch in promptMFAChallenge") + } +} + +func promptU2FChallenges(ctx context.Context, proxyAddr string, challenges []*proto.U2FChallenge) (*proto.MFAAuthenticateResponse, error) { + facet := proxyAddr + if !strings.HasPrefix(proxyAddr, "https://") { + facet = "https://" + facet + } + u2fChallenges := make([]u2f.AuthenticateChallenge, 0, len(challenges)) + for _, chal := range challenges { + u2fChallenges = append(u2fChallenges, u2f.AuthenticateChallenge{ + Challenge: chal.Challenge, + KeyHandle: chal.KeyHandle, + AppID: chal.AppID, + }) + } + + resp, err := u2f.AuthenticateSignChallenge(ctx, facet, u2fChallenges...) + if err != nil { + return nil, trace.Wrap(err) + } + return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_U2F{ + U2F: &proto.U2FResponse{ + KeyHandle: resp.KeyHandle, + ClientData: resp.ClientData, + Signature: resp.SignatureData, + }, + }}, nil +} diff --git a/lib/client/weblogin.go b/lib/client/weblogin.go index 03818372f97..a54f11f614e 100644 --- a/lib/client/weblogin.go +++ b/lib/client/weblogin.go @@ -25,10 +25,11 @@ import ( "net/url" "os/exec" "runtime" - "strings" "time" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/client/proto" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/auth/u2f" "github.com/gravitational/teleport/lib/defaults" @@ -80,8 +81,9 @@ type SSOLoginConsoleResponse struct { RedirectURL string `json:"redirect_url"` } -// U2fSignRequestReq is a request from the client for a U2F sign request from the server -type U2fSignRequestReq struct { +// MFAChallengeRequest is a request from the client for a MFA challenge from the +// server. +type MFAChallengeRequest struct { User string `json:"user"` Pass string `json:"pass"` } @@ -114,15 +116,20 @@ type CreateSSHCertReq struct { KubernetesCluster string } -// CreateSSHCertWithU2FReq are passed by web client +// CreateSSHCertWithMFAReq are passed by web client // to authenticate against teleport server and receive // a temporary cert signed by auth server authority -type CreateSSHCertWithU2FReq struct { +type CreateSSHCertWithMFAReq struct { // User is a teleport username User string `json:"user"` - // We only issue U2F sign requests after checking the password, so there's no need to check again. + // Password for the user, to authenticate in case no MFA check was + // performed. + Password string `json:"password"` + // U2FSignResponse is the signature from the U2F device - U2FSignResponse u2f.AuthenticateChallengeResponse `json:"u2f_sign_response"` + U2FSignResponse *u2f.AuthenticateChallengeResponse `json:"u2f_sign_response"` + // TOTPCode is a code from the TOTP device. + TOTPCode string `json:"totp_code"` // PubKey is a public key user wishes to sign PubKey []byte `json:"pub_key"` // TTL is a desired TTL for the cert (max is still capped by server, @@ -201,8 +208,8 @@ type SSHLoginDirect struct { OTPToken string } -// SSHLoginU2F contains SSH login parameters for U2F login. -type SSHLoginU2F struct { +// SSHLoginMFA contains SSH login parameters for MFA login. +type SSHLoginMFA struct { SSHLogin // User is the login username. User string @@ -262,7 +269,7 @@ type AuthenticationSettings struct { Type string `json:"type"` // SecondFactor is the type of second factor to use in authentication. // Supported options are: off, otp, and u2f. - SecondFactor string `json:"second_factor,omitempty"` + SecondFactor constants.SecondFactorType `json:"second_factor,omitempty"` // U2F contains the Universal Second Factor settings needed for authentication. U2F *U2FSettings `json:"u2f,omitempty"` // OIDC contains OIDC connector settings needed for authentication. @@ -495,17 +502,18 @@ func SSHAgentLogin(ctx context.Context, login SSHLoginDirect) (*auth.SSHLoginRes return out, nil } -// SSHAgentU2FLogin requests a U2F sign request (authentication challenge) via -// the proxy. If the credentials are valid, the proxy wiil return a challenge. -// We then perform the signing and pass the signature to the proxy. If the +// SSHAgentMFALogin requests a MFA challenge (U2F or OTP) via the proxy. If the +// credentials are valid, the proxy wiil return a challenge. We then prompt the +// user to provide 2nd factor and pass the response to the proxy. If the // authentication succeeds, we will get a temporary certificate back. -func SSHAgentU2FLogin(ctx context.Context, login SSHLoginU2F) (*auth.SSHLoginResponse, error) { +func SSHAgentMFALogin(ctx context.Context, login SSHLoginMFA) (*auth.SSHLoginResponse, error) { clt, _, err := initClient(login.ProxyAddr, login.Insecure, login.Pool) if err != nil { return nil, trace.Wrap(err) } - challengeRaw, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "u2f", "signrequest"), U2fSignRequestReq{ + // TODO(awly): mfa: rename endpoint + chalRaw, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "u2f", "signrequest"), MFAChallengeRequest{ User: login.User, Pass: login.Password, }) @@ -513,48 +521,69 @@ func SSHAgentU2FLogin(ctx context.Context, login SSHLoginU2F) (*auth.SSHLoginRes return nil, trace.Wrap(err) } - var res auth.U2FAuthenticateChallenge - if err := json.Unmarshal(challengeRaw.Bytes(), &res); err != nil { + var chal auth.MFAAuthenticateChallenge + if err := json.Unmarshal(chalRaw.Bytes(), &chal); err != nil { return nil, trace.Wrap(err) } - if len(res.Challenges) == 0 { + if len(chal.U2FChallenges) == 0 && chal.AuthenticateChallenge != nil { // Challenge sent by a pre-6.0 auth server, fall back to the old // single-device format. - if res.AuthenticateChallenge == nil { - // This shouldn't happen with a well-behaved auth server, but check - // anyway. - return nil, trace.BadParameter("server sent no U2F challenges") - } - res.Challenges = []u2f.AuthenticateChallenge{*res.AuthenticateChallenge} + chal.U2FChallenges = []u2f.AuthenticateChallenge{*chal.AuthenticateChallenge} } - fmt.Println("Please press the button on your U2F key") - facet := "https://" + strings.ToLower(login.ProxyAddr) - challengeResp, err := u2f.AuthenticateSignChallenge(ctx, facet, res.Challenges...) + // Convert to auth gRPC proto challenge. + protoChal := new(proto.MFAAuthenticateChallenge) + if chal.TOTPChallenge { + protoChal.TOTP = new(proto.TOTPChallenge) + } + for _, u2fChal := range chal.U2FChallenges { + protoChal.U2F = append(protoChal.U2F, &proto.U2FChallenge{ + KeyHandle: u2fChal.KeyHandle, + Challenge: u2fChal.Challenge, + AppID: u2fChal.AppID, + }) + } + + protoResp, err := PromptMFAChallenge(ctx, login.ProxyAddr, protoChal, "") if err != nil { return nil, trace.Wrap(err) } - re, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "u2f", "certs"), CreateSSHCertWithU2FReq{ + chalResp := CreateSSHCertWithMFAReq{ User: login.User, - U2FSignResponse: *challengeResp, + Password: login.Password, PubKey: login.PubKey, TTL: login.TTL, Compatibility: login.Compatibility, RouteToCluster: login.RouteToCluster, KubernetesCluster: login.KubernetesCluster, - }) + } + // Convert back from auth gRPC proto response. + switch r := protoResp.Response.(type) { + case *proto.MFAAuthenticateResponse_TOTP: + chalResp.TOTPCode = r.TOTP.Code + case *proto.MFAAuthenticateResponse_U2F: + chalResp.U2FSignResponse = &u2f.AuthenticateChallengeResponse{ + KeyHandle: r.U2F.KeyHandle, + SignatureData: r.U2F.Signature, + ClientData: r.U2F.ClientData, + } + default: + // No challenge was sent, so we send back just username/password. + } + + loginRespRaw, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "u2f", "certs"), chalResp) if err != nil { return nil, trace.Wrap(err) } - var out *auth.SSHLoginResponse - err = json.Unmarshal(re.Bytes(), &out) + var loginResp *auth.SSHLoginResponse + err = json.Unmarshal(loginRespRaw.Bytes(), &loginResp) if err != nil { return nil, trace.Wrap(err) } - return out, nil + return loginResp, nil } // HostCredentials is used to fetch host credentials for a node. diff --git a/lib/config/fileconf.go b/lib/config/fileconf.go index bf4dd0c9966..8f953d4b66b 100644 --- a/lib/config/fileconf.go +++ b/lib/config/fileconf.go @@ -32,6 +32,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/backend" "github.com/gravitational/teleport/lib/bpf" @@ -739,10 +740,10 @@ func (t StaticToken) Parse() (*services.ProvisionTokenV1, error) { // AuthenticationConfig describes the auth_service/authentication section of teleport.yaml type AuthenticationConfig struct { - Type string `yaml:"type"` - SecondFactor string `yaml:"second_factor,omitempty"` - ConnectorName string `yaml:"connector_name,omitempty"` - U2F *UniversalSecondFactor `yaml:"u2f,omitempty"` + Type string `yaml:"type"` + SecondFactor constants.SecondFactorType `yaml:"second_factor,omitempty"` + ConnectorName string `yaml:"connector_name,omitempty"` + U2F *UniversalSecondFactor `yaml:"u2f,omitempty"` // LocalAuth controls if local authentication is allowed. LocalAuth *services.Bool `yaml:"local_auth"` diff --git a/lib/services/suite/suite.go b/lib/services/suite/suite.go index e0bfad4bdd9..d9587d19dd1 100644 --- a/lib/services/suite/suite.go +++ b/lib/services/suite/suite.go @@ -31,6 +31,7 @@ import ( "golang.org/x/crypto/ssh" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/u2f" "github.com/gravitational/teleport/lib/backend" @@ -1054,7 +1055,7 @@ func (s *ServicesTestSuite) AuthPreference(c *check.C) { c.Assert(err, check.IsNil) c.Assert(gotAP.GetType(), check.Equals, "local") - c.Assert(gotAP.GetSecondFactor(), check.Equals, "otp") + c.Assert(gotAP.GetSecondFactor(), check.Equals, constants.SecondFactorOTP) } func (s *ServicesTestSuite) StaticTokens(c *check.C) { diff --git a/lib/web/apiserver.go b/lib/web/apiserver.go index d05e4993312..5e524da535b 100644 --- a/lib/web/apiserver.go +++ b/lib/web/apiserver.go @@ -36,6 +36,7 @@ import ( "time" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/auth/u2f" @@ -53,13 +54,14 @@ import ( "github.com/gravitational/teleport/lib/web/ui" "github.com/gravitational/roundtrip" - "github.com/gravitational/teleport/lib/secret" "github.com/gravitational/trace" "github.com/jonboulle/clockwork" "github.com/julienschmidt/httprouter" lemma_secret "github.com/mailgun/lemma/secret" "github.com/sirupsen/logrus" "golang.org/x/crypto/ssh" + + "github.com/gravitational/teleport/lib/secret" ) // Handler is HTTP web proxy handler @@ -309,9 +311,9 @@ func NewHandler(cfg Config, opts ...HandlerOption) (*RewritingHandler, error) { // U2F related APIs h.GET("/webapi/u2f/signuptokens/:token", httplib.MakeHandler(h.u2fRegisterRequest)) h.POST("/webapi/u2f/password/changerequest", h.WithAuth(h.u2fChangePasswordRequest)) - h.POST("/webapi/u2f/signrequest", httplib.MakeHandler(h.u2fSignRequest)) + h.POST("/webapi/u2f/signrequest", httplib.MakeHandler(h.mfaChallengeRequest)) h.POST("/webapi/u2f/sessions", httplib.MakeHandler(h.createSessionWithU2FSignResponse)) - h.POST("/webapi/u2f/certs", httplib.MakeHandler(h.createSSHCertWithU2FSignResponse)) + h.POST("/webapi/u2f/certs", httplib.MakeHandler(h.createSSHCertWithMFAChallengeResponse)) // trusted clusters h.POST("/webapi/trustedclusters/validate", httplib.MakeHandler(h.validateTrustedCluster)) @@ -502,15 +504,16 @@ func localSettings(authClient auth.ClientI, cap services.AuthPreference) (client SecondFactor: cap.GetSecondFactor(), } - // if the type is u2f, pull some additional data back - if cap.GetSecondFactor() == teleport.U2F { - u2fs, err := cap.GetU2F() - if err != nil { - return client.AuthenticationSettings{}, trace.Wrap(err) - } - - as.U2F = &client.U2FSettings{AppID: u2fs.AppID} + // Add U2F settings, if available. + u2fs, err := cap.GetU2F() + if trace.IsNotFound(err) { + // No U2F settings. + return as, nil } + if err != nil { + return client.AuthenticationSettings{}, trace.Wrap(err) + } + as.U2F = &client.U2FSettings{AppID: u2fs.AppID} return as, nil } @@ -702,7 +705,7 @@ func (h *Handler) getWebConfig(w http.ResponseWriter, r *http.Request, p httprou httplib.SetWebConfigHeaders(w.Header()) authProviders := []ui.WebConfigAuthProvider{} - secondFactor := teleport.OFF + secondFactor := constants.SecondFactorOff // get all OIDC connectors oidcConnectors, err := h.cfg.ProxyClient.GetOIDCConnectors(false) @@ -1243,9 +1246,9 @@ func (h *Handler) createWebSession(w http.ResponseWriter, r *http.Request, p htt var webSession services.WebSession switch cap.GetSecondFactor() { - case teleport.OFF: + case constants.SecondFactorOff: webSession, err = h.auth.AuthWithoutOTP(req.User, req.Pass) - case teleport.OTP, teleport.HOTP, teleport.TOTP: + case constants.SecondFactorOTP, constants.SecondFactorOn, constants.SecondFactorOptional: webSession, err = h.auth.AuthWithOTP(req.User, req.Pass, req.SecondFactorToken) default: return nil, trace.AccessDenied("unknown second factor type: %q", cap.GetSecondFactor()) @@ -1427,7 +1430,7 @@ func (h *Handler) u2fRegisterRequest(w http.ResponseWriter, r *http.Request, p h return u2fRegisterRequest, nil } -// u2fSignRequest is called to get a U2F challenge for authenticating +// mfaChallengeRequest is called to get a MFA challenge for authenticating // // POST /webapi/u2f/signrequest // @@ -1437,17 +1440,17 @@ func (h *Handler) u2fRegisterRequest(w http.ResponseWriter, r *http.Request, p h // // {"version":"U2F_V2","challenge":"randombase64string","keyHandle":"longbase64string","appId":"https://mycorp.com:3080"} // -func (h *Handler) u2fSignRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { - var req *client.U2fSignRequestReq +func (h *Handler) mfaChallengeRequest(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { + var req *client.MFAChallengeRequest if err := httplib.ReadJSON(r, &req); err != nil { return nil, trace.Wrap(err) } - u2fSignReq, err := h.auth.GetU2FSignRequest(req.User, req.Pass) + mfaChallenge, err := h.auth.GetMFAAuthenticateChallenge(req.User, req.Pass) if err != nil { return nil, trace.AccessDenied("bad auth credentials") } - return u2fSignReq, nil + return mfaChallenge, nil } // A request from the client to send the signature from the U2F key @@ -2095,9 +2098,9 @@ func (h *Handler) createSSHCert(w http.ResponseWriter, r *http.Request, p httpro var cert *auth.SSHLoginResponse switch cap.GetSecondFactor() { - case teleport.OFF: + case constants.SecondFactorOff: cert, err = h.auth.GetCertificateWithoutOTP(*req) - case teleport.OTP, teleport.HOTP, teleport.TOTP: + case constants.SecondFactorOTP, constants.SecondFactorOn, constants.SecondFactorOptional: // convert legacy requests to new parameter here. remove once migration to TOTP is complete. if req.HOTPToken != "" { req.OTPToken = req.HOTPToken @@ -2113,8 +2116,9 @@ func (h *Handler) createSSHCert(w http.ResponseWriter, r *http.Request, p httpro return cert, nil } -// createSSHCertWithU2FSignResponse is a web call that generates new SSH certificate based -// on user's name, password, U2F signature and public key user wishes to sign +// createSSHCertWithMFAChallengeResponse is a web call that generates new SSH +// certificate based on user's name, password, MFA response and public key user +// wishes to sign. // // POST /v1/webapi/u2f/certs // @@ -2124,13 +2128,13 @@ func (h *Handler) createSSHCert(w http.ResponseWriter, r *http.Request, p httpro // // { "cert": "base64 encoded signed cert", "host_signers": [{"domain_name": "example.com", "checking_keys": ["base64 encoded public signing key"]}] } // -func (h *Handler) createSSHCertWithU2FSignResponse(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { - var req *client.CreateSSHCertWithU2FReq +func (h *Handler) createSSHCertWithMFAChallengeResponse(w http.ResponseWriter, r *http.Request, p httprouter.Params) (interface{}, error) { + var req *client.CreateSSHCertWithMFAReq if err := httplib.ReadJSON(r, &req); err != nil { return nil, trace.Wrap(err) } - cert, err := h.auth.GetCertificateWithU2F(*req) + cert, err := h.auth.GetCertificateWithMFA(*req) if err != nil { return nil, trace.Wrap(err) } diff --git a/lib/web/apiserver_test.go b/lib/web/apiserver_test.go index ec5d36470cd..b90eb409d2e 100644 --- a/lib/web/apiserver_test.go +++ b/lib/web/apiserver_test.go @@ -44,6 +44,7 @@ import ( "golang.org/x/text/encoding/unicode" "github.com/gravitational/teleport" + "github.com/gravitational/teleport/api/constants" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth" "github.com/gravitational/teleport/lib/auth/mocku2f" @@ -325,7 +326,7 @@ func (s *WebSuite) authPack(c *C, user string) *authPack { ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.OTP, + SecondFactor: constants.SecondFactorOTP, }) c.Assert(err, IsNil) err = s.server.Auth().SetAuthPreference(ap) @@ -1204,7 +1205,7 @@ func (s *WebSuite) TestPlayback(c *C) { func (s *WebSuite) TestLogin(c *C) { ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.OFF, + SecondFactor: constants.SecondFactorOff, }) c.Assert(err, IsNil) err = s.server.Auth().SetAuthPreference(ap) @@ -1271,7 +1272,7 @@ func (s *WebSuite) TestLogin(c *C) { func (s *WebSuite) TestChangePasswordWithTokenOTP(c *C) { ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.OTP, + SecondFactor: constants.SecondFactorOTP, }) c.Assert(err, IsNil) err = s.server.Auth().SetAuthPreference(ap) @@ -1331,7 +1332,7 @@ func (s *WebSuite) TestChangePasswordWithTokenOTP(c *C) { func (s *WebSuite) TestChangePasswordWithTokenU2F(c *C) { ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.U2F, + SecondFactor: constants.SecondFactorU2F, U2F: &services.U2F{ AppID: "https://" + s.server.ClusterName(), Facets: []string{"https://" + s.server.ClusterName()}, @@ -1362,7 +1363,7 @@ func (s *WebSuite) TestChangePasswordWithTokenU2F(c *C) { data, err := json.Marshal(auth.ChangePasswordWithTokenRequest{ TokenID: token.GetName(), Password: []byte("qweQWE"), - U2FRegisterResponse: *u2fRegResp, + U2FRegisterResponse: u2fRegResp, }) c.Assert(err, IsNil) @@ -1384,72 +1385,94 @@ func (s *WebSuite) TestChangePasswordWithTokenU2F(c *C) { c.Assert(rawSess.Token != "", Equals, true) } -func (s *WebSuite) TestU2FLogin(c *C) { +func TestU2FLogin(t *testing.T) { + for _, sf := range []constants.SecondFactorType{ + constants.SecondFactorU2F, + constants.SecondFactorOptional, + constants.SecondFactorOn, + constants.SecondFactorOff, + } { + sf := sf + t.Run(fmt.Sprintf("second_factor_%s", sf), func(t *testing.T) { + t.Parallel() + testU2FLogin(t, sf) + }) + } +} + +func testU2FLogin(t *testing.T, secondFactor constants.SecondFactorType) { + env := newWebPack(t, 1) + defer env.close(t) + // configure cluster authentication preferences cap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.U2F, + SecondFactor: constants.SecondFactorU2F, U2F: &services.U2F{ - AppID: "https://" + s.server.ClusterName(), - Facets: []string{"https://" + s.server.ClusterName()}, + AppID: "https://" + env.server.ClusterName(), + Facets: []string{"https://" + env.server.ClusterName()}, }, }) - c.Assert(err, IsNil) - err = s.server.Auth().SetAuthPreference(cap) - c.Assert(err, IsNil) + require.NoError(t, err) + err = env.server.Auth().SetAuthPreference(cap) + require.NoError(t, err) // create user - s.createUser(c, "bob", "root", "password", "") + ctx := context.TODO() + env.proxies[0].createUser(ctx, t, "bob", "root", "password", "") // create password change token - token, err := s.server.Auth().CreateResetPasswordToken(context.TODO(), auth.CreateResetPasswordTokenRequest{ + token, err := env.server.Auth().CreateResetPasswordToken(context.TODO(), auth.CreateResetPasswordTokenRequest{ Name: "bob", }) - c.Assert(err, IsNil) + require.NoError(t, err) - u2fRegReq, err := s.proxyClient.GetSignupU2FRegisterRequest(token.GetName()) - c.Assert(err, IsNil) + u2fRegReq, err := env.proxies[0].client.GetSignupU2FRegisterRequest(token.GetName()) + require.NoError(t, err) - u2fRegResp, err := s.mockU2F.RegisterResponse(u2fRegReq) - c.Assert(err, IsNil) + mockU2F, err := mocku2f.Create() + require.NoError(t, err) + u2fRegResp, err := mockU2F.RegisterResponse(u2fRegReq) + require.NoError(t, err) tempPass := []byte("abc123") - _, err = s.proxyClient.ChangePasswordWithToken(context.TODO(), auth.ChangePasswordWithTokenRequest{ + _, err = env.proxies[0].client.ChangePasswordWithToken(context.TODO(), auth.ChangePasswordWithTokenRequest{ TokenID: token.GetName(), - U2FRegisterResponse: *u2fRegResp, + U2FRegisterResponse: u2fRegResp, Password: tempPass, }) - c.Assert(err, IsNil) + require.NoError(t, err) // normal login - clt := s.client() - re, err := clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.U2fSignRequestReq{ + clt, err := client.NewWebClient(env.proxies[0].webURL.String(), roundtrip.HTTPClient(client.NewInsecureWebClient())) + require.NoError(t, err) + re, err := clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.MFAChallengeRequest{ User: "bob", Pass: string(tempPass), }) - c.Assert(err, IsNil) + require.NoError(t, err) var u2fSignReq u2f.AuthenticateChallenge - c.Assert(json.Unmarshal(re.Bytes(), &u2fSignReq), IsNil) + require.NoError(t, json.Unmarshal(re.Bytes(), &u2fSignReq)) - u2fSignResp, err := s.mockU2F.SignResponse(&u2fSignReq) - c.Assert(err, IsNil) + u2fSignResp, err := mockU2F.SignResponse(&u2fSignReq) + require.NoError(t, err) _, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), u2fSignResponseReq{ User: "bob", U2FSignResponse: *u2fSignResp, }) - c.Assert(err, IsNil) + require.NoError(t, err) // bad login: corrupted sign responses, should fail - re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.U2fSignRequestReq{ + re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.MFAChallengeRequest{ User: "bob", Pass: string(tempPass), }) - c.Assert(err, IsNil) - c.Assert(json.Unmarshal(re.Bytes(), &u2fSignReq), IsNil) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(re.Bytes(), &u2fSignReq)) - u2fSignResp, err = s.mockU2F.SignResponse(&u2fSignReq) - c.Assert(err, IsNil) + u2fSignResp, err = mockU2F.SignResponse(&u2fSignReq) + require.NoError(t, err) // corrupted KeyHandle u2fSignRespCopy := u2fSignResp @@ -1458,7 +1481,7 @@ func (s *WebSuite) TestU2FLogin(c *C) { User: "bob", U2FSignResponse: *u2fSignRespCopy, }) - c.Assert(err, NotNil) + require.Error(t, err) // corrupted SignatureData u2fSignRespCopy = u2fSignResp @@ -1468,7 +1491,7 @@ func (s *WebSuite) TestU2FLogin(c *C) { User: "bob", U2FSignResponse: *u2fSignRespCopy, }) - c.Assert(err, NotNil) + require.Error(t, err) // corrupted ClientData u2fSignRespCopy = u2fSignResp @@ -1478,25 +1501,25 @@ func (s *WebSuite) TestU2FLogin(c *C) { User: "bob", U2FSignResponse: *u2fSignRespCopy, }) - c.Assert(err, NotNil) + require.Error(t, err) // bad login: counter not increasing, should fail - s.mockU2F.SetCounter(0) - re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.U2fSignRequestReq{ + mockU2F.SetCounter(0) + re, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "signrequest"), client.MFAChallengeRequest{ User: "bob", Pass: string(tempPass), }) - c.Assert(err, IsNil) - c.Assert(json.Unmarshal(re.Bytes(), &u2fSignReq), IsNil) + require.NoError(t, err) + require.NoError(t, json.Unmarshal(re.Bytes(), &u2fSignReq)) - u2fSignResp, err = s.mockU2F.SignResponse(&u2fSignReq) - c.Assert(err, IsNil) + u2fSignResp, err = mockU2F.SignResponse(&u2fSignReq) + require.NoError(t, err) _, err = clt.PostJSON(context.Background(), clt.Endpoint("webapi", "u2f", "sessions"), u2fSignResponseReq{ User: "bob", U2FSignResponse: *u2fSignResp, }) - c.Assert(err, NotNil) + require.Error(t, err) } // TestPing ensures that a response is returned by /webapi/ping @@ -2445,7 +2468,7 @@ func (r *proxy) authPack(t *testing.T, user string) *authPack { ap, err := services.NewAuthPreference(services.AuthPreferenceSpecV2{ Type: teleport.Local, - SecondFactor: teleport.OTP, + SecondFactor: constants.SecondFactorOTP, }) require.NoError(t, err) diff --git a/lib/web/password.go b/lib/web/password.go index e696e380140..0c14dc479e4 100644 --- a/lib/web/password.go +++ b/lib/web/password.go @@ -70,7 +70,7 @@ func (h *Handler) changePassword(w http.ResponseWriter, r *http.Request, p httpr // u2fChangePasswordRequest is called to get U2F challedge for changing a user password func (h *Handler) u2fChangePasswordRequest(w http.ResponseWriter, r *http.Request, _ httprouter.Params, ctx *SessionContext) (interface{}, error) { - var req *client.U2fSignRequestReq + var req *client.MFAChallengeRequest if err := httplib.ReadJSON(r, &req); err != nil { return nil, trace.Wrap(err) } @@ -80,7 +80,7 @@ func (h *Handler) u2fChangePasswordRequest(w http.ResponseWriter, r *http.Reques return nil, trace.Wrap(err) } - u2fReq, err := clt.GetU2FSignRequest(ctx.GetUser(), []byte(req.Pass)) + u2fReq, err := clt.GetMFAAuthenticateChallenge(ctx.GetUser(), []byte(req.Pass)) if err != nil && trace.IsAccessDenied(err) { // logout in case of access denied logoutErr := h.logout(w, ctx) diff --git a/lib/web/sessions.go b/lib/web/sessions.go index 032d5915301..d3590c773be 100644 --- a/lib/web/sessions.go +++ b/lib/web/sessions.go @@ -467,6 +467,7 @@ func (s *sessionCache) clearExpiredSessions(ctx context.Context) { func (s *sessionCache) AuthWithOTP(user, pass, otpToken string) (services.WebSession, error) { return s.proxyClient.AuthenticateWebUser(auth.AuthenticateUserRequest{ Username: user, + Pass: &auth.PassCreds{Password: []byte(pass)}, OTP: &auth.OTPCreds{ Password: []byte(pass), Token: otpToken, @@ -485,8 +486,8 @@ func (s *sessionCache) AuthWithoutOTP(user, pass string) (services.WebSession, e }) } -func (s *sessionCache) GetU2FSignRequest(user, pass string) (*auth.U2FAuthenticateChallenge, error) { - return s.proxyClient.GetU2FSignRequest(user, []byte(pass)) +func (s *sessionCache) GetMFAAuthenticateChallenge(user, pass string) (*auth.MFAAuthenticateChallenge, error) { + return s.proxyClient.GetMFAAuthenticateChallenge(user, []byte(pass)) } func (s *sessionCache) AuthWithU2FSignResponse(user string, response *u2f.AuthenticateChallengeResponse) (services.WebSession, error) { @@ -534,19 +535,31 @@ func (s *sessionCache) GetCertificateWithOTP(c client.CreateSSHCertReq) (*auth.S }) } -func (s *sessionCache) GetCertificateWithU2F(c client.CreateSSHCertWithU2FReq) (*auth.SSHLoginResponse, error) { +func (s *sessionCache) GetCertificateWithMFA(c client.CreateSSHCertWithMFAReq) (*auth.SSHLoginResponse, error) { + authReq := auth.AuthenticateUserRequest{ + Username: c.User, + } + if c.Password != "" { + authReq.Pass = &auth.PassCreds{Password: []byte(c.Password)} + } + if c.U2FSignResponse != nil { + authReq.U2F = &auth.U2FSignResponseCreds{ + SignResponse: *c.U2FSignResponse, + } + } + if c.TOTPCode != "" { + authReq.OTP = &auth.OTPCreds{ + Password: []byte(c.Password), + Token: c.TOTPCode, + } + } return s.proxyClient.AuthenticateSSHUser(auth.AuthenticateSSHRequest{ - AuthenticateUserRequest: auth.AuthenticateUserRequest{ - Username: c.User, - U2F: &auth.U2FSignResponseCreds{ - SignResponse: c.U2FSignResponse, - }, - }, - PublicKey: c.PubKey, - CompatibilityMode: c.Compatibility, - TTL: c.TTL, - RouteToCluster: c.RouteToCluster, - KubernetesCluster: c.KubernetesCluster, + AuthenticateUserRequest: authReq, + PublicKey: c.PubKey, + CompatibilityMode: c.Compatibility, + TTL: c.TTL, + RouteToCluster: c.RouteToCluster, + KubernetesCluster: c.KubernetesCluster, }) } diff --git a/lib/web/ui/webconfig.go b/lib/web/ui/webconfig.go index 81ea6fc851b..7edad93ff60 100644 --- a/lib/web/ui/webconfig.go +++ b/lib/web/ui/webconfig.go @@ -16,6 +16,8 @@ limitations under the License. package ui +import "github.com/gravitational/teleport/api/constants" + const ( // WebConfigAuthProviderOIDCType is OIDC provider type WebConfigAuthProviderOIDCType = "oidc" @@ -58,7 +60,7 @@ type WebConfigAuthProvider struct { // WebConfigAuthSettings describes auth configuration type WebConfigAuthSettings struct { // SecondFactor is the type of second factor to use in authentication. - SecondFactor string `json:"second_factor,omitempty"` + SecondFactor constants.SecondFactorType `json:"second_factor,omitempty"` // Providers contains a list of configured auth providers Providers []WebConfigAuthProvider `json:"providers,omitempty"` // LocalAuthEnabled is a flag that enables local authentication diff --git a/tool/tctl/common/collection.go b/tool/tctl/common/collection.go index d8de4bcb006..d1c4f5887fe 100644 --- a/tool/tctl/common/collection.go +++ b/tool/tctl/common/collection.go @@ -493,7 +493,7 @@ func (c *authPrefCollection) resources() (r []services.Resource) { func (c *authPrefCollection) writeText(w io.Writer) error { t := asciitable.MakeTable([]string{"Type", "Second Factor"}) for _, authPref := range c.authPrefs { - t.AddRow([]string{authPref.GetType(), authPref.GetSecondFactor()}) + t.AddRow([]string{authPref.GetType(), string(authPref.GetSecondFactor())}) } _, err := t.AsBuffer().WriteTo(w) return trace.Wrap(err) diff --git a/tool/tsh/mfa.go b/tool/tsh/mfa.go index 29897d927eb..3867f81d03e 100644 --- a/tool/tsh/mfa.go +++ b/tool/tsh/mfa.go @@ -220,7 +220,7 @@ func (c *mfaAddCommand) addDeviceRPC(cf *CLIConf, devName string, devType proto. if authChallenge == nil { return trace.BadParameter("server bug: server sent %T when client expected AddMFADeviceResponse_ExistingMFAChallenge", resp.Response) } - authResp, err := promptMFAChallenge(cf.Context, tc.Config.WebProxyAddr, authChallenge) + authResp, err := client.PromptMFAChallenge(cf.Context, tc.Config.WebProxyAddr, authChallenge, "*registered* ") if err != nil { return trace.Wrap(err) } @@ -266,110 +266,6 @@ func (c *mfaAddCommand) addDeviceRPC(cf *CLIConf, devName string, devType proto. return dev, nil } -func promptMFAChallenge(ctx context.Context, proxyAddr string, c *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { - switch { - // No challenge. - case c.TOTP == nil && len(c.U2F) == 0: - return &proto.MFAAuthenticateResponse{}, nil - // TOTP only. - case c.TOTP != nil && len(c.U2F) == 0: - totpCode, err := prompt.Input(os.Stdout, os.Stdin, "Enter an OTP code from a *registered* device") - if err != nil { - return nil, trace.Wrap(err) - } - return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_TOTP{ - TOTP: &proto.TOTPResponse{Code: totpCode}, - }}, nil - // U2F only. - case c.TOTP == nil && len(c.U2F) > 0: - fmt.Println("Tap any *registered* security key") - - return promptU2FChallenges(ctx, proxyAddr, c.U2F) - // Both TOTP and U2F. - case c.TOTP != nil && len(c.U2F) > 0: - ctx, cancel := context.WithCancel(ctx) - defer cancel() - - type response struct { - kind string - resp *proto.MFAAuthenticateResponse - err error - } - resCh := make(chan response) - - go func() { - resp, err := promptU2FChallenges(ctx, proxyAddr, c.U2F) - select { - case resCh <- response{kind: "U2F", resp: resp, err: err}: - case <-ctx.Done(): - } - }() - - go func() { - totpCode, err := prompt.Input(os.Stdout, os.Stdin, "Tap any *registered* security key or enter an OTP code from a *registered* device") - res := response{kind: "TOTP", err: err} - if err == nil { - res.resp = &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_TOTP{ - TOTP: &proto.TOTPResponse{Code: totpCode}, - }} - } - - select { - case resCh <- res: - case <-ctx.Done(): - } - }() - - for i := 0; i < 2; i++ { - select { - case res := <-resCh: - if res.err != nil { - log.WithError(res.err).Debugf("%s authentication failed", res.kind) - continue - } - - // Print a newline after the TOTP prompt, so that any future - // output doesn't print on the prompt line. - fmt.Println() - - return res.resp, nil - case <-ctx.Done(): - return nil, trace.Wrap(ctx.Err()) - } - } - return nil, trace.Errorf("failed to authenticate using all U2F and TOTP devices, rerun the command with '-d' to see error details for each device") - default: - return nil, trace.BadParameter("bug: non-exhaustive switch in promptMFAChallenge") - } -} - -func promptU2FChallenges(ctx context.Context, proxyAddr string, challenges []*proto.U2FChallenge) (*proto.MFAAuthenticateResponse, error) { - facet := proxyAddr - if !strings.HasPrefix(proxyAddr, "https://") { - facet = "https://" + facet - } - u2fChallenges := make([]u2f.AuthenticateChallenge, 0, len(challenges)) - for _, chal := range challenges { - u2fChallenges = append(u2fChallenges, u2f.AuthenticateChallenge{ - Challenge: chal.Challenge, - KeyHandle: chal.KeyHandle, - AppID: chal.AppID, - }) - } - - resp, err := u2f.AuthenticateSignChallenge(ctx, facet, u2fChallenges...) - if err != nil { - return nil, trace.Wrap(err) - } - return &proto.MFAAuthenticateResponse{Response: &proto.MFAAuthenticateResponse_U2F{ - U2F: &proto.U2FResponse{ - KeyHandle: resp.KeyHandle, - ClientData: resp.ClientData, - Signature: resp.SignatureData, - }, - }}, nil -} - func promptRegisterChallenge(ctx context.Context, proxyAddr string, c *proto.MFARegisterChallenge) (*proto.MFARegisterResponse, error) { switch c.Request.(type) { case *proto.MFARegisterChallenge_TOTP: @@ -475,7 +371,7 @@ func (c *mfaRemoveCommand) run(cf *CLIConf) error { if authChallenge == nil { return trace.BadParameter("server bug: server sent %T when client expected DeleteMFADeviceResponse_MFAChallenge", resp.Response) } - authResp, err := promptMFAChallenge(cf.Context, tc.Config.WebProxyAddr, authChallenge) + authResp, err := client.PromptMFAChallenge(cf.Context, tc.Config.WebProxyAddr, authChallenge, "") if err != nil { return trace.Wrap(err) }