fix: login with Yubikey not working in some Safari browsers

This commit is contained in:
Elias Schneider
2026-09-20 14:02:08 +02:00
parent 79a0fca12b
commit 4858db3bb4
3 changed files with 93 additions and 0 deletions
@@ -194,6 +194,22 @@ func writeErrorResponse(c *gin.Context, classified classifiedError, requestID st
func logRequestError(c *gin.Context, err error, classified classifiedError, requestID string) {
if classified.status < http.StatusInternalServerError {
cause := errors.Unwrap(err)
if cause == nil {
return
}
slog.DebugContext(c.Request.Context(), "Request rejected",
slog.String("error_code", string(classified.code)),
slog.String("error_type", errorTypeName(err)),
slog.String("cause_type", errorTypeName(cause)),
slog.Int("http_status", classified.status),
slog.String("request_id", requestID),
slog.String("http_method", c.Request.Method),
slog.String("http_path", c.Request.URL.Path),
slog.Any("error", err),
slog.Any("cause", cause),
)
return
}
+17
View File
@@ -281,6 +281,7 @@ func (s *Service) VerifyLogin(ctx context.Context, dbConfig *appconfig.AppConfig
Extensions: storedSession.Extensions,
CredParams: storedSession.CredentialParams,
}
discardUnrequestedFalseAppIDOutput(session.Extensions, credentialAssertionData)
var user *model.User
_, err := s.webAuthn.ValidateDiscoverableLogin(func(_, userHandle []byte) (gowebauthn.User, error) {
@@ -535,6 +536,7 @@ func (s *Service) CreateReauthenticationTokenWithWebauthn(ctx context.Context, s
Extensions: storedSession.Extensions,
CredParams: storedSession.CredentialParams,
}
discardUnrequestedFalseAppIDOutput(session.Extensions, credentialAssertionData)
// Validate the credential assertion
var user *model.User
@@ -589,6 +591,21 @@ func classifyPasskeyError(err error, fallback func(error) *apperror.Error) *appe
return fallback(err)
}
func discardUnrequestedFalseAppIDOutput(session protocol.SessionExtensions, credential *protocol.ParsedCredentialAssertionData) {
if credential == nil || credential.ClientExtensionResults.AppID == nil || *credential.ClientExtensionResults.AppID {
return
}
for _, requested := range session.Requested {
if requested == protocol.ExtensionAppID {
return
}
}
// Safari reports appid=false for security keys even when the relying party did not request the legacy extension
credential.ClientExtensionResults.AppID = nil
}
func (s *Service) ConsumeReauthenticationToken(ctx context.Context, tx *gorm.DB, token string, userID string) (time.Time, error) {
hashedToken := utils.CreateSha256Hash(token)
var reauthToken ReauthenticationToken
+60
View File
@@ -263,6 +263,66 @@ func TestClassifyPasskeyErrorPreservesStructuredLookupFailure(t *testing.T) {
require.ErrorIs(t, err, cause)
}
func TestDiscardUnrequestedFalseAppIDOutput(t *testing.T) {
tests := []struct {
name string
requested []string
appID bool
extra map[string]any
wantAppID *bool
wantError string
}{
{
name: "unrequested false appid is discarded",
appID: false,
wantAppID: nil,
},
{
name: "requested false appid is preserved",
requested: []string{protocol.ExtensionAppID},
appID: false,
wantAppID: new(false),
},
{
name: "unrequested true appid is rejected",
appID: true,
wantAppID: new(true),
wantError: "appid",
},
{
name: "other unsolicited output is rejected",
appID: false,
extra: map[string]any{"example": true},
wantAppID: nil,
wantError: "example",
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
credential := &protocol.ParsedCredentialAssertionData{
ParsedPublicKeyCredential: protocol.ParsedPublicKeyCredential{
ClientExtensionResults: protocol.AuthenticationExtensionsClientOutputs{
AppID: new(tc.appID),
Extra: tc.extra,
},
},
}
session := protocol.SessionExtensions{Requested: tc.requested}
discardUnrequestedFalseAppIDOutput(session, credential)
assert.Equal(t, tc.wantAppID, credential.ClientExtensionResults.AppID)
err := credential.ClientExtensionResults.Verify(session, protocol.AssertCeremony, protocol.UnsolicitedOutputPolicyReject)
if tc.wantError == "" {
require.NoError(t, err)
} else {
require.ErrorContains(t, err, tc.wantError)
}
})
}
}
func TestWebAuthnManagementOperationsReturnSpecificNotFoundErrors(t *testing.T) {
service, err := newService(Dependencies{
DB: testutils.NewDatabaseForTest(t),