-
Notifications
You must be signed in to change notification settings - Fork 2.1k
fix: Verify MFA device locks during authentication #36471
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -119,7 +119,7 @@ type SessionCreds struct { | |
| func (a *Server) AuthenticateUser(ctx context.Context, req AuthenticateUserRequest) (services.UserState, services.AccessChecker, error) { | ||
| username := req.Username | ||
|
|
||
| mfaDev, actualUsername, err := a.authenticateUser(ctx, req) | ||
| verifyMFALocks, mfaDev, actualUsername, err := a.authenticateUser(ctx, req) | ||
| if err != nil { | ||
| // Log event after authentication failure | ||
| if err := a.emitAuthAuditEvent(ctx, authAuditProps{ | ||
|
|
@@ -167,6 +167,23 @@ func (a *Server) AuthenticateUser(ctx context.Context, req AuthenticateUserReque | |
| return nil, nil, trace.Wrap(err) | ||
| } | ||
|
|
||
| // Verify if the MFA device is locked. | ||
| if err := verifyMFALocks(verifyMFADeviceLocksParams{ | ||
| Checker: checker, | ||
| }); err != nil { | ||
| // Log MFA lock failure as an authn failure. | ||
| if err := a.emitAuthAuditEvent(ctx, authAuditProps{ | ||
| username: req.Username, | ||
| clientMetadata: req.ClientMetadata, | ||
| mfaDevice: mfaDev, | ||
| checker: checker, | ||
| authErr: err, | ||
| }); err != nil { | ||
| log.WithError(err).Warn("Failed to emit login event.") | ||
| } | ||
| return nil, nil, trace.Wrap(err) | ||
| } | ||
|
|
||
| // Log event after authentication success | ||
| if err := a.emitAuthAuditEvent(ctx, authAuditProps{ | ||
| username: username, | ||
|
|
@@ -260,14 +277,77 @@ func IsInvalidLocalCredentialError(err error) bool { | |
| return errors.Is(err, invalidUserPassError) || errors.Is(err, invalidUserPass2FError) | ||
| } | ||
|
|
||
| type verifyMFADeviceLocksParams struct { | ||
| // Checker used to verify locks. | ||
| // Optional, created via a [UserState] fetch if nil. | ||
| Checker services.AccessChecker | ||
|
|
||
| // ClusterLockingMode used to verify locks. | ||
| // Optional, acquired from [Server.GetAuthPreference] if nil. | ||
| ClusterLockingMode constants.LockingMode | ||
| } | ||
|
|
||
| // authenticateUser authenticates a user through various methods (password, MFA, | ||
| // passwordless) | ||
| // Returns the device used to authenticate (if applicable) and the username. | ||
| func (a *Server) authenticateUser(ctx context.Context, req AuthenticateUserRequest) (*types.MFADevice, string, error) { | ||
| // | ||
| // Returns a callback to verify MFA device locks, the MFA device used to | ||
| // authenticate (if applicable), and the authenticated user name. | ||
| // | ||
| // Callers MUST call the verifyLocks callback. | ||
| func (a *Server) authenticateUser( | ||
| ctx context.Context, | ||
| req AuthenticateUserRequest, | ||
| ) (verifyLocks func(verifyMFADeviceLocksParams) error, mfaDev *types.MFADevice, user string, err error) { | ||
| mfaDev, user, err = a.authenticateUserInternal(ctx, req) | ||
| if err != nil || mfaDev == nil { | ||
| return func(verifyMFADeviceLocksParams) error { return nil }, mfaDev, user, trace.Wrap(err) | ||
| } | ||
|
|
||
| verifyLocks = func(p verifyMFADeviceLocksParams) error { | ||
| if p.Checker == nil { | ||
| userState, err := a.GetUserOrLoginState(ctx, user) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| accessInfo := services.AccessInfoFromUserState(userState) | ||
| clusterName, err := a.GetClusterName() | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| checker, err := services.NewAccessChecker(accessInfo, clusterName.GetClusterName(), a) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| p.Checker = checker | ||
| } | ||
|
|
||
| if p.ClusterLockingMode == "" { | ||
| authPref, err := a.GetAuthPreference(ctx) | ||
| if err != nil { | ||
| return trace.Wrap(err) | ||
| } | ||
| p.ClusterLockingMode = authPref.GetLockingMode() | ||
| } | ||
|
|
||
| // The MFA device needs to be explicitly verified, as it won't be verified | ||
| // as part of certificate issuance in various scenarios (password change, | ||
| // non-session certificates, etc) | ||
| return a.verifyLocksForUserCerts(verifyLocksForUserCertsReq{ | ||
| checker: p.Checker, | ||
| defaultMode: p.ClusterLockingMode, | ||
| username: user, | ||
| mfaVerified: mfaDev.Id, | ||
| }) | ||
| } | ||
| return verifyLocks, mfaDev, user, nil | ||
| } | ||
|
|
||
| // Do not use this method directly, use authenticateUser instead. | ||
| func (a *Server) authenticateUserInternal(ctx context.Context, req AuthenticateUserRequest) (mfaDev *types.MFADevice, user string, err error) { | ||
| if err := req.CheckAndSetDefaults(); err != nil { | ||
| return nil, "", trace.Wrap(err) | ||
| } | ||
| user := req.Username | ||
| user = req.Username | ||
|
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Changes here are only so we use and take advantage of the named return variables. No behavioral changes. |
||
| passwordless := user == "" | ||
|
|
||
| // Only one path if passwordless, other variants shouldn't see an empty user. | ||
|
|
@@ -299,8 +379,8 @@ func (a *Server) authenticateUser(ctx context.Context, req AuthenticateUserReque | |
| Webauthn: wantypes.CredentialAssertionResponseToProto(req.Webauthn), | ||
| }, | ||
| } | ||
| dev, _, err := a.ValidateMFAAuthResponse(ctx, mfaResponse, user, passwordless) | ||
| return dev, trace.Wrap(err) | ||
| mfaDev, _, err := a.ValidateMFAAuthResponse(ctx, mfaResponse, user, passwordless) | ||
| return mfaDev, trace.Wrap(err) | ||
| } | ||
| authErr = authenticateWebauthnError | ||
| case req.OTP != nil: | ||
|
|
@@ -316,10 +396,9 @@ func (a *Server) authenticateUser(ctx context.Context, req AuthenticateUserReque | |
| authErr = invalidUserPass2FError | ||
| } | ||
| if authenticateFn != nil { | ||
| var dev *types.MFADevice | ||
| err := a.WithUserLock(ctx, user, func() error { | ||
| var err error | ||
| dev, err = authenticateFn() | ||
| mfaDev, err = authenticateFn() | ||
| return err | ||
| }) | ||
| switch { | ||
|
|
@@ -330,13 +409,13 @@ func (a *Server) authenticateUser(ctx context.Context, req AuthenticateUserReque | |
| } | ||
|
|
||
| return nil, "", trace.Wrap(authErr) | ||
| case dev == nil: | ||
| case mfaDev == nil: | ||
| log.Debugf( | ||
| "MFA authentication returned nil device (Webauthn = %v, TOTP = %v, Headless = %v): %v.", | ||
| req.Webauthn != nil, req.OTP != nil, req.HeadlessAuthenticationID != "", err) | ||
| return nil, "", trace.Wrap(authErr) | ||
| default: | ||
| return dev, user, nil | ||
| return mfaDev, user, nil | ||
| } | ||
| } | ||
|
|
||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's surprisingly difficult to get a checker during login, as some of the information required to assemble it includes looking up the user itself. The availability of a checker also seems to vary between methods and I didn't want to change widely-used signatures (like ChangePassword), so this seemed a reasonable compromise.
I've opted for the slightly annoying callback-based design as I wanted a clear sign that something needs to be done, instead of leaving lock verification completely up to the caller.