Skip to content

Commit 5b450e8

Browse files
committed
Rebase to webauthn branch
1 parent df04993 commit 5b450e8

File tree

2 files changed

+74
-18
lines changed

2 files changed

+74
-18
lines changed

lib/auth/webauthn/login.go

Lines changed: 53 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,21 @@ type LoginData struct {
252252
TargetCluster string
253253
}
254254

255-
func (f *loginFlow) finish(ctx context.Context, user string, resp *wantypes.CredentialAssertionResponse, requiredExtensions *mfav1.ChallengeExtensions) (*LoginData, error) {
255+
// mfaValidationResult holds the results of MFA validation needed for subsequent operations.
256+
type mfaValidationResult struct {
257+
user string
258+
device *types.MFADevice
259+
credential *wan.Credential
260+
sessionData *wantypes.SessionData
261+
discoverableLogin bool
262+
challengeAllowReuse bool
263+
challenge string
264+
rpID string
265+
}
266+
267+
// validateMFAResponseInternal performs all cryptographic validation of an MFA
268+
// response without consuming it.
269+
func (f *loginFlow) validateMFAResponseInternal(ctx context.Context, user string, resp *wantypes.CredentialAssertionResponse, requiredExtensions *mfav1.ChallengeExtensions) (*mfaValidationResult, error) {
256270
if requiredExtensions == nil {
257271
return nil, trace.BadParameter("requested challenge extensions must be supplied.")
258272
}
@@ -413,44 +427,65 @@ func (f *loginFlow) finish(ctx context.Context, user string, resp *wantypes.Cred
413427
}
414428
}
415429

430+
// Return validation results without modifying any state
431+
return &mfaValidationResult{
432+
user: user,
433+
device: dev,
434+
credential: credential,
435+
sessionData: sd,
436+
discoverableLogin: discoverableLogin,
437+
challengeAllowReuse: challengeAllowReuse,
438+
challenge: challenge,
439+
rpID: rpID,
440+
}, nil
441+
}
442+
443+
func (f *loginFlow) finish(ctx context.Context, user string, resp *wantypes.CredentialAssertionResponse, requiredExtensions *mfav1.ChallengeExtensions) (*LoginData, error) {
444+
// Validate the MFA response
445+
result, err := f.validateMFAResponseInternal(ctx, user, resp, requiredExtensions)
446+
if err != nil {
447+
return nil, trace.Wrap(err)
448+
}
449+
416450
// Update last used timestamp and device counter.
417-
if err := updateCredentialAndTimestamps(dev, credential, discoverableLogin); err != nil {
451+
if err := updateCredentialAndTimestamps(result.device, result.credential, result.discoverableLogin); err != nil {
418452
return nil, trace.Wrap(err)
419453
}
454+
420455
// Retroactively write the credential RPID, now that it cleared authn.
421-
if webDev := dev.GetWebauthn(); webDev != nil && webDev.CredentialRpId == "" {
456+
if webDev := result.device.GetWebauthn(); webDev != nil && webDev.CredentialRpId == "" {
422457
log.DebugContext(ctx, "Recording RPID in device",
423-
"rpid", rpID,
424-
"user", user,
425-
"device", dev.GetName(),
458+
"rpid", result.rpID,
459+
"user", result.user,
460+
"device", result.device.GetName(),
426461
)
427-
webDev.CredentialRpId = rpID
462+
webDev.CredentialRpId = result.rpID
428463
}
429464

430-
if err := f.identity.UpsertMFADevice(ctx, user, dev); err != nil {
465+
if err := f.identity.UpsertMFADevice(ctx, result.user, result.device); err != nil {
431466
return nil, trace.Wrap(err)
432467
}
433468

434469
// The user just solved the challenge, so let's make sure it won't be used
435470
// again, unless reuse is explicitly allowed.
436471
// Note that even reusable sessions are deleted when their expiration time
437472
// passes.
438-
if !challengeAllowReuse {
439-
if err := f.sessionData.Delete(ctx, user, challenge); err != nil {
473+
if !result.challengeAllowReuse {
474+
if err := f.sessionData.Delete(ctx, result.user, result.challenge); err != nil {
440475
log.WarnContext(ctx, "failed to delete login SessionData for user",
441-
"user", user,
442-
"scope", sd.ChallengeExtensions.Scope,
476+
"user", result.user,
477+
"scope", result.sessionData.ChallengeExtensions.Scope,
443478
)
444479
}
445480
}
446481

447482
return &LoginData{
448-
User: user,
449-
Device: dev,
450-
AllowReuse: sd.ChallengeExtensions.AllowReuse,
451-
Payload: sd.Payload,
452-
SourceCluster: sd.SourceCluster,
453-
TargetCluster: sd.TargetCluster,
483+
User: result.user,
484+
Device: result.device,
485+
AllowReuse: result.sessionData.ChallengeExtensions.AllowReuse,
486+
Payload: result.sessionData.Payload,
487+
SourceCluster: result.sessionData.SourceCluster,
488+
TargetCluster: result.sessionData.TargetCluster,
454489
}, nil
455490
}
456491

lib/auth/webauthn/login_mfa.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,27 @@ func (f *LoginFlow) Finish(ctx context.Context, user string, resp *wantypes.Cred
147147
return lf.finish(ctx, user, resp, requiredExtensions)
148148
}
149149

150+
// Validate validates an MFA credential assertion response against the stored
151+
// challenge without consuming it (i.e., without updating device counters or deleting the session).
152+
// This is useful for multi-step flows where you want to validate the response early but only
153+
// consume it later when issuing credentials, such as the Browser MFA flow.
154+
//
155+
// Unlike Finish, this function:
156+
// - Doesn't update the device counter
157+
// - Doesn't persist any changes to the device
158+
// - Doesn't delete the session data
159+
//
160+
// Returns nil if validation succeeds, error otherwise.
161+
func (f *LoginFlow) Validate(ctx context.Context, user string, resp *wantypes.CredentialAssertionResponse, requiredExtensions *mfav1.ChallengeExtensions) error {
162+
lf := &loginFlow{
163+
U2F: f.U2F,
164+
Webauthn: f.Webauthn,
165+
identity: mfaIdentity{f.Identity},
166+
sessionData: (*userSessionStorage)(f),
167+
}
168+
return lf.validateOnly(ctx, user, resp, requiredExtensions)
169+
}
170+
150171
type mfaIdentity struct {
151172
LoginIdentity
152173
}

0 commit comments

Comments
 (0)