feat(auth): prefer rather than require passkey user verification
Requiring user verification made password managers such as NordPass ask for their master password on every passkey use. Every ceremony now asks for it as preferred and no longer rejects a result without it. This is a deliberate trade-off: a passkey used without verification is possession-only, so passwordless sign-in and step-up rest on the device or vault being unlocked. The spec records the decision.
This commit is contained in:
@@ -216,10 +216,6 @@ func HandleEnrolPasskeyFinish(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "that passkey could not be verified"})
|
||||
return
|
||||
}
|
||||
if !cred.Flags.UserVerified {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "this passkey does not verify the user"})
|
||||
return
|
||||
}
|
||||
transports := make([]string, 0, len(parsed.Response.Transports))
|
||||
for _, tr := range parsed.Response.Transports {
|
||||
transports = append(transports, string(tr))
|
||||
|
||||
@@ -44,7 +44,7 @@ func HandlePasskeyLoginBegin(c *gin.Context) {
|
||||
// Discoverable login: no allowCredentials, so the authenticator offers
|
||||
// whichever resident credential it holds for this RP ID.
|
||||
options, sessionData, err := w.BeginDiscoverableLogin(
|
||||
webauthn.WithUserVerification(protocol.VerificationRequired))
|
||||
webauthn.WithUserVerification(protocol.VerificationPreferred))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not start sign-in"})
|
||||
return
|
||||
|
||||
@@ -67,8 +67,14 @@ func webAuthnFor(c *gin.Context) (*webauthn.WebAuthn, error) {
|
||||
RPID: rpID,
|
||||
RPOrigins: []string{origin},
|
||||
AuthenticatorSelection: protocol.AuthenticatorSelection{
|
||||
ResidentKey: protocol.ResidentKeyRequirementRequired,
|
||||
UserVerification: protocol.VerificationRequired,
|
||||
ResidentKey: protocol.ResidentKeyRequirementRequired,
|
||||
// Preferred, not required, and not checked on the result. Requiring
|
||||
// it made password managers such as NordPass ask for their master
|
||||
// password on every use, which other sites avoid by preferring it.
|
||||
// The cost is deliberate: an authenticator that skips verification
|
||||
// makes a passkey possession-only, so passwordless sign-in and
|
||||
// step-up then rest on the device or vault being unlocked.
|
||||
UserVerification: protocol.VerificationPreferred,
|
||||
},
|
||||
})
|
||||
}
|
||||
@@ -182,7 +188,7 @@ func HandleMFAWebAuthnBegin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
options, sessionData, err := w.BeginLogin(waUser{handle: handle, name: t.Email, credentials: lib},
|
||||
webauthn.WithUserVerification(protocol.VerificationRequired))
|
||||
webauthn.WithUserVerification(protocol.VerificationPreferred))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not start verification"})
|
||||
return
|
||||
@@ -328,10 +334,6 @@ func HandleRegisterPasskeyFinish(c *gin.Context) {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "that passkey could not be verified"})
|
||||
return
|
||||
}
|
||||
if !cred.Flags.UserVerified {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "this passkey does not verify the user"})
|
||||
return
|
||||
}
|
||||
transports := make([]string, 0, len(parsed.Response.Transports))
|
||||
for _, t := range parsed.Response.Transports {
|
||||
transports = append(transports, string(t))
|
||||
@@ -388,7 +390,7 @@ func HandleStepUpWebAuthnBegin(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
options, sessionData, err := w.BeginLogin(waUser{handle: handle, name: sess.Email, credentials: lib},
|
||||
webauthn.WithUserVerification(protocol.VerificationRequired))
|
||||
webauthn.WithUserVerification(protocol.VerificationPreferred))
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": "could not start verification"})
|
||||
return
|
||||
@@ -435,7 +437,7 @@ func HandleStepUpWebAuthnFinish(c *gin.Context) {
|
||||
}
|
||||
|
||||
// finishAssertion is shared by second-factor sign-in, passwordless sign-in and
|
||||
// step-up, so the verification rules (user verification, clone detection,
|
||||
// step-up, so the verification rules (backup eligibility, clone detection,
|
||||
// instance scope) exist once.
|
||||
func finishAssertion(c *gin.Context, instanceID, userID, email string) (*webauthn.Credential, error) {
|
||||
var body struct {
|
||||
@@ -490,9 +492,6 @@ func finishAssertion(c *gin.Context, instanceID, userID, email string) (*webauth
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if !cred.Flags.UserVerified {
|
||||
return nil, errors.New("user verification was not performed")
|
||||
}
|
||||
// A counter that fails to advance is the library's clone signal. Zero on
|
||||
// both sides means the authenticator does not keep one, which is normal.
|
||||
if cred.Authenticator.CloneWarning {
|
||||
|
||||
Reference in New Issue
Block a user