fix(auth): log why a passkey ceremony was refused
Every WebAuthn refusal answered "that passkey could not be verified" and discarded the library's error, leaving a misconfigured relying party undiagnosable. Log the stage, the derived RP ID and expected origin, the request's Origin and X-Forwarded-Proto, and the library error with its DevInfo. None of it is secret.
This commit is contained in:
@@ -212,6 +212,7 @@ func HandleEnrolPasskeyFinish(c *gin.Context) {
|
||||
}
|
||||
cred, err := w.CreateCredential(waUser{handle: handle, name: t.Email}, *sessionData, parsed)
|
||||
if err != nil {
|
||||
logWebAuthnFailure(c, "enrolment registration", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "that passkey could not be verified"})
|
||||
return
|
||||
}
|
||||
|
||||
@@ -85,6 +85,7 @@ func HandlePasskeyLoginFinish(c *gin.Context) {
|
||||
// that owner's.
|
||||
cred, err := finishAssertion(c, instanceID, "", "")
|
||||
if err != nil {
|
||||
logWebAuthnFailure(c, "passwordless assertion", err)
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "that passkey could not be verified", "code": "invalid_assertion",
|
||||
})
|
||||
|
||||
@@ -6,6 +6,7 @@ import (
|
||||
"crypto/subtle"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
@@ -42,6 +43,23 @@ func rpConfig(c *gin.Context) (string, string) {
|
||||
return rpID, scheme + "://" + host
|
||||
}
|
||||
|
||||
// logWebAuthnFailure records why a ceremony was refused. The response stays
|
||||
// deliberately vague, so without this the only evidence of a misconfigured
|
||||
// relying party - most often a proxy that terminates TLS without passing
|
||||
// X-Forwarded-Proto: https - is a user reporting that their passkey "could not
|
||||
// be verified". Nothing logged here is secret: RP ID, origins, and the
|
||||
// library's error, whose DevInfo names expected and received values.
|
||||
func logWebAuthnFailure(c *gin.Context, stage string, err error) {
|
||||
rpID, origin := rpConfig(c)
|
||||
detail := err.Error()
|
||||
var perr *protocol.Error
|
||||
if errors.As(err, &perr) && perr.DevInfo != "" {
|
||||
detail += " (" + perr.DevInfo + ")"
|
||||
}
|
||||
log.Printf("webauthn: %s refused: rp_id=%q expected_origin=%q request_origin=%q x_forwarded_proto=%q: %s",
|
||||
stage, rpID, origin, c.GetHeader("Origin"), c.GetHeader("X-Forwarded-Proto"), detail)
|
||||
}
|
||||
|
||||
func webAuthnFor(c *gin.Context) (*webauthn.WebAuthn, error) {
|
||||
rpID, origin := rpConfig(c)
|
||||
return webauthn.New(&webauthn.Config{
|
||||
@@ -176,6 +194,7 @@ func HandleMFAWebAuthnFinish(c *gin.Context) {
|
||||
}
|
||||
cred, err := finishAssertion(c, t.InstanceID, t.UserID, t.Email)
|
||||
if err != nil {
|
||||
logWebAuthnFailure(c, "second-factor assertion", err)
|
||||
left, ferr := FailTicket(c.Request.Context(), ticketID)
|
||||
services.LogEvent(t.InstanceID, "mfa.failed", t.Email, "", "", "factor=webauthn")
|
||||
if ferr != nil || left == 0 {
|
||||
@@ -287,6 +306,7 @@ func HandleRegisterPasskeyFinish(c *gin.Context) {
|
||||
}
|
||||
cred, err := w.CreateCredential(waUser{handle: handle, name: sess.Email}, *sessionData, parsed)
|
||||
if err != nil {
|
||||
logWebAuthnFailure(c, "registration", err)
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "that passkey could not be verified"})
|
||||
return
|
||||
}
|
||||
@@ -382,6 +402,7 @@ func HandleStepUpWebAuthnFinish(c *gin.Context) {
|
||||
sess := GetSessionFromContext(c)
|
||||
cred, err := finishAssertion(c, sess.InstanceID, sess.UserID, sess.Email)
|
||||
if err != nil {
|
||||
logWebAuthnFailure(c, "step-up assertion", err)
|
||||
services.LogEvent(sess.InstanceID, "step_up.failed", sess.Email, "", "", "factor=webauthn")
|
||||
c.JSON(http.StatusUnauthorized, gin.H{"error": "that passkey could not be verified", "code": "invalid_assertion"})
|
||||
return
|
||||
|
||||
Reference in New Issue
Block a user