mirror of
https://github.com/gravitational/teleport
synced 2024-10-19 16:53:57 +00:00
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:
parent
2db04fe1d0
commit
5739b63e51
|
@ -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")
|
||||
)
|
||||
|
|
|
@ -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 {
|
||||
|
|
19
constants.go
19
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
|
||||
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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}))
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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"},
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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")
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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
140
lib/client/mfa.go
Normal 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
|
||||
}
|
|
@ -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.
|
||||
|
|
|
@ -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"`
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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)
|
||||
}
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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)
|
||||
|
|
|
@ -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,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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)
|
||||
|
|
108
tool/tsh/mfa.go
108
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)
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue