mfa: add new second_factor options "on" and "optional" (#5508)

* mfa: add new second_factor options "on" and "optional"

"on" means that 2FA is required for all users, either TOTP or U2F.

"optional" means that 2FA is supported for all users, but not required.
Only users with MFA devices registered will be prompted for 2FA on
login.

The login with both supported methods is using the same API as the U2F
login. It just now supports TOTP in addition. The API endpoints are
still named after "u2f", I'll rename those in a future PR (in a
backwards-compatible way).

* Apply suggestions from code review

Co-authored-by: Gus Luxton <gus@gravitational.com>
Co-authored-by: a-palchikov <deemok@gmail.com>

* Address reivew feedback

Co-authored-by: Gus Luxton <gus@gravitational.com>
Co-authored-by: a-palchikov <deemok@gmail.com>
This commit is contained in:
Andrew Lytvynov 2021-02-17 00:24:23 +00:00 committed by GitHub
parent 2db04fe1d0
commit 5739b63e51
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
27 changed files with 569 additions and 372 deletions

View file

@ -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")
)

View file

@ -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 {

View file

@ -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

View file

@ -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)
}

View file

@ -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,

View file

@ -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}))
})
}

View file

@ -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

View file

@ -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)

View file

@ -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"},

View file

@ -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)

View file

@ -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")

View file

@ -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")
}

View file

@ -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
}

View file

@ -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,

View file

@ -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)

View file

@ -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,

140
lib/client/mfa.go Normal file
View file

@ -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
}

View file

@ -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.

View file

@ -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"`

View file

@ -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) {

View file

@ -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)
}

View file

@ -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)

View file

@ -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)

View file

@ -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,
})
}

View file

@ -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

View file

@ -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)

View file

@ -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)
}