Goal
Bring Fanout to parity with monk's auth stack: offer passkeys (WebAuthn / FIDO2) as a primary login method alongside the existing email-code flow.
Reference implementation: monk/server/src/monk/auth/webauthn.py + @simplewebauthn/browser on the client. ~440 lines of auth code total.
Why
- Phishing-resistant by design (origin-bound credentials)
- No password / OTP to type or intercept — users tap Touch ID / Windows Hello / security key
- Platform support is now universal (Safari 16+, Chrome, Firefox, Edge, iOS 16+, Android)
- Monk already ships it; feature parity is the bar
Proposed scope
Go side (est. ~400 LOC)
- Library:
github.com/go-webauthn/webauthn (FIDO2-compliant, maintained)
- New SQLite tables (Atlas migration):
webauthn_credentials(id, user_id, credential_id, public_key, sign_count, aaguid, transports, created_at, last_used_at, name)
webauthn_challenges(id, user_id, challenge, purpose, expires_at) — ephemeral, 5 min TTL
- New package:
internal/auth/webauthn.go — helpers for registration + authentication ceremonies
- New API endpoints in
internal/api/auth.go:
POST /api/auth/passkey/register/options (admin-authenticated — starts registration)
POST /api/auth/passkey/register/verify (completes registration, persists credential)
POST /api/auth/passkey/login/options (anonymous — returns challenge for existing user)
POST /api/auth/passkey/login/verify (completes authentication, issues JWTs)
GET /api/auth/passkeys (list registered passkeys for current user)
DELETE /api/auth/passkeys/:id (revoke a passkey)
Config (env vars)
WEBAUTHN_RP_ID — Relying Party ID, e.g. fanout.labstack.com
WEBAUTHN_RP_NAME — display name, e.g. Fanout
- Origins derived from
HTTP_ADDR + WEBAUTHN_RP_ID
Client side (est. ~200 LOC)
- Dep:
@simplewebauthn/browser (already used by monk)
- LoginPage: "Sign in with a passkey" button alongside the email input. If
localStorage has a "prefer passkey" flag from a prior login, auto-launch the ceremony.
- SettingsPage: new "Passkeys" section — list existing credentials, allow rename + revoke, "Add a passkey" button that triggers the registration ceremony.
Tests
- Unit tests for the registration/authentication helpers
- API tests for the four ceremony endpoints (challenge issuance, verification, replay rejection, counter-rollback detection)
Non-goals
- Cross-device sign-in — out of scope for v1; default to platform authenticators
- Passwordless-only mode (forcing passkey + disabling email codes) — keep both paths available
- Account recovery via passkey — email codes remain the recovery path
Monk reference files (for implementation guidance)
monk/server/src/monk/auth/webauthn.py — ceremony helpers (89 lines)
monk/server/src/monk/auth/models.py — credential model (50 lines)
monk/server/src/monk/auth/dependencies.py — FastAPI auth deps (146 lines)
monk/web/src/... — client-side registration + login components
Open questions
- Should passkey be required for new admin accounts (strict) or opt-in after email setup (flexible)?
- Do we allow multiple passkeys per user from day one? (Monk does; recommended.)
- Rename / revoke UX — simple list + buttons, or dedicated manage-passkeys page?
Goal
Bring Fanout to parity with monk's auth stack: offer passkeys (WebAuthn / FIDO2) as a primary login method alongside the existing email-code flow.
Reference implementation:
monk/server/src/monk/auth/webauthn.py+@simplewebauthn/browseron the client. ~440 lines of auth code total.Why
Proposed scope
Go side (est. ~400 LOC)
github.com/go-webauthn/webauthn(FIDO2-compliant, maintained)webauthn_credentials(id, user_id, credential_id, public_key, sign_count, aaguid, transports, created_at, last_used_at, name)webauthn_challenges(id, user_id, challenge, purpose, expires_at)— ephemeral, 5 min TTLinternal/auth/webauthn.go— helpers for registration + authentication ceremoniesinternal/api/auth.go:POST /api/auth/passkey/register/options(admin-authenticated — starts registration)POST /api/auth/passkey/register/verify(completes registration, persists credential)POST /api/auth/passkey/login/options(anonymous — returns challenge for existing user)POST /api/auth/passkey/login/verify(completes authentication, issues JWTs)GET /api/auth/passkeys(list registered passkeys for current user)DELETE /api/auth/passkeys/:id(revoke a passkey)Config (env vars)
WEBAUTHN_RP_ID— Relying Party ID, e.g.fanout.labstack.comWEBAUTHN_RP_NAME— display name, e.g.FanoutHTTP_ADDR+WEBAUTHN_RP_IDClient side (est. ~200 LOC)
@simplewebauthn/browser(already used by monk)localStoragehas a "prefer passkey" flag from a prior login, auto-launch the ceremony.Tests
Non-goals
Monk reference files (for implementation guidance)
monk/server/src/monk/auth/webauthn.py— ceremony helpers (89 lines)monk/server/src/monk/auth/models.py— credential model (50 lines)monk/server/src/monk/auth/dependencies.py— FastAPI auth deps (146 lines)monk/web/src/...— client-side registration + login componentsOpen questions