Some checks failed
CI / Run tests and lints (push) Failing after 29s
CI / Build CLI docs (push) Has been skipped
CI / Run Playwright end-to-end tests (mysql) (push) Failing after 3s
CI / Run Playwright end-to-end tests (postgres) (push) Failing after 2s
CI / Run Playwright end-to-end tests (sqlite) (push) Failing after 3s
CodeQL / Analyze (go) (push) Failing after 4s
CodeQL / Analyze (javascript) (push) Failing after 3s
Docker Image Scanners / scanners (push) Failing after 4s
Synchronize Issue Labels / Synchronize Issue Labels (push) Has been skipped
CI / Run end-to-end tests (postgres) (push) Failing after 19s
CI / Run end-to-end tests (sqlite) (push) Failing after 2s
CI / Run Playwright end-to-end tests (cockroach) (push) Failing after 2s
CI / Draft newsletter (push) Has been skipped
CI / Generate release (push) Has been skipped
CI / Pending approval Slack notification (push) Has been skipped
CI / Send newsletter (push) Has been skipped
67 lines
2.7 KiB
Go
67 lines
2.7 KiB
Go
// Copyright © 2025 Ory Corp
|
|
// SPDX-License-Identifier: Apache-2.0
|
|
|
|
package webauthnx
|
|
|
|
import (
|
|
"context"
|
|
|
|
"github.com/go-webauthn/webauthn/webauthn"
|
|
|
|
"github.com/ory/kratos/identity"
|
|
"github.com/ory/x/logrusx"
|
|
)
|
|
|
|
// SignCountDependencies is the minimal set of registry dependencies required to
|
|
// persist an updated WebAuthn signature counter after a login.
|
|
type SignCountDependencies interface {
|
|
logrusx.Provider
|
|
identity.PrivilegedPoolProvider
|
|
}
|
|
|
|
// UpdateSignCountAfterLogin writes the mutable authenticator state reported
|
|
// during a successful login back onto the stored credential: the signature
|
|
// counter, the clone warning, and the backup state.
|
|
//
|
|
// It implements the relying-party side of W3C WebAuthn signature-counter
|
|
// handling (https://www.w3.org/TR/webauthn-2/#sctn-sign-counter). When the
|
|
// go-webauthn library detects that the counter did not advance it sets
|
|
// CloneWarning, which we persist on the credential but deliberately do NOT act
|
|
// on to block the login:
|
|
//
|
|
// - Synced passkeys (Apple iCloud, Google) legitimately always report a
|
|
// counter of 0, so a hard "reject if not increasing" rule would lock out
|
|
// real users.
|
|
// - The user already proved possession of the authenticator, so a failure to
|
|
// persist the counter must not turn a successful login into an error.
|
|
//
|
|
// The clone warning is durably stored on the credential and is inspectable via
|
|
// the admin identity API, so there is no separate security log event.
|
|
//
|
|
// The write goes through UpdateCredentialsConfig, which re-reads the stored
|
|
// config under an exclusive row lock and applies UpdateFromLogin to the latest
|
|
// committed value, so concurrent logins of the same credential serialize
|
|
// instead of clobbering each other's counter. That lock lives in its own
|
|
// transaction and is deliberately NOT enrolled in the login request: the
|
|
// assertion has already succeeded, so a failed or slow counter write must never
|
|
// roll back or block the login. Persistence is therefore best-effort — errors
|
|
// are logged, not returned.
|
|
func UpdateSignCountAfterLogin(
|
|
ctx context.Context,
|
|
d SignCountDependencies,
|
|
i *identity.Identity,
|
|
credentialsType identity.CredentialsType,
|
|
validated *webauthn.Credential,
|
|
) {
|
|
err := d.PrivilegedIdentityPool().UpdateCredentialsConfig(ctx, i.ID, credentialsType,
|
|
identity.UpdateConfig(func(conf *identity.CredentialsWebAuthnConfig) error {
|
|
conf.Credentials.UpdateFromLogin(validated)
|
|
return nil
|
|
}),
|
|
)
|
|
if err != nil {
|
|
d.Logger().WithError(err).WithField("identity_id", i.ID).WithField("network_id", i.NID).
|
|
Warn("Unable to persist updated WebAuthn signature counter. The login succeeded but clone detection state was not persisted.")
|
|
}
|
|
}
|