Skip to content

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 ISessionStore interface
    public interface ISessionStore
    {
        Task<UserSession?> GetAsync(string sessionId);
        Task StoreAsync(string sessionId, UserSession session);
        Task RemoveAsync(string sessionId);
        Task RefreshAsync(string sessionId, TimeSpan extension);
    }
    
  • Implement InMemorySessionStore for dev/test (Redis can come later)
  • Create UserSession model with:
  • UserId, Email, DisplayName, Roles[]
  • ExpiresAt, AccessToken?, RefreshToken?
  • Provider enum (Auth0, OpenIddict, Mock)
  • Register session store in DI container
  • Add unit tests for session store

1.2 BFF Auth Controller

  • Create BffAuthController.cs at /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-session HttpOnly 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:
  • Auth0 scheme - existing JWT bearer validation
  • BFF scheme - cookie-based session validation
  • Combined policy scheme - tries BFF first, falls back to Auth0
  • Implement SessionAuthenticationHandler for 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.cs at /api/dev-auth/*
  • Implement GET /api/dev-auth/login?returnUrl= - returns investigator list
  • Query pmInvestigator collection
  • Return JSON with id, email, name for each
  • Implement POST /api/dev-auth/login - logs in as selected investigator
  • Accept investigatorId and returnUrl
  • Create BFF session (same as real auth)
  • Set cookie
  • Return success with redirect URL
  • Guard with DEV_AUTH_ENABLED environment 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.cs at /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_ENABLED environment variable
  • Add to appsettings.E2ETest.json
  • Test E2E auth endpoint

1.6 Angular BFF Integration (Minimal)

  • Create BffAuthProvider implementing IAuthProvider
  • 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 from user$
  • 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=true to API service
  • Configure BFF authority for mock OIDC server
  • Keep mock-oauth2-server configuration
  • Update docker-compose.yml for local dev:
  • Add DEV_AUTH_ENABLED=true option
  • Add AUTH_PROVIDER=bff|auth0 selection
  • Document local dev auth options in README

1.8 E2E Test Fixtures

  • Create e2e/fixtures/auth.fixture.ts:
  • authenticateAs(options) - calls E2E auth endpoint
  • authenticatedPage - 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.Endpoint project
  • Create SyRF.Identity.Endpoint.Tests project
  • Add to syrf.sln with proper folder structure
  • Create identity.slnf solution filter
  • Add Dockerfile
  • Add .chart/ Helm chart

2.2 OpenIddict Server Configuration

  • Add NuGet packages:
  • OpenIddict.AspNetCore
  • OpenIddict.MongoDb
  • AspNetCore.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.json with MongoDB connection
  • Test OIDC discovery endpoint (/.well-known/openid-configuration)

2.3 Client Registration

  • Create OpenIddictClientSeeder hosted service
  • Register syrf-web client (SyRF Angular via BFF)
  • Register syrf-forum client (future Discourse SSO)
  • Register e2e-test-client for E2E tests
  • Register dev-client for 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_id
  • https://claims.syrf.org.uk/syrf_groups
  • https://claims.syrf.org.uk/given_name
  • https://claims.syrf.org.uk/family_name
  • Map claims from investigator data
  • Test claim presence in tokens

2.8 Dev Login Support

  • Create DevLoginController in 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.yaml with:
  • 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:
    "Authentication": {
      "BFF": {
        "Authority": "https://identity.syrf.org.uk",
        "ClientId": "syrf-web",
        "ClientSecret": "..."
      }
    }
    
  • Update BffAuthController to 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
  • 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 ApplicationUser for 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 Auth0AuthProvider from Angular
  • Remove Auth0Service from 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.uk to 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
    await page.request.post('/api/e2e-auth/login', {
      data: { userId: 'test-user', roles: ['User'] }
    });
    
  • Full OIDC flow test - verifies auth integration
    await page.goto('/');
    await page.click('[data-testid="login-button"]');
    // Complete OIDC flow on mock server
    
  • Dev investigator picker - manual testing
    await page.goto('/api/dev-auth/login');
    await page.click(`[data-testid="investigator-${id}"]`);
    

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

  1. Functional parity - All Auth0 features replicated
  2. Zero downtime - Seamless transition
  3. 100% user migration - All active users migrated
  4. Performance - Token issuance <200ms
  5. Security - Pass security review
  6. Cost savings - Auth0 subscription eliminated
  7. 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


References