ADR-004 — Alternative auth providers

Shipped 2026-05-25. All five phases landed; the legacy Entra-only columns were dropped after a clean deploy cycle.

Context

ADR-003 made the Entra tenant per-Brand (BYO), but the provider was still hard-coded across the stack: the User schema knew entraOid, the validator imported createEntraTokenValidator, and the console used MSAL exclusively. Future resellers don't all live on Microsoft. The shortest path to “a Brand signs in with whatever IdP they already have” was to add three sibling providers as first-class options.

Decision

One AuthProvider interface, four implementations: Entra, Google Workspace, Okta, Auth0. Each Brand picks exactly one. The existing JwtValidator cache becomes an AuthProvider cache keyed by (brandId, providerId).

interface AuthProvider {
  readonly id: 'entra' | 'google' | 'okta' | 'auth0';
  validateToken(rawToken: string, cfg: ProviderConfig):
    Promise<ProviderClaims | null>;
}

interface ProviderClaims {
  externalId: string;       // Entra oid, Google sub, Okta sub, Auth0 sub
  email: string | null;
  name: string | null;
  raw: Record<string, unknown>;
}

Out of scope

  • Facebook. Unusual for B2B SaaS; revisit if a Brand explicitly asks.
  • GitHub. OAuth-not-OIDC + email-visibility constraints don't justify the special case without a Brand asking.
  • Multi-provider per Brand. A Brand using Google for some users and Okta for others — defer to a separate ADR if it ever comes up.
  • SAML natively. Auth0 and Okta both federate SAML for the Brands that need it; we don't speak it directly.

Phase plan

  1. Phase 1 — Schema + AuthProvider interface. authProvider + authProviderConfigJson on Brand; authOid on User. Entra implementation only.
  2. Phase 2 — Google + Okta + Auth0 validators. OIDC discovery, JWKS cache, post-validate checks.
  3. Phase 3 — Console sign-in dispatch. oidc-client-ts for non-Entra Brands; OIDC callback at /b/<slug>/auth/callback seeds the access token.
  4. Phase 4 — Admin auth-provider config UI. Brand-Owner picks a provider + pastes config; the UI validates before commit.
  5. Phase 5 — Backfill + read-switch. Existing Entra Brands carry on unchanged; non-Entra Brands route through the new columns. Legacy Entra-only columns dropped in #175.

Provider-specific post-validate checks

  • Entra. iss matches the Brand's tenant id; aud matches the Brand's client id.
  • Google. hd claim equals the Brand's hostedDomain.
  • Okta. iss matches the Brand's issuer URL; JWKS via .well-known/openid-configuration.
  • Auth0. aud claim equals the Brand's configured audience.

References

Full markdown: adr-004-alt-auth-providers.md.

Related: Concepts — BYO identity, How-to — Set up BYO Entra.