Skip to content

FRD: Auth Middleware & SSO Abstraction

FieldValue
IDFRD-010
OwnerDavid Holmes
StatusDraft
Last Updated2026-05-26
Open Source Librariesjose, better-auth
RelatedADR-027 (Default Tech Stack)
Target Releasev2.0.0
TypeLibrary
ComplexityM

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 Authorization header and attach the AuthenticatedUser to the request context.
  • A requireAuth middleware 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

UserPain Point
App developerMust manually wire bearer-token extraction, verification, and current-user attachment in every new API route or middleware chain.
App developerImplements social login redirect/callback logic per app, with inconsistent state parameter handling and CSRF protection.
App developerMFA enrollment is absent from most apps because there is no shared flow to reuse.
Platform maintainerPassword-reset and email-verification flows vary across apps, making security audits harder.
End userInconsistent auth UX across apps (different error messages, different redirect behavior after login).

Definitions

TermDefinition
AuthenticatedUserThe canonical verified-user context (src/lib/auth/auth-claims.ts) containing id, roles, tenantId, and the raw AuthClaims.
SSO providerAn external identity provider (Google, GitHub, Microsoft) that handles credential collection and returns an authorization code or ID token.
TOTPTime-based One-Time Password, the MFA mechanism defined by RFC 6238.
Recovery codeA one-time-use backup code generated during MFA enrollment, used when the TOTP device is unavailable.
Invite tokenA 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 the Authorization: Bearer <token> header.
  • resolveCurrentUser() and resolveCurrentUserFromAuthorizationHeader() — combines extraction and verification into an AuthenticatedUser.
  • requireRole() — coarse role gate that throws AuthError on mismatch.
  • createInMemoryAuthService() — reference issuer for tests and Storybook with token minting, refresh rotation, and family revocation.
  • AuthError with 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:

  1. Calls extractBearerToken() on the request’s Authorization header.
  2. Calls verifyAccessToken() with the configured issuer, audience, and JWKS URI.
  3. Attaches the resulting AuthenticatedUser to a framework-neutral context object.
  4. Provides a requireAuth wrapper 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 an otpauth:// 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

IDPriorityRequirement
AUTH-MW-01P0createAuthMiddleware resolves AuthenticatedUser from Authorization header and attaches it to request context.
AUTH-MW-02P0requireAuth returns 401 with canonical ApiError envelope when no valid token is present.
AUTH-MW-03P0Astro middleware adapter maps APIContext to the generic middleware interface.
AUTH-SSO-01P0SsoProvider interface with buildAuthorizationUrl, exchangeCode, fetchUserInfo.
AUTH-SSO-02P0Google, GitHub, and Microsoft concrete providers with PKCE support.
AUTH-SSO-03P1SSO state parameter includes CSRF nonce validated on callback.
AUTH-MFA-01P0TOTP secret generation and code verification with configurable time window.
AUTH-MFA-02P0Recovery-code generation and constant-time verification.
AUTH-LIFE-01P0Password-reset token issuance and verification.
AUTH-LIFE-02P0Email-verification token issuance and verification.
AUTH-LIFE-03P1Invitation token issuance and verification with tenant/role encoding.

Functional Requirements

  1. Middleware chain: createAuthMiddleware({ issuer, audience, jwksUri }) returns an object with resolve(request) and require(request) methods. resolve returns AuthenticatedUser | null. require throws or returns a 401 response.
  2. SSO redirect: Each SsoProvider.buildAuthorizationUrl() must generate a PKCE code challenge and encode a CSRF nonce in the state parameter.
  3. 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 }.
  4. MFA enrollment: generateTotpSecret() returns { secret, uri, algorithm, digits, period }. The URI is suitable for direct QR rendering.
  5. MFA verification: verifyTotpCode() accepts the current code and checks a configurable window (default: 1 step before and after) to tolerate clock drift.
  6. Recovery codes: generateRecoveryCodes(8) produces 8 codes. The library returns both plaintext (for one-time display) and bcrypt/SHA-256 hashes (for storage).
  7. Token helpers: All lifecycle tokens (password reset, email verify, invitation) are HMAC-SHA256-signed JSON payloads with iat and exp fields. verify* functions reject expired or tampered tokens with descriptive AuthError codes.

Non-Functional Requirements

CategoryRequirement
PortabilityAll crypto operations use the Web Crypto API so the library runs on Node 20+, Deno, Cloudflare Workers, and Vercel Edge.
Bundle sizeThe 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).
LatencyJWKS cache ensures middleware adds less than 2ms after the initial fetch per issuer.
SecurityRecovery-code comparison is constant-time. TOTP verification is constant-time. State parameters include a cryptographic nonce. All tokens have enforced expiry.
TestabilityEvery function accepts a now clock and a crypto override for deterministic tests.

API/Interface Requirements

Middleware

// createAuthMiddleware returns framework-agnostic helpers
createAuthMiddleware(options: AuthMiddlewareOptions): {
resolve(headers: Headers): Promise<AuthenticatedUser | null>;
require(headers: Headers): Promise<AuthenticatedUser>;
};
// Astro adapter
createAstroAuthMiddleware(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 pattern

Accessibility 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 @remarks linking to the governing ADR.
  • A docs/best-practices/auth-middleware-and-sso.mdx guide 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 & SSO that renders the guide.

Dependencies

DependencyTypeNotes
joseExistingAlready used for JWT verification and signing.
src/lib/auth/InternalExtends the existing auth module with new exports.
src/lib/api-types.tsInternalError responses use the canonical ApiError envelope.
ADR-051GovernanceSSO providers follow the provider pattern.
ADR-025GovernanceAll JWT handling follows the identity standard.
Web Crypto APIRuntimeRequired for HMAC token signing and TOTP.

Risks and Tradeoffs

RiskLikelihoodImpactMitigation
TOTP implementation without a battle-tested library may have timing bugs.MediumHighUse constant-time comparison from Web Crypto. Write property-based tests covering clock drift and boundary conditions.
SSO providers change their OAuth endpoints or scopes.LowMediumIsolate 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).LowMediumCI runs tests on Node 20, Deno, and Cloudflare Workers miniflare.
Middleware abstraction may not fit all frameworks cleanly.MediumLowKeep the generic layer as a plain function that takes Headers; framework adapters are thin wrappers.

Open Questions

  1. 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.
  2. 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.
  3. Do we need a createExpressAuthMiddleware adapter in v2.0.0, or is Astro-only sufficient for launch?

Acceptance Criteria

  • createAuthMiddleware resolves AuthenticatedUser from a valid Authorization: Bearer &lt;token&gt; header.
  • requireAuth returns a 401 response with the canonical ApiError envelope when the token is missing or invalid.
  • createAstroAuthMiddleware integrates with Astro’s defineMiddleware and attaches the user to locals.
  • Google, GitHub, and Microsoft SSO providers implement the SsoProvider interface with PKCE and CSRF nonce.
  • generateTotpSecret produces a valid otpauth:// URI that works with Google Authenticator.
  • verifyTotpCode accepts codes within the configured time window and rejects codes outside it.
  • generateRecoveryCodes produces unique codes; verifyRecoveryCode validates 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 AuthError with 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 @remarks linking to ADR-025 or ADR-051.

LLM Handoff Instructions

When implementing this FRD:

  1. 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.
  2. Re-export everything from src/lib/auth/index.ts.
  3. Follow the existing code style: Zod schemas for all structured data, AuthError for all failure paths, @remarks Governed by ADR-025 on auth functions.
  4. The createAuthMiddleware generic layer takes Headers and returns AuthenticatedUser | null. The Astro adapter lives in src/lib/auth/adapters/astro.ts.
  5. For TOTP, implement HMAC-SHA1 via Web Crypto (RFC 6238 requires SHA1 for Google Authenticator compatibility). Do not add a dependency for this.
  6. For recovery codes, use SHA-256 hashing via Web Crypto to stay edge-compatible. Return both plaintext and hashes from generateRecoveryCodes.
  7. All token helpers (password-reset, email-verification, invitation) follow the same pattern: HMAC-SHA256-signed JSON with iat/exp. Extract the shared signing logic into src/lib/auth/tokens/signed-token.ts.
  8. Tests go in src/lib/auth/*.test.ts mirroring the existing auth.test.ts pattern. Use the existing createInMemoryAuthService and generateAuthSigningKey for test token minting.
  9. Run pnpm typecheck and pnpm vitest run --project unit before declaring complete.

Decision Log

DateDecisionRationale
2026-05-26Use Web Crypto API exclusively (no Node crypto module).Ensures the library runs on edge runtimes without polyfills.
2026-05-26TOTP uses HMAC-SHA1 per RFC 6238.Google Authenticator and most authenticator apps require SHA1 for interoperability.
2026-05-26SSO provider interface follows ADR-051.Consistent with the existing provider pattern used elsewhere in the design system.
2026-05-26Recovery codes hashed with SHA-256, not bcrypt.Edge-runtime compatibility outweighs brute-force resistance for short-lived backup codes.

Document History

VersionDateAuthorChanges
0.12026-05-26David HolmesInitial draft.