Feature/webauthn passkeys - #5
Merged
Merged
Conversation
Unlike TOTPSecret (one per user), a user can have several passkeys — one per device/authenticator. CredentialData is the JSON-marshaled form of the go-webauthn library's own Credential struct, stored as a blob rather than decomposed into columns — that struct gains fields as the library evolves, and a blob avoids the storage layer drifting out of sync with it. CredentialID is denormalized out of that blob purely so it's indexable without deserializing every row first. Also adds webauthn_registered/webauthn_removed/webauthn_challenge_failed audit event types.
For tests and local experimentation only, matching the existing in-memory store conventions (not a supported production backend).
credential_data is passed to the driver as a string, not a raw []byte — lib/pq sends a []byte argument as bytea on the wire, which Postgres won't implicitly cast into a jsonb column; a string argument is sent as text, which jsonb's input parser reads correctly.
Unlike TOTPGenerator and Encryptor, this interface exposes go-webauthn's own types directly (SessionData, webauthn.Credential, protocol.CredentialCreation/Assertion) rather than hiding them behind primitive strings — a WebAuthn ceremony is too rich to flatten into a small custom vocabulary without reinventing a parallel API for no real benefit. The public cryden facade still deals in plain []byte JSON at its own boundary; these richer types stay internal to the engine. v2 ships one implementation, GoWebAuthnProvider, wrapping github.com/go-webauthn/webauthn — a WebAuthn ceremony has enough real attack surface (origin/RP-ID validation, attestation formats, signature-counter checks for cloned authenticators, challenge replay) that hand-rolling it would be a serious security liability, unlike TOTP where hand-rolling was a realistic option we chose not to take. Adds github.com/go-webauthn/webauthn to go.mod. Its own transitive dependencies aren't individually pinned here — run 'go mod tidy' after pulling this branch; no network access to the Go module proxy was available while authoring this change to do it here.
…cator Uses github.com/descope/virtualwebauthn to drive an actual cryptographically valid registration and login round trip through the provider — the only way to exercise CreateCredential/ValidateLogin's real success path; a hand-built fake response can only ever test rejection. Covers: registration produces a valid credential, a full login round trip succeeds and advances the signature counter (the mechanism that makes cloned-authenticator detection possible), and a garbage login response is rejected. Adds github.com/descope/virtualwebauthn to go.mod — only ever imported from _test.go files and cmd/smoketest/webauthn-passkeys, never by the engine itself.
Renamed now, before this branch and the TOTP branch it sits on are
tagged/released — no external code depends on the TOTP-only shape yet,
so there's no back-compat cost to unifying now versus carrying two
separate error types (one per second-factor method) forward.
ErrSecondFactorRequired{PendingToken, Methods []string} replaces
ErrTOTPRequired{PendingToken} — Methods lists which confirmed second
factors the account has ("totp", "webauthn", or both), so Login has
one method-agnostic pause state regardless of which method(s) an
account has enrolled, rather than a separate error per method.
Also adds ErrNoPasskeysEnrolled, ErrInvalidWebAuthnResponse, and
ErrInvalidCeremonyToken for the WebAuthn flows landing in the next
few commits.
Login takes a new webauthnStore param (nil-safe, same pattern as totpStore). After password verification, it now collects ALL confirmed second-factor methods — a confirmed TOTP secret AND/OR one or more registered passkeys — into a single Methods list, and pauses with *ErrSecondFactorRequired if that list is non-empty. An account with both enrolled reports both; the caller decides which to prompt for. Updates auth/mfa.go's doc comment and existing lockout_test.go/ login_test.go/login_totp_test.go call sites for the new param and the renamed error type.
- BeginRegisterPasskey/FinishRegisterPasskey: begin/finish ceremony for an already-authenticated user. The ceremony's challenge state (webauthn.SessionData) is JSON-encoded and encrypted with the same Encryptor used for TOTP secrets, handed back as an opaque ceremony token — no new ephemeral store needed, the engine stays fully stateless between the two calls. - ListPasskeys/DeletePasskey: DeletePasskey requires the current password as re-confirmation, same reasoning as DisableTOTP, regardless of how many other factors remain enrolled afterward. - BeginWebAuthnLogin/CompleteLoginWithWebAuthn: the passkey half of a paused login. Each call independently re-verifies pendingToken — callers must never assume ceremony state carries authentication state over implicitly. CompleteLoginWithWebAuthn persists the authenticator's updated signature counter after a successful login, which is what makes cloned-authenticator detection possible on a future login (the library rejects a non-advancing counter). webauthnUser adapts a store.User plus their stored passkeys to the go-webauthn library's own User interface; our UUIDv7 user IDs are used directly as the library's opaque user handle.
Drives BeginRegisterPasskey/FinishRegisterPasskey/BeginWebAuthnLogin/ CompleteLoginWithWebAuthn through a real simulated authenticator (virtualwebauthn) so the success paths are genuinely exercised, not just the rejection paths a fake response would be limited to. Covers: registration stores a credential with its nickname, a garbage registration response is rejected, a tampered ceremony token is rejected, delete requires the correct password, a full login completion issues tokens, a garbage login response is rejected, and an account with no registered passkeys can't begin a webauthn login.
Covers: WebAuthn-only enrollment reports Methods == ["webauthn"], TOTP + WebAuthn both enrolled report both methods, and an account with neither still issues tokens directly — confirms the unification in Login didn't change behavior for accounts using only one method, or neither.
- Config.WebAuthn (optional, store.WebAuthnCredentialStore), WebAuthnRPID/WebAuthnRPDisplayName/WebAuthnRPOrigins (all required together if WebAuthn is set). RPID is a genuine security parameter, not cosmetic — credentials are cryptographically bound to it. - New() validates the RP fields are all set whenever WebAuthn is, and constructs the pending-login issuer and encryptor if EITHER TOTP or WebAuthn is configured (shared infrastructure between both methods — one EncryptionKey, two consumers). - New facade functions: BeginRegisterPasskey, FinishRegisterPasskey, ListPasskeys, DeletePasskey, BeginWebAuthnLogin, CompleteLoginWithWebAuthn, each returning cryden.ErrWebAuthnNotConfigured if called without Config.WebAuthn set. New Passkey DTO for ListPasskeys — a storage-detail-free view (base64url credential ID, nickname, timestamps). - Login's facade signature is unchanged; it now threads e.webauthn through to auth.Login internally alongside e.totp.
Also updates the TOTP section's error-handling example for the ErrTOTPRequired -> ErrSecondFactorRequired rename.
Runnable end-to-end check with no database dependency: go run ./cmd/smoketest/webauthn-passkeys. Uses a real simulated authenticator (virtualwebauthn) to exercise actual cryptographic verification, not just rejection paths. Walks: login before registration (direct), begin/finish registration rejecting a garbage response first, listing the passkey, login after registration (paused, reports Methods == ["webauthn"]), the login ceremony's own begin/finish round trip rejecting a garbage response and a tampered ceremony token, a successful completion, a real access token rejected when used as a pending token, deleting the passkey with wrong password rejected first, and login reverting to direct afterward.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
No description provided.