Skip to content

feat: Implement WebAuthn/Passkey HTTP API endpoints #1236

@theothersideofgod

Description

@theothersideofgod

Background

Supabase announced passkey authentication on 2026-05-28 (beta). After researching their implementation and auditing our codebase, Constructive already has a more complete WebAuthn foundation — we just need to wire up the HTTP layer.

Current State

✅ Database Layer (Ready)

Schema (constructive-db):

  • webauthn_credentials table with full spec support:
    • credential_id (Base64url, globally unique)
    • public_key (COSE-encoded)
    • sign_count (clone detection)
    • webauthn_user_id (privacy-preserving handle)
    • transports[] (usb, nfc, ble, internal, hybrid)
    • credential_device_type (singleDevice/multiDevice)
    • backup_eligible, backup_state (sync passkey tracking)
    • name (user label)
    • last_used_at

Configuration (webauthn_auth_module):

  • rp_id, rp_name
  • origin_allowlist[] (unlimited, vs Supabase's 5-origin limit)
  • attestation_type
  • require_user_verification
  • resident_key
  • challenge_expiry

✅ Stored Procedures (Ready)

Located in constructive-db/packages/ast-plpgsql/deploy/schemas/ast_plpgsql_helpers/procedures/webauthn_auth/:

Procedure Signature Purpose
webauthn_begin_registration (p_user_id uuid) → jsonb Generate registration challenge, store in session_secrets
webauthn_finish_registration (p_credential_id text, p_public_key bytea, p_sign_count bigint, p_transports text[], p_credential_device_type text, p_backup_eligible boolean, p_backup_state boolean, p_webauthn_user_id text, p_user_id uuid, p_name text) → uuid Verify challenge consumed, store credential
webauthn_begin_sign_in (p_user_id uuid DEFAULT NULL) → jsonb Generate auth challenge (supports usernameless/discoverable)
webauthn_finish_sign_in (p_credential_id text, p_new_sign_count bigint, p_new_backup_state boolean, credential_kind text) → (user_id uuid, access_token text, access_token_expires_at timestamptz) Verify assertion, update signCount, mint session token

Key implementation details:

  • Challenge stored in session_secrets table, scoped to jwt_private.current_session_id()
  • Challenge consumed atomically via DELETE ... RETURNING
  • All procedures are SECURITY DEFINER in private schema (not exposed to authenticated role)
  • finish_sign_in returns same signature as sign_in_identity/sign_in_magic_link for consistent downstream handling

✅ ORM Models (Ready)

  • WebauthnCredentialModel - full CRUD
  • WebauthnAuthModuleModel - config management
  • WebauthnSettingModel - per-schema settings

⚠️ GraphQL Server (Partial)

  • webauthnSettings loaded into API context ✅
  • Missing: HTTP routes to orchestrate the WebAuthn ceremony

What's Missing

1. HTTP API Endpoints

POST /auth/webauthn/register/begin
POST /auth/webauthn/register/finish
POST /auth/webauthn/sign-in/begin
POST /auth/webauthn/sign-in/finish

2. WebAuthn Verification Layer

Need @simplewebauthn/server to:

  • Generate registration/authentication options (using challenge from DB procedure)
  • Verify attestation responses (register) — extract credentialId, publicKey, signCount, etc.
  • Verify assertion responses (sign-in) — validate signature, extract new signCount

Flow:

Client                    Server                         Database
  │                         │                              │
  │ POST /register/begin    │                              │
  │────────────────────────>│ call webauthn_begin_registration(user_id)
  │                         │─────────────────────────────>│
  │                         │<─────────────────────────────│ {challenge, user_handle, ...}
  │                         │ generateRegistrationOptions()│
  │<────────────────────────│ {publicKey options}          │
  │                         │                              │
  │ navigator.credentials   │                              │
  │   .create(options)      │                              │
  │                         │                              │
  │ POST /register/finish   │                              │
  │────────────────────────>│ verifyRegistrationResponse() │
  │                         │ call webauthn_finish_registration(...)
  │                         │─────────────────────────────>│
  │                         │<─────────────────────────────│ credential_id
  │<────────────────────────│ {success: true}              │

3. Frontend SDK

Wrapper around navigator.credentials.create/get + API calls:

auth.passkey.register()
auth.passkey.signIn()
auth.passkey.list()
auth.passkey.delete(id)
auth.passkey.rename(id, name)

Implementation Plan

Phase 1: HTTP Routes (~1 day)

  • Add @simplewebauthn/server dependency to graphql/server
  • Create middleware/webauthn.ts with 4 endpoints
  • Wire up to existing stored procedures via webauthnSettings context

Phase 2: Frontend SDK (~1 day)

  • Add @simplewebauthn/browser dependency
  • Add passkey methods to constructive-sdk
  • Handle browser API + server calls

Phase 3: Dashboard UI (optional, ~1 day)

  • Passkey enable/disable toggle
  • RP ID / RP Name configuration
  • User passkey list management

Comparison with Supabase

Feature Supabase Beta Constructive
Credential storage
Multi-device sync tracking ?
Backup state tracking ?
Clone detection (signCount) ?
Transport hints ?
Origin allowlist ≤5 unlimited
Attestation config ?
User verification toggle ?
Resident key config ?
Challenge expiry config ? ✅ (300s default)
HTTP API needed
Frontend SDK needed

File Locations

Database:

  • Schema: constructive-db/services/constructive-services/deploy/migrate/webauthn_*.sql
  • Procedures: constructive-db/packages/ast-plpgsql/deploy/schemas/ast_plpgsql_helpers/procedures/webauthn_auth/
  • ORM types: constructive-db/sdk/constructive-sdk/src/orm/input-types.ts
  • ORM models: constructive-db/sdk/constructive-sdk/src/orm/models/webauthn*.ts

Server:

  • API context: constructive/graphql/server/src/middleware/api.ts (已有 webauthnSettings)
  • New routes: constructive/graphql/server/src/middleware/webauthn.ts (待建)

References

Metadata

Metadata

Labels

enhancementNew feature or request

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions