Auth0 to OpenIddict Migration with BFF Pattern¶
Overview¶
This planning document covers the migration from Auth0 to OpenIddict using a BFF (Backend for Frontend) pattern. The BFF approach stores tokens server-side and uses HttpOnly cookies for authentication, improving security and simplifying the Angular frontend.
Related documents: - Feature Brief: Auth0 to OpenIddict Migration - E2E Testing Infrastructure
Architecture Summary¶
┌─────────────────────────────────────────────────────────────────────────────┐
│ Current (Auth0 + SPA Tokens) Target (OpenIddict + BFF) │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────┐ JWT Token ┌─────┐ ┌─────────┐ Cookie ┌─────┐ │
│ │ Angular │ ──────────────► │ API │ │ Angular │ ────────► │ API │ │
│ │ SPA │ │ │ │ SPA │ │ BFF │ │
│ └────┬────┘ └─────┘ └─────────┘ └──┬──┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────┐ ┌────────────┐ │
│ │ Auth0 │ │ OpenIddict │ │
│ │ Tenant │ │ Identity │ │
│ └─────────┘ │ Service │ │
│ └────────────┘ │
└─────────────────────────────────────────────────────────────────────────────┘
Key Differences: - Tokens stored server-side in session store (not in browser localStorage) - HttpOnly cookies for authentication (not JWT in Authorization header) - API handles OIDC flow on behalf of Angular (Angular just redirects) - Separate Identity Service for user management and SSO
Phase 1: BFF Layer + Dev/E2E Auth Adapters¶
Goal: Add BFF endpoints to API, adapt mock auth for cookies. All existing tests pass.
Duration: 1-2 weeks
1.1 Session Infrastructure¶
- Create
src/services/api/SyRF.API.Endpoint/Auth/directory - Implement
ISessionStoreinterface - Implement
InMemorySessionStorefor dev/test (Redis can come later) - Create
UserSessionmodel with: UserId,Email,DisplayName,Roles[]ExpiresAt,AccessToken?,RefreshToken?Providerenum (Auth0,OpenIddict,Mock)- Register session store in DI container
- Add unit tests for session store
1.2 BFF Auth Controller¶
- Create
BffAuthController.csat/api/auth/* - Implement
GET /api/auth/login- initiates OIDC flow - Generate PKCE code verifier/challenge
- Store verifier in temp cookie (10 min expiry)
- Build authorization URL with state parameter
- Return redirect to IdP
- Implement
GET /api/auth/callback- handles OIDC callback - Exchange code for tokens using PKCE
- Parse ID token claims
- Create session in store
- Set
syrf-sessionHttpOnly cookie - Redirect to original destination (from state)
- Implement
GET /api/auth/me- returns current user - Read session from cookie
- Return user info JSON (or 401)
- Implement
POST /api/auth/logout- clears session - Remove session from store
- Delete cookie
- Optionally trigger IdP logout
- Implement
POST /api/auth/refresh- refreshes session - Use refresh token to get new access token
- Update session in store
- Add integration tests for BFF endpoints
1.3 Combined Authentication Scheme¶
- Configure dual authentication in
Program.cs: Auth0scheme - existing JWT bearer validationBFFscheme - cookie-based session validationCombinedpolicy scheme - tries BFF first, falls back to Auth0- Implement
SessionAuthenticationHandlerfor BFF scheme - Add claim principal construction from session
- Test that both auth methods work simultaneously
- Add integration tests verifying both paths
1.4 Dev Auth Controller (Investigator Picker)¶
- Create
DevAuthController.csat/api/dev-auth/* - Implement
GET /api/dev-auth/login?returnUrl=- returns investigator list - Query
pmInvestigatorcollection - Return JSON with id, email, name for each
- Implement
POST /api/dev-auth/login- logs in as selected investigator - Accept
investigatorIdandreturnUrl - Create BFF session (same as real auth)
- Set cookie
- Return success with redirect URL
- Guard with
DEV_AUTH_ENABLEDenvironment variable - Guard with
IsDevelopment()or explicit env var - Create simple HTML page for investigator selection (or return JSON for Angular)
- Test dev auth flow end-to-end
1.5 E2E Auth Controller (Fast Path)¶
- Create
E2EAuthController.csat/api/e2e-auth/* - Implement
POST /api/e2e-auth/login- direct session creation - Accept userId, email, displayName, roles in body
- Create session immediately (no OIDC flow)
- Set cookie
- Return success
- Guard with
E2E_AUTH_ENABLEDenvironment variable - Add to
appsettings.E2ETest.json - Test E2E auth endpoint
1.6 Angular BFF Integration (Minimal)¶
- Create
BffAuthProviderimplementingIAuthProvider - Implement
loginWithRedirect()- redirects to/api/auth/login - Implement
getAccessTokenSilently()- returns empty (cookies handle auth) - Implement
logout()- calls/api/auth/logout - Implement
user$observable - polls/api/auth/me - Implement
isAuthenticated$- derives fromuser$ - Add
authProvider: 'bff' | 'auth0'config option - Register provider selection in
main.ts - Update HTTP interceptor to NOT add Authorization header when using BFF
- Test login/logout flow with BFF provider
1.7 Docker Compose Updates¶
- Update
docker-compose.e2e.yml: - Add
E2E_AUTH_ENABLED=trueto API service - Configure BFF authority for mock OIDC server
- Keep mock-oauth2-server configuration
- Update
docker-compose.ymlfor local dev: - Add
DEV_AUTH_ENABLED=trueoption - Add
AUTH_PROVIDER=bff|auth0selection - Document local dev auth options in README
1.8 E2E Test Fixtures¶
- Create
e2e/fixtures/auth.fixture.ts: authenticateAs(options)- calls E2E auth endpointauthenticatedPage- default authenticated context- Update existing auth setup to use direct session creation
- Verify all existing E2E tests pass with new auth
- Add E2E tests for BFF auth flow
- Add E2E test for dev investigator picker
Phase 1 Verification¶
- All unit tests pass
- All integration tests pass
- All E2E tests pass
- Dev investigator picker works locally
- Auth0 production auth still works (no regression)
- Document phase 1 completion
Phase 2: Identity Service with OpenIddict¶
Goal: Create standalone Identity Service. Not yet connected to SyRF.
Duration: 2-3 weeks
2.1 Project Structure¶
- Create
src/services/identity/directory - Create
SyRF.Identity.Endpointproject - Create
SyRF.Identity.Endpoint.Testsproject - Add to
syrf.slnwith proper folder structure - Create
identity.slnfsolution filter - Add
Dockerfile - Add
.chart/Helm chart
2.2 OpenIddict Server Configuration¶
- Add NuGet packages:
OpenIddict.AspNetCoreOpenIddict.MongoDbAspNetCore.Identity.MongoDbCore- Configure OpenIddict Core with MongoDB stores
- Configure OpenIddict Server:
- Authorization code flow with PKCE
- Refresh token flow
- Standard endpoints (
/connect/authorize,/connect/token, etc.) - Custom scopes for SyRF
- Token lifetimes (1h access, 14d refresh)
- Configure ASP.NET Core Identity with MongoDB
- Add development signing/encryption certificates
- Create
appsettings.jsonwith MongoDB connection - Test OIDC discovery endpoint (
/.well-known/openid-configuration)
2.3 Client Registration¶
- Create
OpenIddictClientSeederhosted service - Register
syrf-webclient (SyRF Angular via BFF) - Register
syrf-forumclient (future Discourse SSO) - Register
e2e-test-clientfor E2E tests - Register
dev-clientfor local development - Test client configuration
2.4 Account Management Pages¶
- Create Razor Pages structure in
Pages/Account/ - Implement Login page
- Username/password form
- Error handling
- Remember me option
- Implement Register page
- Email, password, name fields
- Password validation
- Email verification trigger
- Implement Password Reset flow
- Request page (enter email)
- Reset page (enter new password)
- Email template
- Implement Profile page
- View/edit user info
- Change password
- Style pages to match SyRF branding
- Test all account flows
2.5 Email Integration (AWS SES)¶
- Add email service interface
IEmailService - Implement
SesEmailService - Create email templates:
- Email verification
- Password reset
- Welcome email
- Configure SES credentials from secrets
- Test email delivery
2.6 Google OAuth Integration¶
- Add
Microsoft.AspNetCore.Authentication.Google - Configure with existing Google OAuth credentials (from Auth0)
- Implement external login flow
- Link external logins to local accounts
- Test Google login end-to-end
2.7 Custom Claims¶
- Implement
IOpenIddictServerHandler<ProcessSignInContext> - Add SyRF-specific claims to tokens:
https://claims.syrf.org.uk/user_idhttps://claims.syrf.org.uk/syrf_groupshttps://claims.syrf.org.uk/given_namehttps://claims.syrf.org.uk/family_name- Map claims from investigator data
- Test claim presence in tokens
2.8 Dev Login Support¶
- Create
DevLoginControllerin Identity Service - Implement investigator picker (queries pmInvestigator)
- Sign in selected investigator directly
- Guard with environment check
- Test dev login flow
2.9 Helm Chart¶
- Create
Chart.yaml - Create
values.yamlwith: - MongoDB connection
- Google OAuth credentials
- SES configuration
- Signing key secrets
- Create deployment template
- Create service template
- Create ingress template
- Add to ArgoCD ApplicationSet
2.10 Deployment to Staging¶
- Deploy Identity Service to staging namespace
- Configure DNS (
identity.staging.syrf.org.uk) - Verify OIDC endpoints work
- Test login flow against staging
- Do NOT connect to SyRF services yet
Phase 2 Verification¶
- Identity Service running in staging
- All OIDC endpoints functional
- Account management pages work
- Google OAuth works
- Dev login works
- Document phase 2 completion
Phase 3: Parallel Authentication¶
Goal: API accepts both Auth0 and OpenIddict. Users unaffected.
Duration: 1-2 weeks
3.1 API Configuration Update¶
- Add OpenIddict configuration to
appsettings.json: - Update
BffAuthControllerto use OpenIddict authority - Keep Auth0 JWT validation as fallback
- Test both auth paths work
3.2 Angular Environment Updates¶
- Add environment config for staging with OpenIddict
- Add environment toggle for auth provider
- Test Angular works with both providers
3.3 E2E Tests for Both Paths¶
- Create migration verification tests:
- Test BFF + OpenIddict flow
- Test legacy Auth0 token still works
- Run full E2E suite against both providers
- Document any differences in behavior
3.4 Staging Validation¶
- Deploy API with parallel auth to staging
- Deploy Angular with BFF provider to staging
- Verify new auth flow works end-to-end
- Verify old Auth0 tokens still accepted
- Internal team testing on staging
Phase 3 Verification¶
- Both auth methods work simultaneously
- All E2E tests pass
- Staging environment validated
- No regressions in Auth0 flow
- Document phase 3 completion
Phase 4: User Migration¶
Goal: Import Auth0 users to OpenIddict. Gradual rollout.
Duration: 2-4 weeks
4.1 User Export from Auth0¶
- Create Auth0 Management API client
- Export all users with:
- User ID
- Email verified status
- Name fields
- Google identity links
- App metadata (syrf_groups)
- Store export as JSON for processing
- Document user count and characteristics
4.2 User Import to OpenIddict¶
- Create
Auth0MigrationService - Implement user import:
- Create
ApplicationUserfor each Auth0 user - Keep same user ID (critical for data integrity)
- Set email verified status
- Link Google identities
- Set migration status flag
- Handle duplicate detection
- Create import script/endpoint
- Test import with subset of users
4.3 Password Reset Campaign¶
- Design password reset email
- Implement bulk password reset sender
- Create landing page with clear instructions
- Schedule reset emails (batched to avoid spam filters)
- Monitor bounce/delivery rates
4.4 Migration Dashboard¶
- Create admin dashboard showing:
- Total users
- Imported users
- Email sent users
- Password set users
- First login completed users
- Add migration status to user model
- Create API endpoints for dashboard
4.5 Gradual Rollout¶
- Define rollout groups (by user activity, organization, etc.)
- Implement feature flag for new auth
- Roll out to internal users first
- Roll out to beta users
- Monitor success/failure rates
- Full rollout
4.6 Google OAuth User Migration¶
- Verify Google credentials work with OpenIddict
- Test Google users can log in seamlessly
- Verify no consent screen appears (same client ID)
- Document any edge cases
Phase 4 Verification¶
- All users imported
- Password reset emails sent
- Migration dashboard shows progress
- Google OAuth users work
- Support process documented
- Document phase 4 completion
Phase 5: Cutover and Decommission¶
Goal: Disable Auth0. OpenIddict + BFF is the only auth.
Duration: 1 week
5.1 Final Configuration¶
- Remove Auth0 JWT validation from API
- Remove Auth0 configuration
- Remove Auth0 packages from Angular
- Remove Auth0 environment variables
- Update all environment configs
5.2 Code Cleanup¶
- Remove
Auth0AuthProviderfrom Angular - Remove
Auth0Servicefrom API - Remove
AuthManagementApiClientProvider - Remove Auth0 sync handlers
- Remove migration middleware
- Remove parallel auth code paths
- Update documentation
5.3 DNS Cutover¶
- Update
signin.syrf.org.ukto point to Identity Service - Monitor DNS propagation
- Verify all flows work with new DNS
5.4 Production Deployment¶
- Deploy final API configuration
- Deploy final Angular build
- Monitor error rates
- Monitor login success rates
- Have rollback plan ready
5.5 Auth0 Decommission¶
- Keep Auth0 tenant read-only for 4 weeks
- Monitor for any fallback attempts
- Export final audit logs
- Cancel Auth0 subscription
- Document savings achieved
5.6 Final E2E Verification¶
- Run full E2E suite
- Verify dev login works
- Verify E2E auth works
- Performance baseline tests
- Security review
Phase 5 Verification¶
- Auth0 disabled
- All users on OpenIddict
- All tests pass
- Production stable
- Cost savings confirmed
- Document migration complete
E2E Testing Strategy¶
Test Infrastructure¶
The E2E test infrastructure supports all migration phases:
┌─────────────────────────────────────────────────────────────────────────────┐
│ E2E Auth Configuration │
├─────────────────────────────────────────────────────────────────────────────┤
│ │
│ Phase 1-3: mock-oauth2-server via BFF │
│ ┌──────────────┐ │
│ │ Playwright │───► /api/e2e-auth/login ───► Direct session creation │
│ │ Test Runner │ (fast path, no OIDC) │
│ └──────────────┘ │
│ │ │
│ └─────────► /api/auth/login ───► mock-oauth2-server ───► callback │
│ (full OIDC flow test) │
│ │
│ Phase 4-5: OpenIddict via BFF (same pattern) │
│ ┌──────────────┐ │
│ │ Playwright │───► /api/e2e-auth/login ───► Direct session creation │
│ │ Test Runner │ │
│ └──────────────┘ │
│ │ │
│ └─────────► /api/auth/login ───► OpenIddict (dev mode) ───► callback│
│ │
└─────────────────────────────────────────────────────────────────────────────┘
Test Patterns¶
- Direct session creation - fastest, for most tests
- Full OIDC flow test - verifies auth integration
- Dev investigator picker - manual testing
Test Tiers¶
| Tier | Trigger | Duration | Auth Method |
|---|---|---|---|
| Smoke (@smoke) | PR label, path filter | ~3 min | Direct session |
| Full (@full) | Merge to main, label | ~15 min | Direct + OIDC flow |
E2E Test Files to Create/Update¶
-
e2e/fixtures/auth.fixture.ts- session creation helpers -
e2e/fixtures/auth.setup.ts- OIDC login + save storageState -
e2e/tests/smoke/login.spec.ts- basic auth verification -
e2e/tests/full/auth-flow.spec.ts- complete OIDC flow -
e2e/tests/full/multi-user.spec.ts- multiple user sessions
Local Development Testing¶
Dev Auth Options¶
Developers can choose their auth method:
# Option 1: Mock OIDC server (full flow)
docker compose -f docker-compose.e2e.yml up mock-oidc
AUTH_PROVIDER=bff dotnet run
# Option 2: Dev investigator picker (quick switch)
DEV_AUTH_ENABLED=true dotnet run
# Then visit http://localhost:5080/api/dev-auth/login
# Option 3: Auth0 (production-like)
AUTH_PROVIDER=auth0 dotnet run
Docker Compose Profiles¶
# docker-compose.yml
services:
api:
environment:
- AUTH_PROVIDER=${AUTH_PROVIDER:-bff}
- DEV_AUTH_ENABLED=${DEV_AUTH_ENABLED:-true}
mock-oidc:
profiles: ["mock", "e2e"]
image: ghcr.io/navikt/mock-oauth2-server:3.0.1
Quick Start Commands¶
- Document
pnpm dev:auth:mock- starts mock OIDC + services - Document
pnpm dev:auth:picker- uses investigator picker - Document
pnpm dev:auth:auth0- uses Auth0 (requires credentials) - Document
pnpm e2e:local- runs E2E tests locally
Risk Mitigation¶
| Risk | Impact | Mitigation | Status |
|---|---|---|---|
| Password migration confusion | High | Clear email communications, support process | Planned |
| Google OAuth re-consent | Medium | Reuse same client credentials | Verified |
| Token validation issues | High | Parallel auth period, gradual rollout | Planned |
| Session store failures | High | Redis with replication for prod | Planned |
| Rollback needed | High | Keep Auth0 active 4 weeks post-cutover | Planned |
| E2E tests break during migration | Medium | Direct session creation bypasses OIDC | Implemented |
Success Criteria¶
- Functional parity - All Auth0 features replicated
- Zero downtime - Seamless transition
- 100% user migration - All active users migrated
- Performance - Token issuance <200ms
- Security - Pass security review
- Cost savings - Auth0 subscription eliminated
- SSO ready - Can add Discourse/other apps easily
Timeline¶
| Phase | Duration | Dependencies |
|---|---|---|
| Phase 1: BFF Layer | 1-2 weeks | None |
| Phase 2: Identity Service | 2-3 weeks | Phase 1 |
| Phase 3: Parallel Auth | 1-2 weeks | Phase 2 |
| Phase 4: User Migration | 2-4 weeks | Phase 3 |
| Phase 5: Cutover | 1 week | Phase 4 |
Total: 7-12 weeks