Skip to content

feat(auth): add passkeys (WebAuthn) like monk #87

Description

@vishr

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?

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions