Phase 6 WebAuthn bind, enrollment-only slice (spec section 14), web app face. An already-authenticated principal binds a passkey to their own account and manages the credentials they have bound; email-OTP stays the fallback factor. - four account routes: POST register/begin mints a credential-creation challenge, POST register/finish verifies the attestation against the server-stashed SessionData and binds the credential, GET/DELETE credentials list and unbind the caller's OWN passkeys. App-tier, principal-scoped (the body never names a user). - PasskeyVerifier seam keeps go-webauthn out of this package: ceremony state crosses as opaque bytes, attestation as an io.Reader, result as a plain VerifiedCredential. A nil verifier makes begin/finish report 503 so the authenticated boundary is exercised before the real verifier is wired in. - the view never leaks the public key; credential_id collisions map to 409. - OpenAPI: the four paths plus the PasskeyCredential schema, keeping the served-routes parity gate green. Scope: ENROLLMENT only. The passkey login/assertion path (proving a passkey from an unauthenticated state) is deferred; every ceremony here rides on a known principal. Tests: handler + challenge state machine against a fake repo and a fake verifier (no real attestation crypto, no SQL). The decisive assertion is the session-data round-trip -- the finish body carries no challenge, so the only path for the stashed blob into FinishRegistration is store-stash then consume, proving the challenge is server-held and never client-echoed. Also covers supersede-on-begin, single-use, expiry, 503-unavailable, 409-already-bound, owner-scoped list/delete, and external-only face separation.
78 KiB
78 KiB