fix(auth): set WebAuthn user verification per request (#38805)

Registration omitted `userVerification`, so Chromium raised the
credential to credProtect level 3 and the authenticator then hid it from
the second-factor login, which asked for `discouraged`. Registration and
each login now set their own value, with `preferred` on the second
factor so credentials already registered at level 3 keep working without
re-enrollment.

Also add relevant e2e test coverage for webauthn, one test chromium only
because Firefox lacks the APIs needed.

Fixes https://github.com/go-gitea/gitea/issues/33531
Fixes https://github.com/go-gitea/gitea/issues/36019
Fixes https://github.com/go-gitea/gitea/issues/38139
This commit is contained in:
silverwind
2026-08-07 00:11:34 +00:00
committed by GitHub
parent c210ef6dbb
commit 9dac77fdc2
16 changed files with 316 additions and 107 deletions
+11 -15
View File
@@ -54,7 +54,8 @@ func WebAuthnPasskeyAssertion(ctx *context.Context) {
return
}
assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin()
// a passkey is the only factor here
assertion, sessionData, err := wa.WebAuthn.BeginDiscoverableLogin(webauthn.WithUserVerification(protocol.VerificationRequired))
if err != nil {
ctx.ServerError("webauthn.BeginDiscoverableLogin", err)
return
@@ -91,7 +92,7 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
parsedResponse, err := protocol.ParseCredentialRequestResponse(ctx.Req)
if err != nil {
// Failed authentication attempt.
log.Info("Failed authentication attempt for %s from %s: %v", user.Name, ctx.RemoteAddr(), err)
log.Info("Failed authentication attempt from %s: %v", ctx.RemoteAddr(), err)
ctx.Status(http.StatusForbidden)
return
}
@@ -147,12 +148,9 @@ func WebAuthnPasskeyLogin(ctx *context.Context) {
return
}
// Now handle account linking if that's requested
if ctx.Session.Get("linkAccount") != nil {
if err := linkAccountFromContext(ctx, user); err != nil {
ctx.ServerError("LinkAccountFromStore", err)
return
}
if err := completePendingLinks(ctx, user); err != nil {
ctx.ServerError("completePendingLinks", err)
return
}
remember := false // TODO: implement remember me
@@ -186,7 +184,8 @@ func WebAuthnLoginAssertion(ctx *context.Context) {
}
webAuthnUser := wa.NewWebAuthnUser(ctx, user)
assertion, sessionData, err := wa.WebAuthn.BeginLogin(webAuthnUser)
// "discouraged" would hide credProtect protected credentials
assertion, sessionData, err := wa.WebAuthn.BeginLogin(webAuthnUser, webauthn.WithUserVerification(protocol.VerificationPreferred))
if err != nil {
ctx.ServerError("webauthn.BeginLogin", err)
return
@@ -261,12 +260,9 @@ func WebAuthnLoginAssertionPost(ctx *context.Context) {
return
}
// Now handle account linking if that's requested
if ctx.Session.Get("linkAccount") != nil {
if err := linkAccountFromContext(ctx, user); err != nil {
ctx.ServerError("LinkAccountFromStore", err)
return
}
if err := completePendingLinks(ctx, user); err != nil {
ctx.ServerError("completePendingLinks", err)
return
}
remember := ctx.Session.Get("twofaRemember").(bool)