Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion api/client/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -477,7 +477,7 @@ func (c *Client) dialGRPC(ctx context.Context, addr string) error {
otelUnaryClientInterceptor(),
metadata.UnaryClientInterceptor,
interceptors.GRPCClientUnaryErrorInterceptor,
interceptors.WithMFAUnaryInterceptor(c.performMFACeremony),
interceptors.WithMFAUnaryInterceptor(c.performAdminActionMFACeremony),
breaker.UnaryClientInterceptor(cb),
),
grpc.WithChainStreamInterceptor(
Expand Down
8 changes: 5 additions & 3 deletions api/client/mfa.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,17 +23,19 @@ import (

"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/mfa"
webauthnpb "github.com/gravitational/teleport/api/types/webauthn"
)

// performMFACeremony retrieves an MFA challenge from the server, prompts the
// user to answer the challenge, and returns the resulting MFA response.
func (c *Client) performMFACeremony(ctx context.Context, promptOpts ...mfa.PromptOpt) (*proto.MFAAuthenticateResponse, error) {
// performAdminActionMFACeremony retrieves an MFA challenge from the server,
// prompts the user to answer the challenge, and returns the resulting MFA response.
func (c *Client) performAdminActionMFACeremony(ctx context.Context, promptOpts ...mfa.PromptOpt) (*proto.MFAAuthenticateResponse, error) {
if c.c.MFAPromptConstructor == nil {
return nil, trace.BadParameter("missing PromptAdminRequestMFA field, client cannot perform MFA ceremony")
}

chal, err := c.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{},
Scope: webauthnpb.ChallengeScope_CHALLENGE_SCOPE_ADMIN_ACTION,
})
if err != nil {
return nil, trace.Wrap(err)
Expand Down
2 changes: 1 addition & 1 deletion api/client/mfa_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ func TestPerformMFACeremony(t *testing.T) {
clt, err := New(ctx, cfg)
require.NoError(t, err)

resp, err := clt.performMFACeremony(ctx)
resp, err := clt.performAdminActionMFACeremony(ctx)
require.NoError(t, err)
require.Equal(t, mfaTestResp.Response, resp.Response)
}
Expand Down
1,767 changes: 903 additions & 864 deletions api/client/proto/authservice.pb.go

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions api/proto/teleport/legacy/client/proto/authservice.proto
Original file line number Diff line number Diff line change
Expand Up @@ -1839,6 +1839,9 @@ message CreateAuthenticateChallengeRequest {
// If you are issuing challenges from the root cluster, but accessing a leaf,
// call [AuthService.IsMFARequired] in the leaf instead of setting this field.
IsMFARequiredRequest MFARequiredCheck = 5 [(gogoproto.jsontag) = "mfa_required_check,omitempty"];
// Scope is a authorization scope for this MFA challenge.
// Required. Only applies to webauthn challenges.
webauthn.ChallengeScope Scope = 6 [(gogoproto.jsontag) = "scope,omitempty"];
}

// CreatePrivilegeTokenRequest defines a request to obtain a privilege token.
Expand Down
33 changes: 33 additions & 0 deletions api/proto/teleport/legacy/types/webauthn/webauthn.proto
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,39 @@ message SessionData {
// "required".
// An empty value is treated equivalently to "discouraged".
string user_verification = 5 [(gogoproto.jsontag) = "userVerification,omitempty"];
// Scope authorized by this webauthn session.
ChallengeScope scope = 6 [(gogoproto.jsontag) = "scope,omitempty"];
}

// ChallengeScope is a scope authorized by a webauthn challenge's resolution.
enum ChallengeScope {
// Invalid, scope should always be specified.
CHALLENGE_SCOPE_UNSPECIFIED = 0;
// Standard webauthn login.
CHALLENGE_SCOPE_LOGIN = 1;
// Passwordless webauthn login.
CHALLENGE_SCOPE_PASSWORDLESS_LOGIN = 2;
// MFA device management.
CHALLENGE_SCOPE_MANAGE_DEVICES = 3;
// Account recovery.
CHALLENGE_SCOPE_RECOVERY = 4;
// Used for per-session MFA and moderated session presence checks.
CHALLENGE_SCOPE_SESSION = 5;
// Headless login approval.
CHALLENGE_SCOPE_HEADLESS = 6;
// Used for various administrative actions, such as adding, updating, or
// deleting administrative resources (users, roles, etc.).
//
// Note: this scope should not be used for new MFA capabilities that have
// more precise scope. Instead, new scopes should be added. This scope may
// also be split into multiple smaller scopes in the future.
CHALLENGE_SCOPE_ADMIN_ACTION = 7;
// Webauthn credentials resolved from a challenge with this scope can be
// reused for a short span of time before the challenge expires.
//
// This scope for a select few admin actions and will be rejected by other
// admin actions. See the server implementation for details.
CHALLENGE_SCOPE_ADMIN_ACTION_WITH_REUSE = 8;
}

// User represents a WebAuthn user.
Expand Down
260 changes: 187 additions & 73 deletions api/types/webauthn/webauthn.pb.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion e
Submodule e updated from e5a5a0 to 812779
5 changes: 3 additions & 2 deletions lib/auth/accountrecovery.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
webauthnpb "github.com/gravitational/teleport/api/types/webauthn"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/lib/authz"
"github.com/gravitational/teleport/lib/defaults"
Expand Down Expand Up @@ -264,8 +265,8 @@ func (a *Server) VerifyAccountRecovery(ctx context.Context, req *proto.VerifyAcc
}

if err := a.verifyAuthnWithRecoveryLock(ctx, startToken, func() error {
_, _, err := a.ValidateMFAAuthResponse(
ctx, req.GetMFAAuthenticateResponse(), startToken.GetUser(), false /* passwordless */)
_, _, err := a.ValidateMFAAuthResponseWithScope(
ctx, req.GetMFAAuthenticateResponse(), startToken.GetUser(), webauthnpb.ChallengeScope_CHALLENGE_SCOPE_RECOVERY)
return err
}); err != nil {
return nil, trace.Wrap(err)
Expand Down
9 changes: 6 additions & 3 deletions lib/auth/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/types"
"github.com/gravitational/teleport/api/types/discoveryconfig"
webauthnpb "github.com/gravitational/teleport/api/types/webauthn"
"github.com/gravitational/teleport/lib/events"
"github.com/gravitational/teleport/lib/services"
)
Expand Down Expand Up @@ -90,9 +91,11 @@ type accessPoint interface {
// ConnectionDiagnosticTraceAppender adds a method to append traces into ConnectionDiagnostics.
services.ConnectionDiagnosticTraceAppender

// ValidateMFAAuthResponse validates an MFA or passwordless challenge.
// Returns the device used to solve the challenge (if applicable) and the username.
ValidateMFAAuthResponse(ctx context.Context, resp *proto.MFAAuthenticateResponse, user string, passwordless bool) (*types.MFADevice, string, error)
// ValidateMFAAuthResponseWithScope validates an MFA challenge response. If the challenge
// response if of type webauthn, this also validates that the challenge response satisfies
// the given scope. Returns the device used to solve the challenge (if applicable) and the
// username.
ValidateMFAAuthResponseWithScope(ctx context.Context, resp *proto.MFAAuthenticateResponse, user string, requiredScope webauthnpb.ChallengeScope) (*types.MFADevice, string, error)
}

// ReadNodeAccessPoint is a read only API interface implemented by a certificate authority (CA) to be
Expand Down
3 changes: 2 additions & 1 deletion lib/auth/assist/assistv1/test/service_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
assistpb "github.com/gravitational/teleport/api/gen/proto/go/assist/v1"
"github.com/gravitational/teleport/api/types"
webauthnpb "github.com/gravitational/teleport/api/types/webauthn"
"github.com/gravitational/teleport/api/utils/retryutils"
"github.com/gravitational/teleport/lib/ai"
"github.com/gravitational/teleport/lib/assist"
Expand Down Expand Up @@ -321,7 +322,7 @@ type testClient struct {
services.UserGetter
}

func (c *testClient) ValidateMFAAuthResponse(ctx context.Context, resp *proto.MFAAuthenticateResponse, user string, passwordless bool) (*types.MFADevice, string, error) {
func (c *testClient) ValidateMFAAuthResponseWithScope(ctx context.Context, resp *proto.MFAAuthenticateResponse, user string, scope webauthnpb.ChallengeScope) (*types.MFADevice, string, error) {
return nil, "", nil
}

Expand Down
39 changes: 23 additions & 16 deletions lib/auth/auth.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,7 @@ import (
"github.com/gravitational/teleport/api/metadata"
"github.com/gravitational/teleport/api/types"
apievents "github.com/gravitational/teleport/api/types/events"
webauthnpb "github.com/gravitational/teleport/api/types/webauthn"
"github.com/gravitational/teleport/api/types/wrappers"
apiutils "github.com/gravitational/teleport/api/utils"
"github.com/gravitational/teleport/api/utils/keys"
Expand Down Expand Up @@ -2877,11 +2878,12 @@ func (a *Server) PreAuthenticatedSignIn(ctx context.Context, user string, identi
// CreateAuthenticateChallenge implements AuthService.CreateAuthenticateChallenge.
func (a *Server) CreateAuthenticateChallenge(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) {
var username string
var passwordless bool

scope := req.GetScope()
switch req.GetRequest().(type) {
case *proto.CreateAuthenticateChallengeRequest_UserCredentials:
username = req.GetUserCredentials().GetUsername()
scope = webauthnpb.ChallengeScope_CHALLENGE_SCOPE_LOGIN

if err := a.WithUserLock(ctx, username, func() error {
return a.checkPasswordWOToken(username, req.GetUserCredentials().GetPassword())
Expand All @@ -2901,19 +2903,21 @@ func (a *Server) CreateAuthenticateChallenge(ctx context.Context, req *proto.Cre
}

username = token.GetUser()
scope = webauthnpb.ChallengeScope_CHALLENGE_SCOPE_RECOVERY

case *proto.CreateAuthenticateChallengeRequest_Passwordless:
passwordless = true // Allows empty username.
scope = webauthnpb.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN

default: // unset or CreateAuthenticateChallengeRequest_ContextUser.
// TODO(Joerger): in v16.0.0, require scope to be specified in the request.
var err error
username, err = authz.GetClientUsername(ctx)
if err != nil {
return nil, trace.Wrap(err)
}
}

challenges, err := a.mfaAuthChallenge(ctx, username, passwordless)
challenges, err := a.mfaAuthChallenge(ctx, username, scope)
if err != nil {
log.Error(trace.DebugReport(err))
return nil, trace.AccessDenied("unable to create MFA challenges")
Expand Down Expand Up @@ -3155,8 +3159,8 @@ func (a *Server) DeleteMFADeviceSync(ctx context.Context, req *proto.DeleteMFADe
return trace.Wrap(err)
}

if _, _, err := a.ValidateMFAAuthResponse(
ctx, req.ExistingMFAResponse, user, false, /* passwordless */
if _, _, err := a.ValidateMFAAuthResponseWithScope(
ctx, req.ExistingMFAResponse, user, webauthnpb.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES,
); err != nil {
return trace.Wrap(err)
}
Expand Down Expand Up @@ -5637,7 +5641,7 @@ func (a *Server) isMFARequired(ctx context.Context, checker services.AccessCheck

// mfaAuthChallenge constructs an MFAAuthenticateChallenge for all MFA devices
// registered by the user.
func (a *Server) mfaAuthChallenge(ctx context.Context, user string, passwordless bool) (*proto.MFAAuthenticateChallenge, error) {
func (a *Server) mfaAuthChallenge(ctx context.Context, user string, scope webauthnpb.ChallengeScope) (*proto.MFAAuthenticateChallenge, error) {
// Check what kind of MFA is enabled.
apref, err := a.GetAuthPreference(ctx)
if err != nil {
Expand Down Expand Up @@ -5666,7 +5670,7 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user string, passwordless
}

// Handle passwordless separately, it works differently from MFA.
if passwordless {
if scope == webauthnpb.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN {
if !enableWebauthn {
return nil, trace.BadParameter("passwordless requires WebAuthn")
}
Expand Down Expand Up @@ -5707,7 +5711,7 @@ func (a *Server) mfaAuthChallenge(ctx context.Context, user string, passwordless
Webauthn: webConfig,
Identity: wanlib.WithDevices(a.Services, groupedDevs.Webauthn),
}
assertion, err := webLogin.Begin(ctx, user)
assertion, err := webLogin.Begin(ctx, user, scope)
if err != nil {
return nil, trace.Wrap(err)
}
Expand Down Expand Up @@ -5781,8 +5785,8 @@ func (a *Server) validateMFAAuthResponseForRegister(
}

if err := a.WithUserLock(ctx, username, func() error {
_, _, err := a.ValidateMFAAuthResponse(
ctx, resp, username, false /* passwordless */)
_, _, err := a.ValidateMFAAuthResponseWithScope(
ctx, resp, username, webauthnpb.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES)
return err
}); err != nil {
return false, trace.Wrap(err)
Expand All @@ -5791,12 +5795,15 @@ func (a *Server) validateMFAAuthResponseForRegister(
return true, nil
}

// ValidateMFAAuthResponse validates an MFA or passwordless challenge.
// Returns the device used to solve the challenge (if applicable) and the
// ValidateMFAAuthResponseWithScope validates an MFA challenge response. If the challenge
// response if of type webauthn, this also validates that the challenge response satisfies
// the given scope. Returns the device used to solve the challenge (if applicable) and the
// username.
func (a *Server) ValidateMFAAuthResponse(ctx context.Context, resp *proto.MFAAuthenticateResponse, user string, passwordless bool) (*types.MFADevice, string, error) {
func (a *Server) ValidateMFAAuthResponseWithScope(ctx context.Context, resp *proto.MFAAuthenticateResponse, user string, requiredScope webauthnpb.ChallengeScope) (*types.MFADevice, string, error) {
isPasswordless := requiredScope == webauthnpb.ChallengeScope_CHALLENGE_SCOPE_PASSWORDLESS_LOGIN

// Sanity check user/passwordless.
if user == "" && !passwordless {
if user == "" && !isPasswordless {
return nil, "", trace.BadParameter("user required")
}

Expand All @@ -5821,7 +5828,7 @@ func (a *Server) ValidateMFAAuthResponse(ctx context.Context, resp *proto.MFAAut

assertionResp := wantypes.CredentialAssertionResponseFromProto(res.Webauthn)
var dev *types.MFADevice
if passwordless {
if isPasswordless {
webLogin := &wanlib.PasswordlessFlow{
Webauthn: webConfig,
Identity: a.Services,
Expand All @@ -5833,7 +5840,7 @@ func (a *Server) ValidateMFAAuthResponse(ctx context.Context, resp *proto.MFAAut
Webauthn: webConfig,
Identity: a.Services,
}
dev, err = webLogin.Finish(ctx, user, wantypes.CredentialAssertionResponseFromProto(res.Webauthn))
dev, err = webLogin.Finish(ctx, user, wantypes.CredentialAssertionResponseFromProto(res.Webauthn), requiredScope)
}
if err != nil {
return nil, "", trace.AccessDenied("MFA response validation failed: %v", err)
Expand Down
11 changes: 9 additions & 2 deletions lib/auth/auth_login_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ import (
"github.com/gravitational/teleport/api/client/proto"
"github.com/gravitational/teleport/api/constants"
"github.com/gravitational/teleport/api/types"
webauthnpb "github.com/gravitational/teleport/api/types/webauthn"
"github.com/gravitational/teleport/lib/auth/mocku2f"
wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes"
"github.com/gravitational/teleport/lib/defaults"
Expand Down Expand Up @@ -185,14 +186,16 @@ func TestCreateAuthenticateChallenge_WithAuth(t *testing.T) {
clt, err := srv.NewClient(TestUser(u.username))
require.NoError(t, err)

res, err := clt.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{})
res, err := clt.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{
Scope: webauthnpb.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
})
require.NoError(t, err)

// MFA authentication works.
// TODO(codingllama): Use a public endpoint to verify?
mfaResp, err := u.webDev.SolveAuthn(res)
require.NoError(t, err)
_, _, err = srv.Auth().ValidateMFAAuthResponse(ctx, mfaResp, u.username, false /* passwordless */)
_, _, err = srv.Auth().ValidateMFAAuthResponseWithScope(ctx, mfaResp, u.username, webauthnpb.ChallengeScope_CHALLENGE_SCOPE_LOGIN)
require.NoError(t, err)
}

Expand Down Expand Up @@ -414,6 +417,7 @@ func TestCreateAuthenticateChallenge_mfaVerification(t *testing.T) {
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
Scope: webauthnpb.ChallengeScope_CHALLENGE_SCOPE_SESSION,
MFARequiredCheck: &proto.IsMFARequiredRequest{
Target: &proto.IsMFARequiredRequest_Node{
Node: &proto.NodeLogin{
Expand Down Expand Up @@ -537,6 +541,7 @@ func TestCreateRegisterChallenge(t *testing.T) {
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
Scope: webauthnpb.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES,
})
require.NoError(t, err, "CreateAuthenticateChallenge")
authnSolved, err := u.webDev.SolveAuthn(authnChal)
Expand Down Expand Up @@ -776,6 +781,7 @@ func TestServer_Authenticate_passwordless(t *testing.T) {
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{}, // already authenticated
},
Scope: webauthnpb.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES,
})
require.NoError(t, err)
mfaResp, err := webDev.SolveAuthn(mfaChallenge)
Expand Down Expand Up @@ -975,6 +981,7 @@ func TestServer_Authenticate_nonPasswordlessRequiresUsername(t *testing.T) {
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
Scope: webauthnpb.ChallengeScope_CHALLENGE_SCOPE_LOGIN,
})
require.NoError(t, err)

Expand Down
4 changes: 4 additions & 0 deletions lib/auth/auth_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import (
"github.com/gravitational/teleport/api/types/installers"
"github.com/gravitational/teleport/api/types/trait"
"github.com/gravitational/teleport/api/types/userloginstate"
webauthnpb "github.com/gravitational/teleport/api/types/webauthn"
"github.com/gravitational/teleport/api/types/wrappers"
"github.com/gravitational/teleport/api/utils/keys"
"github.com/gravitational/teleport/api/utils/sshutils"
Expand Down Expand Up @@ -2202,6 +2203,7 @@ func TestDeleteMFADeviceSync(t *testing.T) {
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
Scope: webauthnpb.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES,
})
require.NoError(t, err, "CreateAuthenticateChallenge")

Expand Down Expand Up @@ -2445,6 +2447,7 @@ func TestDeleteMFADeviceSync_lastDevice(t *testing.T) {
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
Scope: webauthnpb.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES,
})
if err != nil {
return err
Expand Down Expand Up @@ -2576,6 +2579,7 @@ func TestAddMFADeviceSync(t *testing.T) {
Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{
ContextUser: &proto.ContextUser{},
},
Scope: webauthnpb.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES,
})
require.NoError(t, err, "CreateAuthenticateChallenge")

Expand Down
Loading