diff --git a/api/client/mfa.go b/api/client/mfa.go index 03cfc13b88a5..beba5b20c79d 100644 --- a/api/client/mfa.go +++ b/api/client/mfa.go @@ -19,8 +19,6 @@ package client import ( "context" - "github.com/gravitational/trace" - "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/mfa" ) @@ -29,19 +27,9 @@ import ( // and prompts the user to answer the challenge with the given promptOpts, and ultimately returning // an MFA challenge response for the user. func (c *Client) PerformMFACeremony(ctx context.Context, challengeRequest *proto.CreateAuthenticateChallengeRequest, promptOpts ...mfa.PromptOpt) (*proto.MFAAuthenticateResponse, error) { - // Don't attempt the MFA ceremony if we can't prompt for a response. - if c.c.MFAPromptConstructor == nil { - return nil, trace.Wrap(&mfa.ErrMFANotSupported, "missing MFAPromptConstructor field, client cannot perform MFA ceremony") - } - - return mfa.PerformMFACeremony(ctx, c, challengeRequest, promptOpts...) -} - -// PromptMFA prompts the user for MFA. Implements [mfa.MFACeremonyClient]. -func (c *Client) PromptMFA(ctx context.Context, chal *proto.MFAAuthenticateChallenge, promptOpts ...mfa.PromptOpt) (*proto.MFAAuthenticateResponse, error) { - if c.c.MFAPromptConstructor == nil { - return nil, trace.Wrap(&mfa.ErrMFANotSupported, "missing MFAPromptConstructor field, client cannot prompt for MFA") + mfaCeremony := &mfa.Ceremony{ + CreateAuthenticateChallenge: c.CreateAuthenticateChallenge, + PromptConstructor: c.c.MFAPromptConstructor, } - - return c.c.MFAPromptConstructor(promptOpts...).Run(ctx, chal) + return mfaCeremony.Run(ctx, challengeRequest, promptOpts...) } diff --git a/api/mfa/ceremony.go b/api/mfa/ceremony.go index 09fd11c91027..f3c5f88e23d6 100644 --- a/api/mfa/ceremony.go +++ b/api/mfa/ceremony.go @@ -25,32 +25,37 @@ import ( mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1" ) -// MFACeremonyClient is a client that can perform an MFA ceremony, from retrieving -// the MFA challenge to prompting for an MFA response from the user. -type MFACeremonyClient interface { - // CreateAuthenticateChallenge creates and returns MFA challenges for a users registered MFA devices. - CreateAuthenticateChallenge(ctx context.Context, in *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) - // PromptMFA prompts the user for MFA. - PromptMFA(ctx context.Context, chal *proto.MFAAuthenticateChallenge, promptOpts ...PromptOpt) (*proto.MFAAuthenticateResponse, error) +// Ceremony is an MFA ceremony. +type Ceremony struct { + // CreateAuthenticateChallenge creates an authentication challenge. + CreateAuthenticateChallenge func(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) + // PromptConstructor creates a prompt to prompt the user to solve an authentication challenge. + PromptConstructor PromptConstructor + // SolveAuthenticateChallenge solves an authentication challenge. Used in non-interactive settings, + // such as the WebUI with layers abstracting user interaction, and tests. + SolveAuthenticateChallenge func(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) } -// PerformMFACeremony retrieves an MFA challenge from the server with the given challenge extensions -// and prompts the user to answer the challenge with the given promptOpts, and ultimately returning -// an MFA challenge response for the user. -func PerformMFACeremony(ctx context.Context, clt MFACeremonyClient, challengeRequest *proto.CreateAuthenticateChallengeRequest, promptOpts ...PromptOpt) (*proto.MFAAuthenticateResponse, error) { - if challengeRequest == nil { - return nil, trace.BadParameter("missing challenge request") - } - - if challengeRequest.ChallengeExtensions == nil { +// Run the MFA ceremony. +// +// req may be nil if ceremony.CreateAuthenticateChallenge does not require it, e.g. in +// the moderated session mfa ceremony which uses a custom stream rpc to create challenges. +func (c *Ceremony) Run(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest, promptOpts ...PromptOpt) (*proto.MFAAuthenticateResponse, error) { + switch { + case c.CreateAuthenticateChallenge == nil: + return nil, trace.BadParameter("mfa ceremony must have CreateAuthenticateChallenge set in order to begin") + case c.SolveAuthenticateChallenge != nil && c.PromptConstructor != nil: + return nil, trace.BadParameter("mfa ceremony should have SolveAuthenticateChallenge or PromptConstructor set, not both") + case req == nil: + // req may be nil in cases where the ceremony's CreateAuthenticateChallenge sources + // its own req or uses a different rpc, e.g. moderated sessions. + case req.ChallengeExtensions == nil: return nil, trace.BadParameter("missing challenge extensions") - } - - if challengeRequest.ChallengeExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_UNSPECIFIED { + case req.ChallengeExtensions.Scope == mfav1.ChallengeScope_CHALLENGE_SCOPE_UNSPECIFIED: return nil, trace.BadParameter("mfa challenge scope must be specified") } - chal, err := clt.CreateAuthenticateChallenge(ctx, challengeRequest) + chal, err := c.CreateAuthenticateChallenge(ctx, req) if err != nil { // CreateAuthenticateChallenge returns a bad parameter error when the client // user is not a Teleport user - for example, the AdminRole. Treat this as an MFA @@ -67,21 +72,31 @@ func PerformMFACeremony(ctx context.Context, clt MFACeremonyClient, challengeReq return nil, &ErrMFANotRequired } - return clt.PromptMFA(ctx, chal, promptOpts...) + if c.SolveAuthenticateChallenge == nil && c.PromptConstructor == nil { + return nil, trace.Wrap(&ErrMFANotSupported, "mfa ceremony must have SolveAuthenticateChallenge or PromptConstructor set in order to succeed") + } + + if c.SolveAuthenticateChallenge != nil { + resp, err := c.SolveAuthenticateChallenge(ctx, chal) + return resp, trace.Wrap(err) + } + + resp, err := c.PromptConstructor(promptOpts...).Run(ctx, chal) + return resp, trace.Wrap(err) } -type MFACeremony func(ctx context.Context, challengeRequest *proto.CreateAuthenticateChallengeRequest, promptOpts ...PromptOpt) (*proto.MFAAuthenticateResponse, error) +// CeremonyFn is a function that will carry out an MFA ceremony. +type CeremonyFn func(ctx context.Context, in *proto.CreateAuthenticateChallengeRequest, promptOpts ...PromptOpt) (*proto.MFAAuthenticateResponse, error) // PerformAdminActionMFACeremony retrieves an MFA challenge from the server for an admin // action, prompts the user to answer the challenge, and returns the resulting MFA response. -func PerformAdminActionMFACeremony(ctx context.Context, mfaCeremony MFACeremony, allowReuse bool) (*proto.MFAAuthenticateResponse, error) { +func PerformAdminActionMFACeremony(ctx context.Context, mfaCeremony CeremonyFn, allowReuse bool) (*proto.MFAAuthenticateResponse, error) { allowReuseExt := mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_NO if allowReuse { allowReuseExt = mfav1.ChallengeAllowReuse_CHALLENGE_ALLOW_REUSE_YES } challengeRequest := &proto.CreateAuthenticateChallengeRequest{ - Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{}, MFARequiredCheck: &proto.IsMFARequiredRequest{ Target: &proto.IsMFARequiredRequest_AdminAction{ AdminAction: &proto.AdminAction{}, @@ -93,5 +108,6 @@ func PerformAdminActionMFACeremony(ctx context.Context, mfaCeremony MFACeremony, }, } - return mfaCeremony(ctx, challengeRequest, WithPromptReasonAdminAction()) + resp, err := mfaCeremony(ctx, challengeRequest, WithPromptReasonAdminAction()) + return resp, trace.Wrap(err) } diff --git a/api/mfa/ceremony_test.go b/api/mfa/ceremony_test.go index 5e9df622534b..bb6a24b6fcdb 100644 --- a/api/mfa/ceremony_test.go +++ b/api/mfa/ceremony_test.go @@ -21,6 +21,7 @@ import ( "errors" "testing" + "github.com/gravitational/trace" "github.com/stretchr/testify/assert" "github.com/gravitational/teleport/api/client/proto" @@ -32,6 +33,9 @@ func TestPerformMFACeremony(t *testing.T) { t.Parallel() ctx := context.Background() + testMFAChallenge := &proto.MFAAuthenticateChallenge{ + TOTP: &proto.TOTPChallenge{}, + } testMFAResponse := &proto.MFAAuthenticateResponse{ Response: &proto.MFAAuthenticateResponse_TOTP{ TOTP: &proto.TOTPResponse{ @@ -42,13 +46,34 @@ func TestPerformMFACeremony(t *testing.T) { for _, tt := range []struct { name string - ceremonyClient *fakeMFACeremonyClient + ceremony *mfa.Ceremony assertCeremonyResponse func(*testing.T, *proto.MFAAuthenticateResponse, error, ...interface{}) }{ { - name: "OK ceremony success", - ceremonyClient: &fakeMFACeremonyClient{ - challengeResponse: testMFAResponse, + name: "OK ceremony success prompt", + ceremony: &mfa.Ceremony{ + CreateAuthenticateChallenge: func(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) { + return testMFAChallenge, nil + }, + PromptConstructor: func(po ...mfa.PromptOpt) mfa.Prompt { + return mfa.PromptFunc(func(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { + return testMFAResponse, nil + }) + }, + }, + assertCeremonyResponse: func(t *testing.T, mr *proto.MFAAuthenticateResponse, err error, i ...interface{}) { + assert.NoError(t, err) + assert.Equal(t, testMFAResponse, mr) + }, + }, { + name: "OK ceremony success solve", + ceremony: &mfa.Ceremony{ + CreateAuthenticateChallenge: func(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) { + return testMFAChallenge, nil + }, + SolveAuthenticateChallenge: func(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { + return testMFAResponse, nil + }, }, assertCeremonyResponse: func(t *testing.T, mr *proto.MFAAuthenticateResponse, err error, i ...interface{}) { assert.NoError(t, err) @@ -56,9 +81,15 @@ func TestPerformMFACeremony(t *testing.T) { }, }, { name: "OK ceremony not required", - ceremonyClient: &fakeMFACeremonyClient{ - challengeResponse: testMFAResponse, - mfaRequired: proto.MFARequired_MFA_REQUIRED_NO, + ceremony: &mfa.Ceremony{ + CreateAuthenticateChallenge: func(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) { + return &proto.MFAAuthenticateChallenge{ + MFARequired: proto.MFARequired_MFA_REQUIRED_NO, + }, nil + }, + SolveAuthenticateChallenge: func(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { + return nil, trace.BadParameter("expected mfa not required") + }, }, assertCeremonyResponse: func(t *testing.T, mr *proto.MFAAuthenticateResponse, err error, i ...interface{}) { assert.Error(t, err, mfa.ErrMFANotRequired) @@ -66,9 +97,13 @@ func TestPerformMFACeremony(t *testing.T) { }, }, { name: "NOK create challenge fail", - ceremonyClient: &fakeMFACeremonyClient{ - challengeResponse: testMFAResponse, - createAuthenticateChallengeErr: errors.New("create authenticate challenge failure"), + ceremony: &mfa.Ceremony{ + CreateAuthenticateChallenge: func(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) { + return nil, errors.New("create authenticate challenge failure") + }, + SolveAuthenticateChallenge: func(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { + return nil, trace.BadParameter("expected challenge failure") + }, }, assertCeremonyResponse: func(t *testing.T, mr *proto.MFAAuthenticateResponse, err error, i ...interface{}) { assert.ErrorContains(t, err, "create authenticate challenge failure") @@ -76,18 +111,38 @@ func TestPerformMFACeremony(t *testing.T) { }, }, { name: "NOK prompt mfa fail", - ceremonyClient: &fakeMFACeremonyClient{ - challengeResponse: testMFAResponse, - promptMFAErr: errors.New("prompt mfa failure"), + ceremony: &mfa.Ceremony{ + CreateAuthenticateChallenge: func(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) { + return testMFAChallenge, nil + }, + PromptConstructor: func(po ...mfa.PromptOpt) mfa.Prompt { + return mfa.PromptFunc(func(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { + return nil, errors.New("prompt mfa failure") + }) + }, }, assertCeremonyResponse: func(t *testing.T, mr *proto.MFAAuthenticateResponse, err error, i ...interface{}) { assert.ErrorContains(t, err, "prompt mfa failure") assert.Nil(t, mr) }, + }, { + name: "NOK solve mfa fail", + ceremony: &mfa.Ceremony{ + CreateAuthenticateChallenge: func(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) { + return testMFAChallenge, nil + }, + SolveAuthenticateChallenge: func(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { + return nil, errors.New("solve mfa failure") + }, + }, + assertCeremonyResponse: func(t *testing.T, mr *proto.MFAAuthenticateResponse, err error, i ...interface{}) { + assert.ErrorContains(t, err, "solve mfa failure") + assert.Nil(t, mr) + }, }, } { t.Run(tt.name, func(t *testing.T) { - resp, err := mfa.PerformMFACeremony(ctx, tt.ceremonyClient, &proto.CreateAuthenticateChallengeRequest{ + resp, err := tt.ceremony.Run(ctx, &proto.CreateAuthenticateChallengeRequest{ ChallengeExtensions: &mfav1.ChallengeExtensions{ Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_ADMIN_ACTION, }, @@ -97,34 +152,3 @@ func TestPerformMFACeremony(t *testing.T) { }) } } - -type fakeMFACeremonyClient struct { - createAuthenticateChallengeErr error - promptMFAErr error - mfaRequired proto.MFARequired - challengeResponse *proto.MFAAuthenticateResponse -} - -func (c *fakeMFACeremonyClient) CreateAuthenticateChallenge(ctx context.Context, in *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) { - if c.createAuthenticateChallengeErr != nil { - return nil, c.createAuthenticateChallengeErr - } - - chal := &proto.MFAAuthenticateChallenge{ - TOTP: &proto.TOTPChallenge{}, - } - - if in.MFARequiredCheck != nil { - chal.MFARequired = c.mfaRequired - } - - return chal, nil -} - -func (c *fakeMFACeremonyClient) PromptMFA(ctx context.Context, chal *proto.MFAAuthenticateChallenge, promptOpts ...mfa.PromptOpt) (*proto.MFAAuthenticateResponse, error) { - if c.promptMFAErr != nil { - return nil, c.promptMFAErr - } - - return c.challengeResponse, nil -} diff --git a/api/utils/grpc/interceptors/mfa.go b/api/utils/grpc/interceptors/mfa.go index 367851fef1cc..e8dae8e45e1f 100644 --- a/api/utils/grpc/interceptors/mfa.go +++ b/api/utils/grpc/interceptors/mfa.go @@ -31,7 +31,7 @@ import ( // to the rpc call when an MFA response is provided through the context. Additionally, // when the call returns an error that indicates that MFA is required, this interceptor // will prompt for MFA using the given mfaCeremony and retry. -func WithMFAUnaryInterceptor(mfaCeremony mfa.MFACeremony) grpc.UnaryClientInterceptor { +func WithMFAUnaryInterceptor(mfaCeremony mfa.CeremonyFn) grpc.UnaryClientInterceptor { return func(ctx context.Context, method string, req, reply interface{}, cc *grpc.ClientConn, invoker grpc.UnaryInvoker, opts ...grpc.CallOption) error { // Check for MFA response passed through the context. if mfaResp, err := mfa.MFAResponseFromContext(ctx); err == nil { diff --git a/lib/auth/helpers_mfa.go b/lib/auth/helpers_mfa.go index 853c3ae8e5e1..d41e5e6e95ac 100644 --- a/lib/auth/helpers_mfa.go +++ b/lib/auth/helpers_mfa.go @@ -29,6 +29,7 @@ import ( "github.com/gravitational/teleport/api/client/proto" mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1" + "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/auth/mocku2f" wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" @@ -101,13 +102,15 @@ type authClientI interface { AddMFADeviceSync(context.Context, *proto.AddMFADeviceSyncRequest) (*proto.AddMFADeviceSyncResponse, error) } -func (d *TestDevice) registerDevice( - ctx context.Context, authClient authClientI, devName string, devType proto.DeviceType, authenticator *TestDevice) error { - // Re-authenticate using MFA. - authnChal, err := authClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{ - Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{ - ContextUser: &proto.ContextUser{}, +func (d *TestDevice) registerDevice(ctx context.Context, authClient authClientI, devName string, devType proto.DeviceType, authenticator *TestDevice) error { + mfaCeremony := &mfa.Ceremony{ + CreateAuthenticateChallenge: authClient.CreateAuthenticateChallenge, + SolveAuthenticateChallenge: func(_ context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { + return authenticator.SolveAuthn(chal) }, + } + + authnSolved, err := mfaCeremony.Run(ctx, &proto.CreateAuthenticateChallengeRequest{ ChallengeExtensions: &mfav1.ChallengeExtensions{ Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES, }, @@ -115,10 +118,6 @@ func (d *TestDevice) registerDevice( if err != nil { return trace.Wrap(err) } - authnSolved, err := authenticator.SolveAuthn(authnChal) - if err != nil { - return trace.Wrap(err) - } // Acquire and solve registration challenge. usage := proto.DeviceUsage_DEVICE_USAGE_MFA diff --git a/lib/client/api.go b/lib/client/api.go index 6c65c18c6122..437ce593dc86 100644 --- a/lib/client/api.go +++ b/lib/client/api.go @@ -1506,7 +1506,7 @@ func (tc *TeleportClient) ReissueUserCerts(ctx context.Context, cachePolicy Cert // (according to RBAC), IssueCertsWithMFA will: // - for SSH certs, return the existing Key from the keystore. // - for TLS certs, fall back to ReissueUserCerts. -func (tc *TeleportClient) IssueUserCertsWithMFA(ctx context.Context, params ReissueParams, mfaPromptOpts ...mfa.PromptOpt) (*Key, error) { +func (tc *TeleportClient) IssueUserCertsWithMFA(ctx context.Context, params ReissueParams) (*Key, error) { ctx, span := tc.Tracer.Start( ctx, "teleportClient/IssueUserCertsWithMFA", @@ -1520,7 +1520,7 @@ func (tc *TeleportClient) IssueUserCertsWithMFA(ctx context.Context, params Reis } defer clusterClient.Close() - key, _, err := clusterClient.IssueUserCertsWithMFA(ctx, params, tc.NewMFAPrompt(mfaPromptOpts...)) + key, _, err := clusterClient.IssueUserCertsWithMFA(ctx, params) return key, trace.Wrap(err) } @@ -3708,10 +3708,10 @@ func (tc *TeleportClient) mfaLocalLoginWeb(ctx context.Context, priv *keys.Priva } clt, session, err := SSHAgentMFAWebSessionLogin(ctx, SSHLoginMFA{ - SSHLogin: sshLogin, - User: tc.Username, - Password: password, - PromptMFA: tc.NewMFAPrompt(), + SSHLogin: sshLogin, + User: tc.Username, + Password: password, + MFAPromptConstructor: tc.NewMFAPrompt, }) return clt, session, trace.Wrap(err) } @@ -4000,10 +4000,10 @@ func (tc *TeleportClient) mfaLocalLogin(ctx context.Context, priv *keys.PrivateK } response, err := SSHAgentMFALogin(ctx, SSHLoginMFA{ - SSHLogin: sshLogin, - User: tc.Username, - Password: password, - PromptMFA: tc.NewMFAPrompt(), + SSHLogin: sshLogin, + User: tc.Username, + Password: password, + MFAPromptConstructor: tc.NewMFAPrompt, }) return response, trace.Wrap(err) @@ -5146,10 +5146,7 @@ func (tc *TeleportClient) HeadlessApprove(ctx context.Context, headlessAuthentic } } - chal, err := rootClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{ - Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{ - ContextUser: &proto.ContextUser{}, - }, + mfaResp, err := tc.NewMFACeremony().Run(ctx, &proto.CreateAuthenticateChallengeRequest{ ChallengeExtensions: &mfav1.ChallengeExtensions{ Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_HEADLESS_LOGIN, }, @@ -5158,11 +5155,6 @@ func (tc *TeleportClient) HeadlessApprove(ctx context.Context, headlessAuthentic return trace.Wrap(err) } - resp, err := tc.PromptMFA(ctx, chal) - if err != nil { - return trace.Wrap(err) - } - - err = rootClient.UpdateHeadlessAuthenticationState(ctx, headlessAuthenticationID, types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_APPROVED, resp) + err = rootClient.UpdateHeadlessAuthenticationState(ctx, headlessAuthenticationID, types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_APPROVED, mfaResp) return trace.Wrap(err) } diff --git a/lib/client/cluster_client.go b/lib/client/cluster_client.go index 024e646ffb2d..3aa8a679c050 100644 --- a/lib/client/cluster_client.go +++ b/lib/client/cluster_client.go @@ -20,6 +20,7 @@ package client import ( "context" + "errors" "net" "time" @@ -299,7 +300,7 @@ func (c *ClusterClient) SessionSSHConfig(ctx context.Context, user string, targe } log.Debug("Attempting to issue a single-use user certificate with an MFA check.") - key, err = c.performMFACeremony(ctx, + key, err = c.performSessionMFACeremony(ctx, mfaClt, ReissueParams{ NodeName: nodeName(TargetNode{Addr: target.Addr}), @@ -307,7 +308,6 @@ func (c *ClusterClient) SessionSSHConfig(ctx context.Context, user string, targe MFACheck: target.MFACheck, }, key, - c.tc.NewMFAPrompt(), ) if err != nil { return nil, trace.Wrap(err) @@ -368,32 +368,43 @@ func (c *ClusterClient) prepareUserCertsRequest(params ReissueParams, key *Key) }, nil } -// performMFACeremony runs the mfa ceremony to completion. +// performSessionMFACeremony runs the mfa ceremony to completion. // If successful the returned [Key] will be authorized to connect to the target. -func (c *ClusterClient) performMFACeremony(ctx context.Context, rootClient *ClusterClient, params ReissueParams, key *Key, mfaPrompt mfa.Prompt) (*Key, error) { +func (c *ClusterClient) performSessionMFACeremony(ctx context.Context, rootClient *ClusterClient, params ReissueParams, key *Key) (*Key, error) { certsReq, err := rootClient.prepareUserCertsRequest(params, key) if err != nil { return nil, trace.Wrap(err) } - key, _, err = PerformMFACeremony(ctx, PerformMFACeremonyParams{ + mfaRequiredReq := params.isMFARequiredRequest(c.tc.HostLogin) + + var promptOpts []mfa.PromptOpt + switch { + case params.NodeName != "": + promptOpts = append(promptOpts, mfa.WithPromptReasonSessionMFA("Node", params.NodeName)) + case params.KubernetesCluster != "": + promptOpts = append(promptOpts, mfa.WithPromptReasonSessionMFA("Kubernetes cluster", params.KubernetesCluster)) + case params.RouteToDatabase.ServiceName != "": + promptOpts = append(promptOpts, mfa.WithPromptReasonSessionMFA("Database", params.RouteToDatabase.ServiceName)) + case params.RouteToApp.Name != "": + promptOpts = append(promptOpts, mfa.WithPromptReasonSessionMFA("Application", params.RouteToApp.Name)) + } + + key, _, err = PerformSessionMFACeremony(ctx, PerformSessionMFACeremonyParams{ CurrentAuthClient: c.AuthClient, RootAuthClient: rootClient.AuthClient, - MFAPrompt: mfaPrompt, + MFACeremony: c.tc.NewMFACeremony(), MFAAgainstRoot: c.cluster == rootClient.cluster, - MFARequiredReq: params.isMFARequiredRequest(c.tc.HostLogin), - ChallengeExtensions: mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION, - }, - CertsReq: certsReq, - Key: key, - }) + MFARequiredReq: mfaRequiredReq, + CertsReq: certsReq, + Key: key, + }, promptOpts...) return key, trace.Wrap(err) } // IssueUserCertsWithMFA generates a single-use certificate for the user. If MFA is required // to access the resource the provided [mfa.Prompt] will be used to perform the MFA ceremony. -func (c *ClusterClient) IssueUserCertsWithMFA(ctx context.Context, params ReissueParams, mfaPrompt mfa.Prompt) (*Key, proto.MFARequired, error) { +func (c *ClusterClient) IssueUserCertsWithMFA(ctx context.Context, params ReissueParams) (*Key, proto.MFARequired, error) { ctx, span := c.Tracer.Start( ctx, "ClusterClient/IssueUserCertsWithMFA", @@ -489,7 +500,7 @@ func (c *ClusterClient) IssueUserCertsWithMFA(ctx context.Context, params Reissu } // Perform the MFA ceremony and retrieve a new key. - key, err := c.performMFACeremony(ctx, certClient, params, key, mfaPrompt) + key, err := c.performSessionMFACeremony(ctx, certClient, params, key) if err != nil { return nil, proto.MFARequired_MFA_REQUIRED_YES, trace.Wrap(err) } @@ -498,39 +509,36 @@ func (c *ClusterClient) IssueUserCertsWithMFA(ctx context.Context, params Reissu return key, proto.MFARequired_MFA_REQUIRED_YES, nil } -// PerformMFARootClient is a subset of Auth methods required for MFA. -// Used by [PerformMFACeremony]. -type PerformMFARootClient interface { +// PerformSessionMFARootClient is a subset of Auth methods required for MFA. +// Used by [PerformSessionMFACeremony]. +type PerformSessionMFARootClient interface { CreateAuthenticateChallenge(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) GenerateUserCerts(ctx context.Context, req proto.UserCertsRequest) (*proto.Certs, error) } -// PerformMFACurrentClient is a subset of Auth methods required for MFA. -// Used by [PerformMFACeremony]. -type PerformMFACurrentClient interface { +// PerformSessionMFACurrentClient is a subset of Auth methods required for MFA. +// Used by [PerformSessionMFACeremony]. +type PerformSessionMFACurrentClient interface { IsMFARequired(ctx context.Context, req *proto.IsMFARequiredRequest) (*proto.IsMFARequiredResponse, error) } -// PerformMFACeremonyParams are the input parameters for [PerformMFACeremony]. -type PerformMFACeremonyParams struct { +// PerformSessionMFACeremonyParams are the input parameters for [PerformSessionMFACeremony]. +type PerformSessionMFACeremonyParams struct { // CurrentAuthClient is the Auth client for the target cluster. // Unused if MFAAgainstRoot is true. - CurrentAuthClient PerformMFACurrentClient + CurrentAuthClient PerformSessionMFACurrentClient // RootAuthClient is the Auth client for the root cluster. // This is the client used to acquire the authn challenge and issue the user // certificates. - RootAuthClient PerformMFARootClient - // MFAPrompt is used to prompt the user for an MFA solution. - MFAPrompt mfa.Prompt + RootAuthClient PerformSessionMFARootClient + // MFACeremony handles the MFA ceremony. + MFACeremony *mfa.Ceremony // MFAAgainstRoot tells whether to run the MFA required check against root or // current cluster. MFAAgainstRoot bool // MFARequiredReq is the request for the MFA verification check. MFARequiredReq *proto.IsMFARequiredRequest - // ChallengeExtensions is used to provide additional extensions to apply to the - // MFA challenge used in the ceremony. The scope extension must be supplied. - ChallengeExtensions mfav1.ChallengeExtensions // CertsReq is the request for new certificates. CertsReq *proto.UserCertsRequest @@ -539,7 +547,7 @@ type PerformMFACeremonyParams struct { Key *Key } -// PerformMFACeremony issues single-use certificates via GenerateUserCerts, +// PerformSessionMFACeremony issues single-use certificates via GenerateUserCerts, // following its recommended RPC flow. // // It is a lower-level, less opinionated variant of @@ -555,10 +563,9 @@ type PerformMFACeremonyParams struct { // 4. Call RootAuthClient.GenerateUserCerts // // Returns the modified params.Key and the GenerateUserCertsResponse, or an error. -func PerformMFACeremony(ctx context.Context, params PerformMFACeremonyParams) (*Key, *proto.Certs, error) { +func PerformSessionMFACeremony(ctx context.Context, params PerformSessionMFACeremonyParams, promptOpts ...mfa.PromptOpt) (*Key, *proto.Certs, error) { rootClient := params.RootAuthClient currentClient := params.CurrentAuthClient - mfaRequiredReq := params.MFARequiredReq // If connecting to a host in a leaf cluster and MFA failed check to see @@ -576,8 +583,8 @@ func PerformMFACeremony(ctx context.Context, params PerformMFACeremonyParams) (* mfaRequiredReq = nil // Already checked, don't check again at root. } - // Acquire MFA challenge. - authnChal, err := rootClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{ + params.MFACeremony.CreateAuthenticateChallenge = rootClient.CreateAuthenticateChallenge + mfaResp, err := params.MFACeremony.Run(ctx, &proto.CreateAuthenticateChallengeRequest{ Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{ ContextUser: &proto.ContextUser{}, }, @@ -585,32 +592,24 @@ func PerformMFACeremony(ctx context.Context, params PerformMFACeremonyParams) (* ChallengeExtensions: &mfav1.ChallengeExtensions{ Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION, }, - }) - if err != nil { - return nil, nil, trace.Wrap(err) - } - - log.Debugf("MFA requirement from CreateAuthenticateChallenge, MFARequired=%s", authnChal.GetMFARequired()) - if authnChal.MFARequired == proto.MFARequired_MFA_REQUIRED_NO { + }, promptOpts...) + if errors.Is(err, &mfa.ErrMFANotRequired) { return nil, nil, trace.Wrap(services.ErrSessionMFANotRequired) + } else if err != nil { + return nil, nil, trace.Wrap(err) } - if authnChal.TOTP == nil && authnChal.WebauthnChallenge == nil { - // TODO(Joerger): CreateAuthenticateChallenge should return - // this error directly instead of an empty challenge, without - // regressing https://github.com/gravitational/teleport/issues/36482. + // If mfaResp is nil, the ceremony was a no-op (no devices registered). + // TODO(Joerger): CreateAuthenticateChallenge, should return + // this error directly instead of an empty challenge, without + // regressing https://github.com/gravitational/teleport/issues/36482. + if mfaResp == nil { return nil, nil, trace.Wrap(authclient.ErrNoMFADevices) } - // Prompt user for solution (eg, security key touch). - authnSolved, err := params.MFAPrompt.Run(ctx, authnChal) - if err != nil { - return nil, nil, trace.Wrap(ceremonyFailedErr{err}) - } - // Issue certificate. certsReq := params.CertsReq - certsReq.MFAResponse = authnSolved + certsReq.MFAResponse = mfaResp certsReq.Purpose = proto.UserCertsRequest_CERT_PURPOSE_SINGLE_USE_CERTS log.Debug("Issuing single-use certificate from unary GenerateUserCerts") newCerts, err := rootClient.GenerateUserCerts(ctx, *certsReq) diff --git a/lib/client/cluster_client_test.go b/lib/client/cluster_client_test.go index b23e2dbcf25c..43d2a2dea528 100644 --- a/lib/client/cluster_client_test.go +++ b/lib/client/cluster_client_test.go @@ -31,6 +31,7 @@ import ( "github.com/gravitational/teleport/api/mfa" webauthnpb "github.com/gravitational/teleport/api/types/webauthn" "github.com/gravitational/teleport/lib/auth/authclient" + libmfa "github.com/gravitational/teleport/lib/client/mfa" "github.com/gravitational/teleport/lib/fixtures" "github.com/gravitational/teleport/lib/observability/tracing" "github.com/gravitational/teleport/lib/services" @@ -191,7 +192,8 @@ func TestIssueUserCertsWithMFA(t *testing.T) { require.Nil(t, key) require.Equal(t, proto.MFARequired_MFA_REQUIRED_YES, mfaRequired) }, - }, { + }, + { name: "db no mfa", mfaRequired: proto.MFARequired_MFA_REQUIRED_NO, params: ReissueParams{ @@ -315,7 +317,12 @@ func TestIssueUserCertsWithMFA(t *testing.T) { clt := &ClusterClient{ tc: &TeleportClient{ localAgent: agent, - Config: Config{SiteName: "test"}, + Config: Config{ + SiteName: "test", + MFAPromptConstructor: func(cfg *libmfa.PromptConfig) mfa.Prompt { + return test.prompt + }, + }, }, ProxyClient: &proxy.Client{}, AuthClient: fakeAuthClient{ @@ -370,7 +377,7 @@ func TestIssueUserCertsWithMFA(t *testing.T) { ctx := context.Background() - key, mfaRequired, err := clt.IssueUserCertsWithMFA(ctx, test.params, test.prompt) + key, mfaRequired, err := clt.IssueUserCertsWithMFA(ctx, test.params) test.assertion(t, key, mfaRequired, err) }) } diff --git a/lib/client/local_proxy_middleware.go b/lib/client/local_proxy_middleware.go index 90a2713658a3..f4d8a1113b33 100644 --- a/lib/client/local_proxy_middleware.go +++ b/lib/client/local_proxy_middleware.go @@ -36,7 +36,6 @@ import ( "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/constants" - "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/auth/authclient" "github.com/gravitational/teleport/lib/defaults" @@ -238,7 +237,7 @@ func (c *DBCertIssuer) IssueCert(ctx context.Context) (tls.Certificate, error) { return trace.Wrap(err) } - newKey, mfaRequired, err := clusterClient.IssueUserCertsWithMFA(ctx, dbCertParams, c.Client.NewMFAPrompt(mfa.WithPromptReasonSessionMFA("database", c.RouteToApp.ServiceName))) + newKey, mfaRequired, err := clusterClient.IssueUserCertsWithMFA(ctx, dbCertParams) if err != nil { return trace.Wrap(err) } @@ -315,7 +314,7 @@ func (c *AppCertIssuer) IssueCert(ctx context.Context) (tls.Certificate, error) return trace.Wrap(err) } - newKey, mfaRequired, err := clusterClient.IssueUserCertsWithMFA(ctx, appCertParams, c.Client.NewMFAPrompt(mfa.WithPromptReasonSessionMFA("application", c.RouteToApp.Name))) + newKey, mfaRequired, err := clusterClient.IssueUserCertsWithMFA(ctx, appCertParams) if err != nil { return trace.Wrap(err) } diff --git a/lib/client/mfa.go b/lib/client/mfa.go index 76c672b46f72..7ade0186f692 100644 --- a/lib/client/mfa.go +++ b/lib/client/mfa.go @@ -21,11 +21,34 @@ package client import ( "context" + "github.com/gravitational/trace" + "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/mfa" libmfa "github.com/gravitational/teleport/lib/client/mfa" ) +// NewMFACeremony returns a new MFA ceremony configured for this client. +func (tc *TeleportClient) NewMFACeremony() *mfa.Ceremony { + return &mfa.Ceremony{ + CreateAuthenticateChallenge: tc.createAuthenticateChallenge, + PromptConstructor: tc.NewMFAPrompt, + } +} + +// createAuthenticateChallenge creates and returns MFA challenges for a users registered MFA devices. +func (tc *TeleportClient) createAuthenticateChallenge(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) { + clusterClient, err := tc.ConnectToCluster(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + rootClient, err := clusterClient.ConnectToRootCluster(ctx) + if err != nil { + return nil, trace.Wrap(err) + } + return rootClient.CreateAuthenticateChallenge(ctx, req) +} + // WebauthnLoginFunc is a function that performs WebAuthn login. // Mimics the signature of [webauthncli.Login]. type WebauthnLoginFunc = libmfa.WebauthnLoginFunc @@ -42,11 +65,6 @@ func (tc *TeleportClient) NewMFAPrompt(opts ...mfa.PromptOpt) mfa.Prompt { return prompt } -// PromptMFA runs a standard MFA prompt from client settings. -func (tc *TeleportClient) PromptMFA(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { - return tc.NewMFAPrompt().Run(ctx, chal) -} - func (tc *TeleportClient) newPromptConfig(opts ...mfa.PromptOpt) *libmfa.PromptConfig { cfg := libmfa.NewPromptConfig(tc.WebProxyAddr, opts...) cfg.AuthenticatorAttachment = tc.AuthenticatorAttachment diff --git a/lib/client/presence.go b/lib/client/presence.go index edf101e74994..14a8cb5d542f 100644 --- a/lib/client/presence.go +++ b/lib/client/presence.go @@ -76,46 +76,58 @@ func RunPresenceTask(ctx context.Context, term io.Writer, maintainer PresenceMai return trace.Wrap(err) } - for { - select { - case <-ticker.Chan(): + mfaCeremony := &mfa.Ceremony{ + CreateAuthenticateChallenge: func(ctx context.Context, _ *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) { req := &proto.PresenceMFAChallengeSend{ Request: &proto.PresenceMFAChallengeSend_ChallengeRequest{ ChallengeRequest: &proto.PresenceMFAChallengeRequest{SessionID: sessionID}, }, } - err = stream.Send(req) - if err != nil { - return trace.Wrap(err) + if err := stream.Send(req); err != nil { + return nil, trace.Wrap(err) } challenge, err := stream.Recv() if err != nil { - return trace.Wrap(err) + return nil, trace.Wrap(err) } - fmt.Fprint(term, "\r\nTeleport > Please tap your MFA key\r\n") - // This is here to enforce the usage of a MFA device. // We don't support TOTP for live presence. challenge.TOTP = nil - solution, err := mfaPrompt.Run(ctx, challenge) + return challenge, nil + }, + SolveAuthenticateChallenge: func(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { + fmt.Fprint(term, "\r\nTeleport > Please tap your MFA key\r\n") + + mfaResp, err := mfaPrompt.Run(ctx, chal) if err != nil { fmt.Fprintf(term, "\r\nTeleport > Failed to confirm presence: %v\r\n", err) - return trace.Wrap(err) + return nil, trace.Wrap(err) } fmt.Fprint(term, "\r\nTeleport > Received MFA presence confirmation\r\n") + return mfaResp, nil + }, + } + + for { + select { + case <-ticker.Chan(): + mfaResp, err := mfaCeremony.Run(ctx, nil /* req is not needed for MaintainSessionPresence */) + if err != nil { + return trace.Wrap(err) + } - req = &proto.PresenceMFAChallengeSend{ + resp := &proto.PresenceMFAChallengeSend{ Request: &proto.PresenceMFAChallengeSend_ChallengeResponse{ - ChallengeResponse: solution, + ChallengeResponse: mfaResp, }, } - err = stream.Send(req) + err = stream.Send(resp) if err != nil { return trace.Wrap(err) } diff --git a/lib/client/weblogin.go b/lib/client/weblogin.go index ca3d60eac344..1886ffb2eea2 100644 --- a/lib/client/weblogin.go +++ b/lib/client/weblogin.go @@ -52,7 +52,6 @@ import ( "github.com/gravitational/teleport/lib/auth/authclient" wancli "github.com/gravitational/teleport/lib/auth/webauthncli" wantypes "github.com/gravitational/teleport/lib/auth/webauthntypes" - libmfa "github.com/gravitational/teleport/lib/client/mfa" "github.com/gravitational/teleport/lib/defaults" "github.com/gravitational/teleport/lib/httplib" "github.com/gravitational/teleport/lib/httplib/csrf" @@ -282,9 +281,8 @@ type SSHLoginDirect struct { // SSHLoginMFA contains SSH login parameters for MFA login. type SSHLoginMFA struct { SSHLogin - // PromptMFA is a customizable MFA prompt function. - // Defaults to [mfa.NewPrompt().Run] - PromptMFA mfa.Prompt + // MFAPromptConstructor is a custom MFA prompt constructor to use when prompting for MFA. + MFAPromptConstructor mfa.PromptConstructor // User is the login username. User string // Password is the login password. @@ -633,35 +631,7 @@ func SSHAgentMFALogin(ctx context.Context, login SSHLoginMFA) (*authclient.SSHLo return nil, trace.Wrap(err) } - beginReq := MFAChallengeRequest{ - User: login.User, - Pass: login.Password, - } - challengeJSON, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "mfa", "login", "begin"), beginReq) - if err != nil { - return nil, trace.Wrap(err) - } - - challenge := &MFAAuthenticateChallenge{} - if err := json.Unmarshal(challengeJSON.Bytes(), challenge); err != nil { - return nil, trace.Wrap(err) - } - - // Convert to auth gRPC proto challenge. - chal := &proto.MFAAuthenticateChallenge{} - if challenge.TOTPChallenge { - chal.TOTP = &proto.TOTPChallenge{} - } - if challenge.WebauthnChallenge != nil { - chal.WebauthnChallenge = wantypes.CredentialAssertionToProto(challenge.WebauthnChallenge) - } - - promptMFA := login.PromptMFA - if promptMFA == nil { - promptMFA = libmfa.NewCLIPrompt(libmfa.NewPromptConfig(login.ProxyAddr), os.Stderr) - } - - respPB, err := promptMFA.Run(ctx, chal) + mfaResp, err := newMFALoginCeremony(clt, login).Run(ctx, nil) if err != nil { return nil, trace.Wrap(err) } @@ -677,7 +647,7 @@ func SSHAgentMFALogin(ctx context.Context, login SSHLoginMFA) (*authclient.SSHLo AttestationStatement: login.AttestationStatement, } // Convert back from auth gRPC proto response. - switch r := respPB.Response.(type) { + switch r := mfaResp.Response.(type) { case *proto.MFAAuthenticateResponse_TOTP: challengeResp.TOTPCode = r.TOTP.Code case *proto.MFAAuthenticateResponse_Webauthn: @@ -695,6 +665,37 @@ func SSHAgentMFALogin(ctx context.Context, login SSHLoginMFA) (*authclient.SSHLo return loginResp, trace.Wrap(json.Unmarshal(loginRespJSON.Bytes(), loginResp)) } +func newMFALoginCeremony(clt *WebClient, login SSHLoginMFA) *mfa.Ceremony { + return &mfa.Ceremony{ + CreateAuthenticateChallenge: func(ctx context.Context, req *proto.CreateAuthenticateChallengeRequest) (*proto.MFAAuthenticateChallenge, error) { + beginReq := MFAChallengeRequest{ + User: login.User, + Pass: login.Password, + } + challengeJSON, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "mfa", "login", "begin"), beginReq) + if err != nil { + return nil, trace.Wrap(err) + } + + challenge := &MFAAuthenticateChallenge{} + if err := json.Unmarshal(challengeJSON.Bytes(), challenge); err != nil { + return nil, trace.Wrap(err) + } + + // Convert to auth gRPC proto challenge. + chal := &proto.MFAAuthenticateChallenge{} + if challenge.TOTPChallenge { + chal.TOTP = &proto.TOTPChallenge{} + } + if challenge.WebauthnChallenge != nil { + chal.WebauthnChallenge = wantypes.CredentialAssertionToProto(challenge.WebauthnChallenge) + } + return chal, nil + }, + PromptConstructor: login.MFAPromptConstructor, + } +} + // HostCredentials is used to fetch host credentials for a node. func HostCredentials(ctx context.Context, proxyAddr string, insecure bool, req types.RegisterUsingTokenRequest) (*proto.Certs, error) { clt, _, err := initClient(proxyAddr, insecure, nil, nil) @@ -831,35 +832,7 @@ func SSHAgentMFAWebSessionLogin(ctx context.Context, login SSHLoginMFA) (*WebCli return nil, nil, trace.Wrap(err) } - beginReq := MFAChallengeRequest{ - User: login.User, - Pass: login.Password, - } - challengeJSON, err := clt.PostJSON(ctx, clt.Endpoint("webapi", "mfa", "login", "begin"), beginReq) - if err != nil { - return nil, nil, trace.Wrap(err) - } - - challenge := &MFAAuthenticateChallenge{} - if err := json.Unmarshal(challengeJSON.Bytes(), challenge); err != nil { - return nil, nil, trace.Wrap(err) - } - - // Convert to auth gRPC proto challenge. - chal := &proto.MFAAuthenticateChallenge{} - if challenge.TOTPChallenge { - chal.TOTP = &proto.TOTPChallenge{} - } - if challenge.WebauthnChallenge != nil { - chal.WebauthnChallenge = wantypes.CredentialAssertionToProto(challenge.WebauthnChallenge) - } - - promptMFA := login.PromptMFA - if promptMFA == nil { - promptMFA = libmfa.NewCLIPrompt(libmfa.NewPromptConfig(login.ProxyAddr), os.Stderr) - } - - respPB, err := promptMFA.Run(ctx, chal) + mfaResp, err := newMFALoginCeremony(clt, login).Run(ctx, nil) if err != nil { return nil, nil, trace.Wrap(err) } @@ -868,7 +841,7 @@ func SSHAgentMFAWebSessionLogin(ctx context.Context, login SSHLoginMFA) (*WebCli User: login.User, } // Convert back from auth gRPC proto response. - switch r := respPB.Response.(type) { + switch r := mfaResp.Response.(type) { case *proto.MFAAuthenticateResponse_Webauthn: challengeResp.WebauthnAssertionResponse = wantypes.CredentialAssertionResponseFromProto(r.Webauthn) default: diff --git a/lib/teleterm/clusters/cluster_apps.go b/lib/teleterm/clusters/cluster_apps.go index 3512a3dbf05f..8cff401277ed 100644 --- a/lib/teleterm/clusters/cluster_apps.go +++ b/lib/teleterm/clusters/cluster_apps.go @@ -26,7 +26,6 @@ import ( apiclient "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/defaults" - "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" "github.com/gravitational/teleport/lib/auth/authclient" @@ -189,7 +188,7 @@ func (c *Cluster) ReissueAppCert(ctx context.Context, clusterClient *client.Clus AccessRequests: c.status.ActiveRequests.AccessRequests, RequesterName: proto.UserCertsRequest_TSH_APP_LOCAL_PROXY, TTL: c.clusterClient.KeyTTL, - }, c.clusterClient.NewMFAPrompt(mfa.WithPromptReasonSessionMFA("application", routeToApp.Name))) + }) if err != nil { return tls.Certificate{}, trace.Wrap(err) } diff --git a/lib/teleterm/clusters/cluster_auth.go b/lib/teleterm/clusters/cluster_auth.go index 16793f715a4e..ffa227adf578 100644 --- a/lib/teleterm/clusters/cluster_auth.go +++ b/lib/teleterm/clusters/cluster_auth.go @@ -233,9 +233,9 @@ func (c *Cluster) localMFALogin(user, password string) client.SSHLoginFunc { RouteToCluster: c.clusterClient.SiteName, KubernetesCluster: c.clusterClient.KubernetesCluster, }, - User: user, - Password: password, - PromptMFA: c.clusterClient.NewMFAPrompt(), + User: user, + Password: password, + MFAPromptConstructor: c.clusterClient.NewMFAPrompt, }) if err != nil { return nil, trace.Wrap(err) diff --git a/lib/teleterm/clusters/cluster_databases.go b/lib/teleterm/clusters/cluster_databases.go index ed65f859ca6e..d4f792bb08f1 100644 --- a/lib/teleterm/clusters/cluster_databases.go +++ b/lib/teleterm/clusters/cluster_databases.go @@ -28,7 +28,6 @@ import ( apiclient "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/defaults" - "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" "github.com/gravitational/teleport/lib/auth/authclient" @@ -142,7 +141,7 @@ func (c *Cluster) reissueDBCerts(ctx context.Context, clusterClient *client.Clus AccessRequests: c.status.ActiveRequests.AccessRequests, RequesterName: proto.UserCertsRequest_TSH_DB_LOCAL_PROXY_TUNNEL, TTL: c.clusterClient.KeyTTL, - }, c.clusterClient.NewMFAPrompt(mfa.WithPromptReasonSessionMFA("database", routeToDatabase.ServiceName))) + }) if err != nil { return tls.Certificate{}, trace.Wrap(err) } diff --git a/lib/teleterm/clusters/cluster_headless.go b/lib/teleterm/clusters/cluster_headless.go index 3313616551f4..a714810480c6 100644 --- a/lib/teleterm/clusters/cluster_headless.go +++ b/lib/teleterm/clusters/cluster_headless.go @@ -72,11 +72,9 @@ func (c *Cluster) UpdateHeadlessAuthenticationState(ctx context.Context, rootAut err := AddMetadataToRetryableError(ctx, func() error { // If changing state to approved, create an MFA challenge and prompt for MFA. var mfaResponse *proto.MFAAuthenticateResponse + var err error if state == types.HeadlessAuthenticationState_HEADLESS_AUTHENTICATION_STATE_APPROVED { - chall, err := rootAuthClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{ - Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{ - ContextUser: &proto.ContextUser{}, - }, + mfaResponse, err = c.clusterClient.NewMFACeremony().Run(ctx, &proto.CreateAuthenticateChallengeRequest{ ChallengeExtensions: &mfav1.ChallengeExtensions{ Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_HEADLESS_LOGIN, }, @@ -84,14 +82,9 @@ func (c *Cluster) UpdateHeadlessAuthenticationState(ctx context.Context, rootAut if err != nil { return trace.Wrap(err) } - - mfaResponse, err = c.clusterClient.PromptMFA(ctx, chall) - if err != nil { - return trace.Wrap(err) - } } - err := rootAuthClient.UpdateHeadlessAuthenticationState(ctx, headlessID, state, mfaResponse) + err = rootAuthClient.UpdateHeadlessAuthenticationState(ctx, headlessID, state, mfaResponse) return trace.Wrap(err) }) return trace.Wrap(err) diff --git a/lib/teleterm/clusters/cluster_kubes.go b/lib/teleterm/clusters/cluster_kubes.go index d18a3bd7db65..ab654398dad6 100644 --- a/lib/teleterm/clusters/cluster_kubes.go +++ b/lib/teleterm/clusters/cluster_kubes.go @@ -28,7 +28,6 @@ import ( apiclient "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/defaults" - "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" api "github.com/gravitational/teleport/gen/proto/go/teleport/lib/teleterm/v1" "github.com/gravitational/teleport/lib/auth/authclient" @@ -118,7 +117,6 @@ func (c *Cluster) reissueKubeCert(ctx context.Context, clusterClient *client.Clu RequesterName: proto.UserCertsRequest_TSH_KUBE_LOCAL_PROXY, TTL: c.clusterClient.KeyTTL, }, - c.clusterClient.NewMFAPrompt(mfa.WithPromptReasonSessionMFA("Kubernetes cluster", kubeCluster)), ) if err != nil { return tls.Certificate{}, trace.Wrap(err) diff --git a/lib/web/desktop.go b/lib/web/desktop.go index 9001ce437b3d..eca8ee5552fc 100644 --- a/lib/web/desktop.go +++ b/lib/web/desktop.go @@ -41,7 +41,6 @@ import ( "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/constants" - mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1" "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/keys" @@ -322,7 +321,7 @@ func (h *Handler) issueCerts( withheld *[]tdp.Message, ) (certs *proto.Certs, err error) { if mfaRequired { - certs, err = h.performMFACeremony(ctx, ws, sctx, certsReq, withheld) + certs, err = h.performSessionMFACeremony(ctx, ws, sctx, certsReq, withheld) if err != nil { return nil, trace.Wrap(err) } @@ -361,92 +360,92 @@ func (h *Handler) createDesktopTLSConfig( return tlsConfig, nil } -// performMFACeremony completes the mfa ceremony and returns the raw TLS certificate +// performSessionMFACeremony completes the mfa ceremony and returns the raw TLS certificate // on success. The user will be prompted to tap their security key by the UI // in order to perform the assertion. -func (h *Handler) performMFACeremony( +func (h *Handler) performSessionMFACeremony( ctx context.Context, ws *websocket.Conn, sctx *SessionContext, certsReq *proto.UserCertsRequest, withheld *[]tdp.Message, ) (_ *proto.Certs, err error) { - ctx, span := h.tracer.Start(ctx, "desktop/performMFACeremony") + ctx, span := h.tracer.Start(ctx, "desktop/performSessionMFACeremony") defer func() { span.RecordError(err) span.End() }() - promptMFA := mfa.PromptFunc(func(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { - codec := tdpMFACodec{} - - // Send the challenge over the socket. - msg, err := codec.Encode( - &client.MFAAuthenticateChallenge{ - WebauthnChallenge: wantypes.CredentialAssertionFromProto(chal.WebauthnChallenge), - }, - defaults.WebsocketWebauthnChallenge, - ) - if err != nil { - return nil, trace.Wrap(err) - } - - if err := ws.WriteMessage(websocket.BinaryMessage, msg); err != nil { - return nil, trace.Wrap(err) - } - - span.AddEvent("waiting for user to complete mfa ceremony") - var buf []byte - // Loop through incoming messages until we receive an MFA message that lets us - // complete the ceremony. Non-MFA messages (e.g. ClientScreenSpecs representing - // screen resizes) are withheld for later. - for { - var ty int - ty, buf, err = ws.ReadMessage() + mfaCeremony := &mfa.Ceremony{ + CreateAuthenticateChallenge: sctx.cfg.RootClient.CreateAuthenticateChallenge, + SolveAuthenticateChallenge: func(ctx context.Context, chal *proto.MFAAuthenticateChallenge) (*proto.MFAAuthenticateResponse, error) { + codec := tdpMFACodec{} + + // Send the challenge over the socket. + msg, err := codec.Encode( + &client.MFAAuthenticateChallenge{ + WebauthnChallenge: wantypes.CredentialAssertionFromProto(chal.WebauthnChallenge), + }, + defaults.WebsocketWebauthnChallenge, + ) if err != nil { return nil, trace.Wrap(err) } - if ty != websocket.BinaryMessage { - return nil, trace.BadParameter("received unexpected web socket message type %d", ty) - } - if len(buf) == 0 { - return nil, trace.BadParameter("empty message received") + + if err := ws.WriteMessage(websocket.BinaryMessage, msg); err != nil { + return nil, trace.Wrap(err) } - if tdp.MessageType(buf[0]) != tdp.TypeMFA { - // This is not an MFA message, withhold it for later. - msg, err := tdp.Decode(buf) - h.log.Debugf("Received non-MFA message, withholding:", msg) + span.AddEvent("waiting for user to complete mfa ceremony") + var buf []byte + // Loop through incoming messages until we receive an MFA message that lets us + // complete the ceremony. Non-MFA messages (e.g. ClientScreenSpecs representing + // screen resizes) are withheld for later. + for { + var ty int + ty, buf, err = ws.ReadMessage() if err != nil { return nil, trace.Wrap(err) } - *withheld = append(*withheld, msg) - continue - } + if ty != websocket.BinaryMessage { + return nil, trace.BadParameter("received unexpected web socket message type %d", ty) + } + if len(buf) == 0 { + return nil, trace.BadParameter("empty message received") + } - break - } + if tdp.MessageType(buf[0]) != tdp.TypeMFA { + // This is not an MFA message, withhold it for later. + msg, err := tdp.Decode(buf) + h.log.Debugf("Received non-MFA message, withholding:", msg) + if err != nil { + return nil, trace.Wrap(err) + } + *withheld = append(*withheld, msg) + continue + } - assertion, err := codec.DecodeResponse(buf, defaults.WebsocketWebauthnChallenge) - if err != nil { - return nil, trace.Wrap(err) - } - span.AddEvent("mfa ceremony completed") + break + } - return assertion, nil - }) + assertion, err := codec.DecodeResponse(buf, defaults.WebsocketWebauthnChallenge) + if err != nil { + return nil, trace.Wrap(err) + } + span.AddEvent("mfa ceremony completed") - _, newCerts, err := client.PerformMFACeremony(ctx, client.PerformMFACeremonyParams{ + return assertion, nil + }, + } + + _, newCerts, err := client.PerformSessionMFACeremony(ctx, client.PerformSessionMFACeremonyParams{ CurrentAuthClient: nil, // Only RootAuthClient is used. RootAuthClient: sctx.cfg.RootClient, - MFAPrompt: promptMFA, + MFACeremony: mfaCeremony, MFAAgainstRoot: true, MFARequiredReq: nil, // No need to verify. - ChallengeExtensions: mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION, - }, - CertsReq: certsReq, - Key: nil, // We just want the certs. + CertsReq: certsReq, + Key: nil, // We just want the certs. }) if err != nil { return nil, trace.Wrap(err) @@ -483,7 +482,8 @@ type connector struct { // to any of the services or if it encounters an error that is not a connection problem. func (c *connector) connectToWindowsService( clusterName string, - desktopServiceIDs []string) (conn net.Conn, version string, err error) { + desktopServiceIDs []string, +) (conn net.Conn, version string, err error) { for _, id := range desktopServiceIDs { conn, ver, err := c.tryConnect(clusterName, id) if err != nil && !trace.IsConnectionProblem(err) { diff --git a/lib/web/kube.go b/lib/web/kube.go index f05e52d011b4..0762e6e55673 100644 --- a/lib/web/kube.go +++ b/lib/web/kube.go @@ -41,7 +41,6 @@ import ( clientproto "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/constants" - mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1" "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/keys" @@ -237,20 +236,22 @@ func (p *podHandler) handler(r *http.Request) error { Usage: clientproto.UserCertsRequest_Kubernetes, } - _, certs, err := client.PerformMFACeremony(ctx, client.PerformMFACeremonyParams{ + mfaCeremony := &mfa.Ceremony{ + CreateAuthenticateChallenge: p.sctx.cfg.RootClient.CreateAuthenticateChallenge, + SolveAuthenticateChallenge: func(ctx context.Context, chal *clientproto.MFAAuthenticateChallenge) (*clientproto.MFAAuthenticateResponse, error) { + assertion, err := mfaPrompt(stream.WSStream, protobufMFACodec{}).Run(ctx, chal) + return assertion, trace.Wrap(err) + }, + } + + _, certs, err := client.PerformSessionMFACeremony(ctx, client.PerformSessionMFACeremonyParams{ CurrentAuthClient: p.userClient, RootAuthClient: p.sctx.cfg.RootClient, - MFAPrompt: mfa.PromptFunc(func(ctx context.Context, chal *clientproto.MFAAuthenticateChallenge) (*clientproto.MFAAuthenticateResponse, error) { - assertion, err := promptMFAChallenge(stream.WSStream, protobufMFACodec{}).Run(ctx, chal) - return assertion, trace.Wrap(err) - }), - MFAAgainstRoot: p.sctx.cfg.RootClusterName == p.teleportCluster, + MFACeremony: mfaCeremony, + MFAAgainstRoot: p.sctx.cfg.RootClusterName == p.teleportCluster, MFARequiredReq: &clientproto.IsMFARequiredRequest{ Target: &clientproto.IsMFARequiredRequest_KubernetesCluster{KubernetesCluster: p.req.KubeCluster}, }, - ChallengeExtensions: mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION, - }, CertsReq: &certsReq, Key: userKey, }) diff --git a/lib/web/terminal.go b/lib/web/terminal.go index 20f7b16c61aa..a04cf63ba7d0 100644 --- a/lib/web/terminal.go +++ b/lib/web/terminal.go @@ -43,7 +43,6 @@ import ( "github.com/gravitational/teleport" authproto "github.com/gravitational/teleport/api/client/proto" apidefaults "github.com/gravitational/teleport/api/defaults" - mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1" "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/observability/tracing" tracessh "github.com/gravitational/teleport/api/observability/tracing/ssh" @@ -591,22 +590,24 @@ func (t *sshBaseHandler) issueSessionMFACerts(ctx context.Context, tc *client.Te SSHLogin: tc.HostLogin, } - key, _, err = client.PerformMFACeremony(ctx, client.PerformMFACeremonyParams{ - CurrentAuthClient: t.userAuthClient, - RootAuthClient: t.ctx.cfg.RootClient, - MFAPrompt: mfa.PromptFunc(func(ctx context.Context, chal *authproto.MFAAuthenticateChallenge) (*authproto.MFAAuthenticateResponse, error) { + mfaCeremony := &mfa.Ceremony{ + CreateAuthenticateChallenge: t.ctx.cfg.RootClient.CreateAuthenticateChallenge, + SolveAuthenticateChallenge: func(ctx context.Context, chal *authproto.MFAAuthenticateChallenge) (*authproto.MFAAuthenticateResponse, error) { span.AddEvent("prompting user with mfa challenge") - assertion, err := promptMFAChallenge(wsStream, protobufMFACodec{}).Run(ctx, chal) + assertion, err := mfaPrompt(wsStream, protobufMFACodec{}).Run(ctx, chal) span.AddEvent("user completed mfa challenge") return assertion, trace.Wrap(err) - }), - MFAAgainstRoot: t.ctx.cfg.RootClusterName == tc.SiteName, - MFARequiredReq: mfaRequiredReq, - ChallengeExtensions: mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_USER_SESSION, }, - CertsReq: certsReq, - Key: key, + } + + key, _, err = client.PerformSessionMFACeremony(ctx, client.PerformSessionMFACeremonyParams{ + CurrentAuthClient: t.userAuthClient, + RootAuthClient: t.ctx.cfg.RootClient, + MFACeremony: mfaCeremony, + MFAAgainstRoot: t.ctx.cfg.RootClusterName == tc.SiteName, + MFARequiredReq: mfaRequiredReq, + CertsReq: certsReq, + Key: key, }) if err != nil { return nil, trace.Wrap(err) @@ -621,7 +622,7 @@ func (t *sshBaseHandler) issueSessionMFACerts(ctx context.Context, tc *client.Te return []ssh.AuthMethod{am}, nil } -func promptMFAChallenge(stream *terminal.WSStream, codec terminal.MFACodec) mfa.Prompt { +func mfaPrompt(stream *terminal.WSStream, codec terminal.MFACodec) mfa.Prompt { return mfa.PromptFunc(func(ctx context.Context, chal *authproto.MFAAuthenticateChallenge) (*authproto.MFAAuthenticateResponse, error) { var challenge *client.MFAAuthenticateChallenge @@ -799,7 +800,7 @@ func (t *TerminalHandler) streamTerminal(ctx context.Context, tc *client.Telepor if t.participantMode == types.SessionModeratorMode { beforeStart = func(out io.Writer) { nc.OnMFA = func() { - if err := t.presenceChecker(ctx, out, t.userAuthClient, t.sessionData.ID.String(), promptMFAChallenge(t.stream.WSStream, protobufMFACodec{})); err != nil { + if err := t.presenceChecker(ctx, out, t.userAuthClient, t.sessionData.ID.String(), mfaPrompt(t.stream.WSStream, protobufMFACodec{})); err != nil { t.log.WithError(err).Warn("Unable to stream terminal - failure performing presence checks") return } diff --git a/tool/tsh/common/app.go b/tool/tsh/common/app.go index 18e4d78ea92f..2edaf86cdb3c 100644 --- a/tool/tsh/common/app.go +++ b/tool/tsh/common/app.go @@ -34,7 +34,6 @@ import ( "github.com/gravitational/teleport" apiclient "github.com/gravitational/teleport/api/client" "github.com/gravitational/teleport/api/client/proto" - "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/lib/asciitable" "github.com/gravitational/teleport/lib/auth/authclient" @@ -120,8 +119,7 @@ func appLogin( return nil, trace.Wrap(err) } - key, _, err := clusterClient.IssueUserCertsWithMFA(ctx, appCertParams, - tc.NewMFAPrompt(mfa.WithPromptReasonSessionMFA("Application", appCertParams.RouteToApp.Name))) + key, _, err := clusterClient.IssueUserCertsWithMFA(ctx, appCertParams) return key, trace.Wrap(err) } diff --git a/tool/tsh/common/kube_proxy.go b/tool/tsh/common/kube_proxy.go index 18ff39cb82ca..c78b47d8da3f 100644 --- a/tool/tsh/common/kube_proxy.go +++ b/tool/tsh/common/kube_proxy.go @@ -39,7 +39,6 @@ import ( "github.com/gravitational/teleport/api/client/proto" apidefaults "github.com/gravitational/teleport/api/defaults" - "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/keys" "github.com/gravitational/teleport/lib/asciitable" @@ -590,7 +589,6 @@ func issueKubeCert(ctx context.Context, tc *client.TeleportClient, clusterClient RequesterName: requesterName, TTL: tc.KeyTTL, }, - tc.NewMFAPrompt(mfa.WithPromptReasonSessionMFA("Kubernetes cluster", kubeCluster)), ) if err != nil { return tls.Certificate{}, trace.Wrap(err) diff --git a/tool/tsh/common/mfa.go b/tool/tsh/common/mfa.go index 1ce15bd11ec3..c540cce3b122 100644 --- a/tool/tsh/common/mfa.go +++ b/tool/tsh/common/mfa.go @@ -36,7 +36,6 @@ import ( "github.com/gravitational/teleport/api/client/proto" "github.com/gravitational/teleport/api/constants" mfav1 "github.com/gravitational/teleport/api/gen/proto/go/teleport/mfa/v1" - "github.com/gravitational/teleport/api/mfa" "github.com/gravitational/teleport/api/types" "github.com/gravitational/teleport/api/utils/prompt" "github.com/gravitational/teleport/lib/asciitable" @@ -333,36 +332,29 @@ func (c *mfaAddCommand) addDeviceRPC(ctx context.Context, tc *client.TeleportCli usage = proto.DeviceUsage_DEVICE_USAGE_PASSWORDLESS } - // Issue the authn challenge. - // Required for the registration challenge. - authChallenge, err := rootAuthClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{ - ChallengeExtensions: &mfav1.ChallengeExtensions{ - Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES, - }, - }) - if err != nil { - return trace.Wrap(err) - } - // Tweak Windows platform messages so it's clear we whether we are prompting // for the *registered* or *new* device. // We do it here, preemptively, because it's the simpler solution (instead // of finding out whether it is a Windows prompt or not). + // + // TODO(Joerger): this should live in lib/client/mfa/cli.go using the prompt device prefix. const registeredMsg = "Using platform authentication for *registered* device, follow the OS dialogs" const newMsg = "Using platform authentication for *new* device, follow the OS dialogs" wanwin.SetPromptPlatformMessage(registeredMsg) defer wanwin.ResetPromptPlatformMessage() - // Prompt for authentication. - // Does nothing if no challenges were issued (aka user has no devices). - authnResp, err := tc.NewMFAPrompt(mfa.WithPromptDeviceType(mfa.DeviceDescriptorRegistered)).Run(ctx, authChallenge) + mfaResp, err := tc.NewMFACeremony().Run(ctx, &proto.CreateAuthenticateChallengeRequest{ + ChallengeExtensions: &mfav1.ChallengeExtensions{ + Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES, + }, + }) if err != nil { return trace.Wrap(err) } // Issue the registration challenge. registerChallenge, err := rootAuthClient.CreateRegisterChallenge(ctx, &proto.CreateRegisterChallengeRequest{ - ExistingMFAResponse: authnResp, + ExistingMFAResponse: mfaResp, DeviceType: devTypePB, DeviceUsage: usage, }) @@ -596,11 +588,7 @@ func (c *mfaRemoveCommand) run(cf *CLIConf) error { return trace.NotFound("device %q not found", c.name) } - // Issue and solve authn challenge. - authnChal, err := rootAuthClient.CreateAuthenticateChallenge(ctx, &proto.CreateAuthenticateChallengeRequest{ - Request: &proto.CreateAuthenticateChallengeRequest_ContextUser{ - ContextUser: &proto.ContextUser{}, - }, + mfaResponse, err := tc.NewMFACeremony().Run(ctx, &proto.CreateAuthenticateChallengeRequest{ ChallengeExtensions: &mfav1.ChallengeExtensions{ Scope: mfav1.ChallengeScope_CHALLENGE_SCOPE_MANAGE_DEVICES, }, @@ -608,15 +596,11 @@ func (c *mfaRemoveCommand) run(cf *CLIConf) error { if err != nil { return trace.Wrap(err) } - authnSolved, err := tc.PromptMFA(ctx, authnChal) - if err != nil { - return trace.Wrap(err) - } // Delete device. if err := rootAuthClient.DeleteMFADeviceSync(ctx, &proto.DeleteMFADeviceSyncRequest{ DeviceName: c.name, - ExistingMFAResponse: authnSolved, + ExistingMFAResponse: mfaResponse, }); err != nil { return trace.Wrap(err) } diff --git a/tool/tsh/common/tsh_test.go b/tool/tsh/common/tsh_test.go index 9a401c945263..ff8e35b41d39 100644 --- a/tool/tsh/common/tsh_test.go +++ b/tool/tsh/common/tsh_test.go @@ -1388,7 +1388,7 @@ func TestSSHOnMultipleNodes(t *testing.T) { webauthnLogin: successfulChallenge("localhost"), target: "env=prod", stderrAssertion: func(tt require.TestingT, i interface{}, i2 ...interface{}) { - require.Equal(t, "error\n", i, i2...) + require.Contains(t, i, "error\n", i2...) }, stdoutAssertion: func(t require.TestingT, i interface{}, i2 ...interface{}) { require.Equal(t, "test\n", i, i2...) @@ -1483,7 +1483,7 @@ func TestSSHOnMultipleNodes(t *testing.T) { require.Equal(t, "test\n", i, i2...) }, stderrAssertion: func(tt require.TestingT, i interface{}, i2 ...interface{}) { - require.Equal(t, "error\n", i, i2...) + require.Contains(t, i, "error\n", i2...) }, mfaPromptCount: 1, errAssertion: require.NoError, @@ -1570,7 +1570,7 @@ func TestSSHOnMultipleNodes(t *testing.T) { roles: []string{perSessionMFARole.GetName()}, webauthnLogin: successfulChallenge("leafcluster"), stderrAssertion: func(tt require.TestingT, i interface{}, i2 ...interface{}) { - require.Equal(t, "error\n", i, i2...) + require.Contains(t, i, "error\n", i2...) }, stdoutAssertion: func(t require.TestingT, i interface{}, i2 ...interface{}) { require.Equal(t, "test\n", i, i2...) @@ -1618,7 +1618,7 @@ func TestSSHOnMultipleNodes(t *testing.T) { roles: []string{perSessionMFARole.GetName()}, webauthnLogin: successfulChallenge("localhost"), stderrAssertion: func(tt require.TestingT, i interface{}, i2 ...interface{}) { - require.Equal(t, "error\n", i, i2...) + require.Contains(t, i, "error\n", i2...) }, stdoutAssertion: func(t require.TestingT, i interface{}, i2 ...interface{}) { require.Equal(t, "test\n", i, i2...)