The migration, PRD by PRD
The trail of changes since the upstream fork: the milestone azure-migration issues in order, each paired with the code it produced. How Mike went from hosted-Supabase SaaS to a single-tenant Azure deployment.
Parent docs
docs/entraId/entra-implementation-deep-dive.md (Workstream 1, Task 1.3; security baseline) docs/entraId/entra-implementation-task-breakdown.md (Task 1.3) docs/AzureMove/azure-migration-proposal.md (Track A, steps 3–4)
What to build
Implement the Entra token validator in backend/src/lib/auth/providers/entra.ts. When AUTH_PROVIDER=entra, the backend validates Entra-issued JWTs, extracts the normalized principal, and populates res.locals exactly as the Supabase path does.
backend/src/lib/auth/providers/entra.ts
Use jose for OIDC/JWKS validation (no Entra SDK dependency — lightweight, already available or add as a devDependency):
export async function validateEntraToken(token: string): Promise<AuthValidationResult>
Validation steps (in order):
- Fetch JWKS from
https://login.microsoftonline.com/<ENTRA_TENANT_ID>/discovery/v2.0/keys(cache with TTL; rotate automatically onjwks_uriupdate). - Verify signature.
- Validate claims:
issmust equalhttps://login.microsoftonline.com/<ENTRA_TENANT_ID>/v2.0audmust equalENTRA_BACKEND_CLIENT_ID(the backend API app registration's application ID URI or client ID)tidmust equalENTRA_TENANT_IDexpandnbfstandard checks
- Extract principal:
userId=oidclaim (Entra object ID — stable, unique per user per tenant)email=preferred_usernameoremailclaim (claims-first; Graph fallback only if both absent — log a warning, do not call Graph in this slice)tenantId=tidclaimgroups=groupsclaim array if present; if absent or overage marker (_claim_names.groups) detected, return empty array and log a warning (Graph group lookup is deferred — access will be denied in slice 010 if groups are required but empty)
- Return
AuthValidationResult.
Required env vars (add to backend/.env.example):
ENTRA_TENANT_ID— Azure AD tenant GUIDENTRA_BACKEND_CLIENT_ID— backend API app registration client ID
Wire into backend/src/middleware/auth.ts provider switch (placeholder from slice 008 replaced).
Acceptance criteria
- [ ] Valid Entra access token with correct
aud,iss,tid→AuthValidationResult { ok: true }withprincipal.userId= the token'soid. - [ ] Expired token →
{ ok: false, status: 401, detail: 'Token expired' }. - [ ] Wrong
aud→{ ok: false, status: 401, detail: 'Invalid audience' }. - [ ] Wrong
tid(different tenant) →{ ok: false, status: 401, detail: 'Invalid tenant' }. - [ ] Malformed / unsigned token → 401.
- [ ] Group overage marker present →
groups: []with logged warning; does not crash. - [ ] JWKS is cached; a second validation in the same process does not make a second HTTP request to
login.microsoftonline.com. - [ ] No token contents are logged; only structured events (userId, provider, result).
- [ ] backend/.env.example updated with
ENTRA_TENANT_IDandENTRA_BACKEND_CLIENT_IDplaceholder values.
Blocked by
008-backend-auth-provider-abstraction.md(provider switch and types must exist)
User stories addressed
- A user with a valid Entra workforce token can reach any protected backend route.
- A token from a different tenant, an expired token, or a tampered token is rejected before any business logic executes.