Passkey (WebAuthn) support: registration, login, and management#4
Draft
lr00rl wants to merge 3 commits into
Draft
Passkey (WebAuthn) support: registration, login, and management#4lr00rl wants to merge 3 commits into
lr00rl wants to merge 3 commits into
Conversation
Introduce the persisted data layer for passkeys: a WebAuthnCredential record (id, user, credential id, public key, AAGUID, sign count, transports, backup flags, timestamps) and a short-lived, single-use, IP-bound WebAuthnChallenge that ties a ceremony's begin and finish steps together, mirroring the TOTP challenge conventions. Public keys and credential ids are not secret, so the credential map is persisted as-is (no per-record envelope). The store gains ownership-scoped CRUD with a per-user cap, credential-id lookup, sign-count/backup/last-used touch, and a sweep-on-write bounded challenge lifecycle.
Wire the WebAuthn ceremonies through go-webauthn (attestation none; no TPM/Apple attestation paths) rather than hand-rolling CBOR/COSE parsing, consistent with the repo's existing vetted auth deps. Registration (interactive) and delete require a fresh 2FA step-up grant when the account has TOTP enrolled; rename does not. Login is usernameless and discoverable: it resolves the user from the authenticator's user handle and issues the same session the password+TOTP path issues, since a user-verified passkey satisfies possession and inherence. A zero or non-incrementing sign counter is tolerated (synced passkeys report 0); only a regression from a previously non-zero counter is logged as a clone warning. The relying-party id and origin derive from the configured external URL (PublicURL) — the same value that anchors OIDC, so no new config surface is introduced. When PublicURL is unset, passkey endpoints fail closed (503), mirroring the OIDC handlers. Tests drive full begin->finish ceremonies with a virtual authenticator and cover challenge lifecycle, bad origin/RPID, challenge reuse, the sign-count warning, and step-up gating.
Describe the passkey endpoints, the library choice, the RP id/origin derivation from PublicURL (the same value that anchors OIDC — no new config field), the fail-closed behavior when it is unset, the step-up gating, the sign-count policy, and the zero-config Apple Passwords / iCloud Keychain behavior over HTTPS.
185c046 to
15df231
Compare
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.
What
Native passkey (WebAuthn) support for the Lattice control plane: operators can register passkeys (stored by Apple Passwords / iCloud Keychain or any platform/roaming authenticator) and sign in with them.
New endpoints:
POST /api/security/webauthn/register/{begin,finish}GET /api/security/webauthn/credentials,POST /api/security/webauthn/credentials/{rename,delete}POST /api/auth/webauthn/login/{begin,finish}Data layer:
auth.WebAuthnCredential(persisted plaintext — public keys/credential ids are not secret) and a short-lived, single-use, IP-boundauth.WebAuthnChallenge, both following the existing TOTP-challenge/store conventions. Per-user credential cap of 10; sweep-on-write bounded challenge set.Why / security decisions
github.com/go-webauthn/webauthn(v0.17.4). Hand-rolling CBOR/COSE/attestation parsing and assertion verification is a security anti-pattern; the repo already carries vetted third-party auth deps (go-oidc). Dependency surface is kept minimal: attestation conveyancenone, no MDS/TPM/Apple attestation verification paths.github.com/descope/virtualwebauthnis a test-only dependency used to drive real begin→finish ceremonies.PublicURL(the same canonical external-URL config that anchors OIDC):RPID = host(no scheme/port),RPOrigin = scheme://host[:port]. Forhttps://lattice.roobli.org→RPID=lattice.roobli.org, originhttps://lattice.roobli.org, zero Apple-specific config. With noPublicURL, passkey endpoints return503unless the new opt-inWebAuthnAllowHostFallbackis set (derives RP identity from the requestHost— development only; an arbitrary Host is never trusted silently). Config field chosen: reuseOptions.PublicURL; default no fallback.StepUpGrant(the existing sensitive-action verb, passed asstep_up_grant) when the account has TOTP enrolled. Rename is not gated. Delete blocks removing the last sign-in method only if the account has no password — password always exists here, so removing all passkeys is allowed.ResidentKey=required,UserVerification=required. Login begin/finish share the existing per-IP login rate limiter; challenges are ≤5 min, single-use, IP-bound.0). Only a regression from a previously non-zero counter is logged as a possible-clone warning; login is never hard-failed on the counter.Test evidence
gofmt -lclean;go vet ./...clean.go test ./internal/...— all packages pass, including new tests:internal/store: credential CRUD + per-user cap, ownership-scoped rename/delete/touch, challenge lifecycle (single-use / expiry / IP bind).internal/server: full register→login round trip (virtual authenticator), bad origin, bad RPID, challenge reuse, IP binding, sign-count regression warns-but-allows, step-up gating on register + delete, passkeys unavailable withoutPublicURL, bearer PAT rejected from management.Manual test plan (browser flows can't be E2E-verified headlessly here)
PublicURLset.Related
Dashboard PR: LatticeNet/lattice-dashboard#5
https://claude.ai/code/session_014jDcsi9xQEcy7DJQVksJac