FRD: Auth Middleware & SSO Abstraction
| Field | Value |
|---|---|
| ID | FRD-010 |
| Owner | David Holmes |
| Status | Draft |
| Last Updated | 2026-05-26 |
| Open Source Libraries | jose, better-auth |
| Related | ADR-027 (Default Tech Stack) |
| Target Release | v2.0.0 |
| Type | Library |
| Complexity | M |
Document Summary
This FRD defines a set of middleware helpers and SSO/social-sign-in abstractions that extend the existing src/lib/auth/ library. The library already ships RS256 verification, JWKS caching, refresh-token rotation, and current-user resolution via jose. This work adds the missing integration layer: framework-agnostic middleware helpers that resolve the current user from HTTP requests, a provider-pattern SSO abstraction for social login handoff, MFA setup and recovery-code flows, password reset, email verification, and invitation acceptance.
Introduction
Every application in the platform needs to resolve the current user from an incoming request, redirect to social login providers, and handle account lifecycle flows (password reset, email verification, MFA enrollment). Today each app wires these up ad hoc, leading to inconsistent error handling, duplicated redirect logic, and gaps in MFA coverage. This library standardises that surface so apps import a single middleware chain and provider configuration instead of reimplementing auth plumbing.
Scope
In scope
- Framework-agnostic middleware helpers that extract and verify the bearer token from the
Authorizationheader and attach theAuthenticatedUserto the request context. - A
requireAuthmiddleware that returns a 401 response when no valid token is present. - An SSO/social handoff abstraction using the ADR-051 provider pattern, supporting Google, GitHub, and Microsoft as initial providers, with a clear extension point for additional providers.
- MFA TOTP enrollment, verification, and recovery-code generation/validation helpers.
- Password-reset request and completion flow helpers.
- Email-verification token issuance and confirmation helpers.
- Invitation-acceptance flow that validates an invite token and links the new user to a tenant.
- Astro middleware adapter as the first concrete integration.
Out of scope
- UI components for login, MFA enrollment, or password reset forms (these exist or will exist in the widget layer).
- Persistent user storage or database schema design.
- Rate limiting (handled at the API gateway layer).
- OAuth token storage (apps own their own token persistence).
Users and Pain Points
| User | Pain Point |
|---|---|
| App developer | Must manually wire bearer-token extraction, verification, and current-user attachment in every new API route or middleware chain. |
| App developer | Implements social login redirect/callback logic per app, with inconsistent state parameter handling and CSRF protection. |
| App developer | MFA enrollment is absent from most apps because there is no shared flow to reuse. |
| Platform maintainer | Password-reset and email-verification flows vary across apps, making security audits harder. |
| End user | Inconsistent auth UX across apps (different error messages, different redirect behavior after login). |
Definitions
| Term | Definition |
|---|---|
| AuthenticatedUser | The canonical verified-user context (src/lib/auth/auth-claims.ts) containing id, roles, tenantId, and the raw AuthClaims. |
| SSO provider | An external identity provider (Google, GitHub, Microsoft) that handles credential collection and returns an authorization code or ID token. |
| TOTP | Time-based One-Time Password, the MFA mechanism defined by RFC 6238. |
| Recovery code | A one-time-use backup code generated during MFA enrollment, used when the TOTP device is unavailable. |
| Invite token | A signed, time-limited token that authorises a new user to join a specific tenant. |
Current State
The src/lib/auth/ module provides:
verifyAccessToken()— RS256 JWT verification against a remote JWKS endpoint with caching and kid-miss refresh.JwksCache— deduplicating, TTL-aware JWKS document cache.extractBearerToken()— parses theAuthorization: Bearer <token>header.resolveCurrentUser()andresolveCurrentUserFromAuthorizationHeader()— combines extraction and verification into anAuthenticatedUser.requireRole()— coarse role gate that throwsAuthErroron mismatch.createInMemoryAuthService()— reference issuer for tests and Storybook with token minting, refresh rotation, and family revocation.AuthErrorwith stable error codes (auth.invalid_token,auth.missing_role, etc.).authClaimsSchema— Zod schema enforcing the ADR-025 claim set.
What is missing
- No
createAuthMiddleware()that composes extraction, verification, and context attachment into a single reusable middleware function. - No SSO abstraction — each app hand-rolls OAuth redirect URLs, state parameters, PKCE challenges, and callback handling.
- No MFA helpers — TOTP secret generation, QR URI construction, code verification, and recovery-code lifecycle are absent.
- No password-reset or email-verification token helpers.
- No invitation-acceptance flow.
Proposed Solution
Middleware helpers
Export a createAuthMiddleware(options) factory that returns a framework-agnostic handler. The handler:
- Calls
extractBearerToken()on the request’sAuthorizationheader. - Calls
verifyAccessToken()with the configured issuer, audience, and JWKS URI. - Attaches the resulting
AuthenticatedUserto a framework-neutral context object. - Provides a
requireAuthwrapper that short-circuits with a 401 JSON error when the user is absent.
A thin Astro adapter (createAstroAuthMiddleware) maps from Astro’s APIContext to the generic interface.
SSO abstraction
Define an SsoProvider interface following ADR-051:
interface SsoProvider { id: string; buildAuthorizationUrl(state: string, redirectUri: string, scopes?: string[]): URL; exchangeCode(code: string, redirectUri: string): Promise<SsoTokenSet>; fetchUserInfo(accessToken: string): Promise<SsoUserInfo>;}Ship concrete implementations for Google, GitHub, and Microsoft. Each provider encapsulates its OAuth discovery, PKCE challenge, token exchange, and user-info normalisation.
MFA helpers
generateTotpSecret()— returns a base32 secret and anotpauth://URI for QR rendering.verifyTotpCode(secret, code, options?)— validates a 6-digit TOTP code with configurable time window.generateRecoveryCodes(count?)— produces a set of single-use recovery codes.verifyRecoveryCode(code, storedHashes)— constant-time comparison against hashed recovery codes.
Account lifecycle helpers
createPasswordResetToken(userId, secret, ttl?)— issues an HMAC-signed, time-limited reset token.verifyPasswordResetToken(token, secret)— validates signature and expiry.createEmailVerificationToken(userId, email, secret, ttl?)/verifyEmailVerificationToken(token, secret)— same pattern for email confirmation.createInvitationToken(tenantId, email, role, secret, ttl?)/verifyInvitationToken(token, secret)— encodes tenant, email, and default role into a signed invite.
All token helpers use HMAC-SHA256 via the Web Crypto API for portability across Node, Deno, and edge runtimes.
Requirements
| ID | Priority | Requirement |
|---|---|---|
| AUTH-MW-01 | P0 | createAuthMiddleware resolves AuthenticatedUser from Authorization header and attaches it to request context. |
| AUTH-MW-02 | P0 | requireAuth returns 401 with canonical ApiError envelope when no valid token is present. |
| AUTH-MW-03 | P0 | Astro middleware adapter maps APIContext to the generic middleware interface. |
| AUTH-SSO-01 | P0 | SsoProvider interface with buildAuthorizationUrl, exchangeCode, fetchUserInfo. |
| AUTH-SSO-02 | P0 | Google, GitHub, and Microsoft concrete providers with PKCE support. |
| AUTH-SSO-03 | P1 | SSO state parameter includes CSRF nonce validated on callback. |
| AUTH-MFA-01 | P0 | TOTP secret generation and code verification with configurable time window. |
| AUTH-MFA-02 | P0 | Recovery-code generation and constant-time verification. |
| AUTH-LIFE-01 | P0 | Password-reset token issuance and verification. |
| AUTH-LIFE-02 | P0 | Email-verification token issuance and verification. |
| AUTH-LIFE-03 | P1 | Invitation token issuance and verification with tenant/role encoding. |
Functional Requirements
- Middleware chain:
createAuthMiddleware({ issuer, audience, jwksUri })returns an object withresolve(request)andrequire(request)methods.resolvereturnsAuthenticatedUser | null.requirethrows or returns a 401 response. - SSO redirect: Each
SsoProvider.buildAuthorizationUrl()must generate a PKCE code challenge and encode a CSRF nonce in thestateparameter. - SSO callback:
exchangeCode()performs the token exchange and returns{ accessToken, idToken, refreshToken?, expiresIn }.fetchUserInfo()normalises the provider’s user-info response into{ providerId, email, emailVerified, name, avatarUrl }. - MFA enrollment:
generateTotpSecret()returns{ secret, uri, algorithm, digits, period }. The URI is suitable for direct QR rendering. - MFA verification:
verifyTotpCode()accepts the current code and checks a configurable window (default: 1 step before and after) to tolerate clock drift. - Recovery codes:
generateRecoveryCodes(8)produces 8 codes. The library returns both plaintext (for one-time display) and bcrypt/SHA-256 hashes (for storage). - Token helpers: All lifecycle tokens (password reset, email verify, invitation) are HMAC-SHA256-signed JSON payloads with
iatandexpfields.verify*functions reject expired or tampered tokens with descriptiveAuthErrorcodes.
Non-Functional Requirements
| Category | Requirement |
|---|---|
| Portability | All crypto operations use the Web Crypto API so the library runs on Node 20+, Deno, Cloudflare Workers, and Vercel Edge. |
| Bundle size | The middleware and token helpers must not pull in Node-only crypto modules. jose remains the only heavy dependency. TOTP implementation uses Web Crypto directly (~200 lines, no new dependency). |
| Latency | JWKS cache ensures middleware adds less than 2ms after the initial fetch per issuer. |
| Security | Recovery-code comparison is constant-time. TOTP verification is constant-time. State parameters include a cryptographic nonce. All tokens have enforced expiry. |
| Testability | Every function accepts a now clock and a crypto override for deterministic tests. |
API/Interface Requirements
Middleware
// createAuthMiddleware returns framework-agnostic helperscreateAuthMiddleware(options: AuthMiddlewareOptions): { resolve(headers: Headers): Promise<AuthenticatedUser | null>; require(headers: Headers): Promise<AuthenticatedUser>;};
// Astro adaptercreateAstroAuthMiddleware(options: AuthMiddlewareOptions): AstroMiddleware;SSO
interface SsoProvider { id: string; buildAuthorizationUrl(state: string, redirectUri: string, scopes?: string[]): URL; exchangeCode(code: string, redirectUri: string): Promise<SsoTokenSet>; fetchUserInfo(accessToken: string): Promise<SsoUserInfo>;}
function createGoogleSsoProvider(config: GoogleSsoConfig): SsoProvider;function createGithubSsoProvider(config: GithubSsoConfig): SsoProvider;function createMicrosoftSsoProvider(config: MicrosoftSsoConfig): SsoProvider;MFA
function generateTotpSecret(options?: TotpOptions): TotpSecret;function verifyTotpCode(secret: string, code: string, options?: TotpVerifyOptions): boolean;function generateRecoveryCodes(count?: number): RecoveryCodeSet;function verifyRecoveryCode(code: string, hashes: string[]): { valid: boolean; remainingHashes: string[] };Lifecycle tokens
function createPasswordResetToken(userId: string, secret: CryptoKey, ttl?: number): Promise<string>;function verifyPasswordResetToken(token: string, secret: CryptoKey): Promise<{ userId: string }>;// Email verify and invitation follow the same patternAccessibility Requirements
This is a headless library with no UI. Accessibility requirements apply to consuming components (login forms, MFA enrollment dialogs) which are out of scope here. The library must produce error messages that are safe to surface in UI without exposing secrets or internal state.
Content and Documentation Requirements
- TSDoc on every exported function and type with
@remarkslinking to the governing ADR. - A
docs/best-practices/auth-middleware-and-sso.mdxguide covering: middleware setup for Astro, SSO provider configuration, MFA enrollment flow, password-reset integration, and invitation flow. - Storybook docs page under
Docs/Best Practices/Auth Middleware & SSOthat renders the guide.
Dependencies
| Dependency | Type | Notes |
|---|---|---|
jose | Existing | Already used for JWT verification and signing. |
src/lib/auth/ | Internal | Extends the existing auth module with new exports. |
src/lib/api-types.ts | Internal | Error responses use the canonical ApiError envelope. |
| ADR-051 | Governance | SSO providers follow the provider pattern. |
| ADR-025 | Governance | All JWT handling follows the identity standard. |
| Web Crypto API | Runtime | Required for HMAC token signing and TOTP. |
Risks and Tradeoffs
| Risk | Likelihood | Impact | Mitigation |
|---|---|---|---|
| TOTP implementation without a battle-tested library may have timing bugs. | Medium | High | Use constant-time comparison from Web Crypto. Write property-based tests covering clock drift and boundary conditions. |
| SSO providers change their OAuth endpoints or scopes. | Low | Medium | Isolate provider config behind the SsoProvider interface so changes are localised to one file per provider. |
Edge-runtime limitations on Web Crypto (e.g., missing subtle.importKey for HMAC). | Low | Medium | CI runs tests on Node 20, Deno, and Cloudflare Workers miniflare. |
| Middleware abstraction may not fit all frameworks cleanly. | Medium | Low | Keep the generic layer as a plain function that takes Headers; framework adapters are thin wrappers. |
Open Questions
- Should the SSO abstraction handle PKCE code-verifier storage, or should that be the caller’s responsibility? Leaning toward the library generating the verifier and returning it alongside the URL, with the caller storing it in a session cookie.
- Should recovery codes be hashed with bcrypt or SHA-256? Bcrypt is more resistant to brute-force but pulls in a Node-specific dependency. SHA-256 via Web Crypto keeps the library edge-compatible.
- Do we need a
createExpressAuthMiddlewareadapter in v2.0.0, or is Astro-only sufficient for launch?
Acceptance Criteria
-
createAuthMiddlewareresolvesAuthenticatedUserfrom a validAuthorization: Bearer <token>header. -
requireAuthreturns a 401 response with the canonicalApiErrorenvelope when the token is missing or invalid. -
createAstroAuthMiddlewareintegrates with Astro’sdefineMiddlewareand attaches the user tolocals. - Google, GitHub, and Microsoft SSO providers implement the
SsoProviderinterface with PKCE and CSRF nonce. -
generateTotpSecretproduces a validotpauth://URI that works with Google Authenticator. -
verifyTotpCodeaccepts codes within the configured time window and rejects codes outside it. -
generateRecoveryCodesproduces unique codes;verifyRecoveryCodevalidates and marks codes as used. - Password-reset, email-verification, and invitation token round-trip (issue then verify) succeeds within TTL and fails after expiry.
- All token verification functions throw
AuthErrorwith descriptive codes on tampered or expired input. - Unit tests cover happy path, expired tokens, tampered tokens, unknown SSO providers, and clock-drift TOTP edge cases.
- All exports have TSDoc with
@remarkslinking to ADR-025 or ADR-051.
LLM Handoff Instructions
When implementing this FRD:
- Start in
src/lib/auth/. Add new files alongside the existing module:middleware.ts,sso/sso-provider.ts,sso/google.ts,sso/github.ts,sso/microsoft.ts,mfa/totp.ts,mfa/recovery-codes.ts,tokens/password-reset.ts,tokens/email-verification.ts,tokens/invitation.ts. - Re-export everything from
src/lib/auth/index.ts. - Follow the existing code style: Zod schemas for all structured data,
AuthErrorfor all failure paths,@remarks Governed by ADR-025on auth functions. - The
createAuthMiddlewaregeneric layer takesHeadersand returnsAuthenticatedUser | null. The Astro adapter lives insrc/lib/auth/adapters/astro.ts. - For TOTP, implement HMAC-SHA1 via Web Crypto (RFC 6238 requires SHA1 for Google Authenticator compatibility). Do not add a dependency for this.
- For recovery codes, use SHA-256 hashing via Web Crypto to stay edge-compatible. Return both plaintext and hashes from
generateRecoveryCodes. - All token helpers (
password-reset,email-verification,invitation) follow the same pattern: HMAC-SHA256-signed JSON withiat/exp. Extract the shared signing logic intosrc/lib/auth/tokens/signed-token.ts. - Tests go in
src/lib/auth/*.test.tsmirroring the existingauth.test.tspattern. Use the existingcreateInMemoryAuthServiceandgenerateAuthSigningKeyfor test token minting. - Run
pnpm typecheckandpnpm vitest run --project unitbefore declaring complete.
Decision Log
| Date | Decision | Rationale |
|---|---|---|
| 2026-05-26 | Use Web Crypto API exclusively (no Node crypto module). | Ensures the library runs on edge runtimes without polyfills. |
| 2026-05-26 | TOTP uses HMAC-SHA1 per RFC 6238. | Google Authenticator and most authenticator apps require SHA1 for interoperability. |
| 2026-05-26 | SSO provider interface follows ADR-051. | Consistent with the existing provider pattern used elsewhere in the design system. |
| 2026-05-26 | Recovery codes hashed with SHA-256, not bcrypt. | Edge-runtime compatibility outweighs brute-force resistance for short-lived backup codes. |
Document History
| Version | Date | Author | Changes |
|---|---|---|---|
| 0.1 | 2026-05-26 | David Holmes | Initial draft. |