Skip to content

Auth0 to OpenIddict Migration

Overview

This document outlines the migration strategy from Auth0 to OpenIddict for authentication and authorization in SyRF. The migration is driven by cost considerations while maintaining all existing functionality.

Current State Analysis

Auth0 Features Currently Used

Feature Description Migration Complexity
JWT Bearer Auth Standard OIDC/OAuth2 with RS256 Low - OpenIddict native support
Custom Domain signin.syrf.org.uk Medium - DNS + certificate setup
Management API User CRUD, email verification, password reset High - Must implement equivalent
Social Logins Google only (custom credentials) Low - Reuse existing OAuth credentials
Identity Linking Not currently used N/A - Can add later if needed
Custom Claims SYRF namespace claims in tokens Low - OpenIddict supports this
Refresh Tokens Offline access with rotation Low - OpenIddict native support
Email Verification Ticket-based verification links Medium - Email service integration
Password Reset Ticket-based reset links Medium - Email service integration

Files Requiring Changes

Backend (.NET) - High Impact: - src/services/api/SyRF.API.Endpoint/Program.cs - JWT configuration - src/services/api/SyRF.API.Endpoint/Services/Auth0Config.cs - Replace with OpenIddict config - src/services/api/SyRF.API.Endpoint/Services/Auth0Service.cs - Replace with local user service - src/services/api/SyRF.API.Endpoint/Services/AuthManagementApiClientProvider.cs - Remove (not needed) - src/services/api/SyRF.API.Endpoint/Controllers/AccountController.cs - Major refactor - src/services/api/SyRF.API.Endpoint/Handlers/Investigator*Handler.cs - Remove Auth0 sync

Frontend (Angular) - High Impact: - src/services/web/src/main.ts - Replace Auth0 module - src/services/web/src/app/core/auth/auth.effects.ts - Major refactor - src/services/web/src/app/core/http-interceptors/auth-interceptor.service.ts - Replace - src/services/web/package.json - Replace Auth0 packages

Shared Libraries - Low Impact: - src/libs/kernel/SyRF.SharedKernel/Constants.cs - Keep claim types (minor updates) - src/libs/webhostconfig/SyRF.WebHostConfig.Common/Infrastructure/CurrentUserService.cs - Minor updates


Architecture Decision: OpenIddict Deployment

Create a dedicated identity server as a new service in the monorepo.

src/services/
├── api/                        # Existing - becomes resource server only
├── project-management/         # Existing - resource server
├── identity/                   # NEW - OpenIddict authorization server
│   ├── SyRF.Identity.Endpoint/
│   │   ├── Controllers/
│   │   │   ├── AuthorizationController.cs
│   │   │   ├── AccountController.cs      # Moved from API
│   │   │   ├── UserInfoController.cs
│   │   │   └── TokenController.cs
│   │   ├── Services/
│   │   ├── Views/              # Login, consent, error pages
│   │   └── Program.cs
│   ├── SyRF.Identity.Core/
│   │   └── Entities/           # ApplicationUser, etc.
│   └── Dockerfile
├── web/
└── quartz/

Pros: - Clean separation of concerns - Independent scaling of identity service - Follows microservices best practices - Easier to maintain and update - Can be deployed to different infrastructure if needed

Cons: - New service to deploy and maintain - Additional infrastructure cost (minimal) - Cross-service database access for user data

Option B: Integrated with API Service

Embed OpenIddict server within the existing API service.

Pros: - Simpler deployment (no new service) - Direct database access for user data - Fewer moving parts

Cons: - Couples identity concerns with business logic - Harder to scale independently - Larger API service codebase - Not recommended by OpenIddict documentation

Recommendation: Option A (New Identity Service)

The identity service should be separate for: 1. Security isolation 2. Independent scaling 3. Cleaner architecture 4. Follows industry best practices


Database Strategy

Use existing MongoDB infrastructure with OpenIddict MongoDB stores.

// OpenIddict with MongoDB
services.AddOpenIddict()
    .AddCore(options =>
    {
        options.UseMongoDb()
            .UseDatabase(mongoClient.GetDatabase("syrf-identity"));
    });

Required Collections: - OpenIddictApplications - Registered clients (web app, API) - OpenIddictAuthorizations - Active authorizations - OpenIddictScopes - Defined scopes - OpenIddictTokens - Issued tokens (for revocation) - IdentityUsers - User accounts (email, password hash, claims)

Packages: - OpenIddict.MongoDb - MongoDB stores for OpenIddict - AspNetCore.Identity.MongoDbCore - ASP.NET Core Identity with MongoDB

Option 2: Entity Framework Core with PostgreSQL

Add a new PostgreSQL database for identity data.

Pros: - More common OpenIddict setup - Better tooling for identity management

Cons: - Additional database to manage - Different technology stack - Migration complexity

Recommendation: Option 1 (MongoDB)

Continue using MongoDB for consistency with existing infrastructure.


User Migration Strategy

Phase 1: Data Export from Auth0

Export all users from Auth0 Management API:

{
  "user_id": "auth0|abc123",
  "email": "user@example.com",
  "email_verified": true,
  "name": "John Doe",
  "given_name": "John",
  "family_name": "Doe",
  "picture": "https://...",
  "identities": [
    {
      "connection": "Username-Password-Authentication",
      "user_id": "abc123",
      "provider": "auth0"
    },
    {
      "connection": "google-oauth2",
      "user_id": "google123",
      "provider": "google-oauth2"
    }
  ],
  "app_metadata": {
    "syrf_groups": "admin researcher"
  }
}

Phase 2: Password Handling

Challenge: Auth0 password hashes cannot be exported (security feature).

Solutions:

  1. Password Reset Required (Recommended)
  2. Import users with email only
  3. Force password reset on first login
  4. Send bulk password reset emails

  5. Lazy Migration with Auth0 Fallback

  6. Keep Auth0 running temporarily
  7. On login failure in OpenIddict, try Auth0
  8. If Auth0 succeeds, migrate password hash
  9. Eventually disable Auth0 fallback

  10. Magic Link Only Initially

  11. Disable password login temporarily
  12. Use email magic links for authentication
  13. Allow users to set password after login

Recommendation: Option 1 (Password Reset Required)

Simplest and most secure approach. Users reset passwords once during migration.

Phase 3: Social Login Migration

Only Google social login is currently configured. Since SyRF uses custom Google OAuth credentials in Auth0 (not Auth0's built-in connection), these credentials can be directly transferred to OpenIddict.

services.AddAuthentication()
    .AddGoogle(options =>
    {
        // Reuse existing credentials from Auth0
        options.ClientId = "<existing-google-client-id>";
        options.ClientSecret = "<existing-google-client-secret>";
    });

Key Benefits of Credential Reuse: - ✅ Users will NOT see Google's consent screen again (consent is tied to Client ID) - ✅ Existing Google user IDs remain valid - ✅ Seamless experience for social login users

Migration Process: 1. Export Google OAuth Client ID and Secret from Auth0 Dashboard 2. Configure the same credentials in OpenIddict 3. Import external login mappings from Auth0 user export 4. Users can log in with Google immediately - no re-linking required


Implementation Plan

Stage 1: Identity Service Foundation

Duration: 1-2 weeks

  1. Create new src/services/identity/ service structure
  2. Set up OpenIddict with MongoDB stores
  3. Configure ASP.NET Core Identity with MongoDB
  4. Implement basic endpoints:
  5. /.well-known/openid-configuration
  6. /connect/authorize
  7. /connect/token
  8. /connect/userinfo
  9. Create login/logout views (Razor Pages or MVC)
  10. Configure development certificates
  11. Add to Docker and Helm charts

Deliverables: - Working identity server with local user registration - OpenID Connect discovery endpoint - Authorization code flow working

Stage 2: Feature Parity

Duration: 1-2 weeks

  1. Email verification flow
  2. Password reset flow
  3. Profile management endpoints
  4. Custom claims configuration
  5. Refresh token support
  6. Social login providers (Google, Apple, Facebook)

Deliverables: - Email service integration - All account management features working - Social login working

Stage 3: API Service Updates

Duration: 1 week

  1. Update JWT Bearer configuration to use OpenIddict
  2. Remove Auth0-specific services
  3. Update authorization handlers (minimal changes)
  4. Update claim extraction (if namespace changes)
  5. Remove Auth0 sync handlers
  6. Update Swagger OAuth configuration

Deliverables: - API validates tokens from OpenIddict - All endpoints working with new auth

Stage 4: Frontend Updates

Duration: 1-2 weeks

  1. Replace @auth0/auth0-angular with OIDC client library
  2. Update login/logout flows
  3. Update token handling
  4. Update HTTP interceptor
  5. Update user profile management
  6. Update ngrx auth state

Recommended Package: angular-auth-oidc-client

Deliverables: - Frontend authenticates with OpenIddict - All user flows working

Stage 5: User Migration

Duration: 1 week

  1. Export users from Auth0
  2. Import users to OpenIddict database
  3. Send password reset emails
  4. Monitor migration progress
  5. Handle edge cases

Deliverables: - All users migrated - Password reset emails sent - Login working for all users

Stage 6: Cutover and Decommission

Duration: 1 week

  1. Update DNS for signin.syrf.org.uk
  2. Switch production to OpenIddict
  3. Monitor for issues
  4. Keep Auth0 in read-only mode temporarily
  5. Decommission Auth0 after validation period

Deliverables: - Production running on OpenIddict - Auth0 decommissioned - Cost savings achieved


Technical Implementation Details

OpenIddict Server Configuration

// Program.cs for Identity Service
builder.Services.AddOpenIddict()
    .AddCore(options =>
    {
        options.UseMongoDb()
            .UseDatabase(mongoDatabase);
    })
    .AddServer(options =>
    {
        // Enable authorization code + refresh token flows
        options.AllowAuthorizationCodeFlow()
               .AllowRefreshTokenFlow();

        // Enable PKCE (required for public clients)
        options.RequireProofKeyForCodeExchange();

        // Token endpoints
        options.SetAuthorizationEndpointUris("/connect/authorize")
               .SetTokenEndpointUris("/connect/token")
               .SetUserinfoEndpointUris("/connect/userinfo")
               .SetLogoutEndpointUris("/connect/logout")
               .SetIntrospectionEndpointUris("/connect/introspect");

        // Signing credentials
        options.AddSigningKey(signingKey)
               .AddEncryptionKey(encryptionKey);

        // ASP.NET Core integration
        options.UseAspNetCore()
               .EnableAuthorizationEndpointPassthrough()
               .EnableTokenEndpointPassthrough()
               .EnableUserinfoEndpointPassthrough()
               .EnableLogoutEndpointPassthrough();

        // Token lifetimes
        options.SetAccessTokenLifetime(TimeSpan.FromMinutes(60))
               .SetRefreshTokenLifetime(TimeSpan.FromDays(14));
    })
    .AddValidation(options =>
    {
        options.UseLocalServer();
        options.UseAspNetCore();
    });

Custom Claims Configuration

// Add custom claims to tokens
public class CustomClaimsService : IOpenIddictServerHandler<ProcessSignInContext>
{
    public async ValueTask HandleAsync(ProcessSignInContext context)
    {
        var identity = (ClaimsIdentity)context.Principal!.Identity!;

        // Add SYRF-specific claims
        identity.AddClaim("https://claims.syrf.org.uk/user_id", userId);
        identity.AddClaim("https://claims.syrf.org.uk/syrf_groups", groups);
        identity.AddClaim("https://claims.syrf.org.uk/given_name", givenName);
        // ... etc
    }
}

API JWT Validation Update

// Program.cs for API Service
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
    .AddJwtBearer(options =>
    {
        options.Authority = "https://signin.syrf.org.uk"; // Now points to OpenIddict
        options.Audience = "https://syrf-api.syrf.org.uk";
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidateAudience = true,
            ValidateLifetime = true,
            ValidateIssuerSigningKey = true
        };

        // Keep SignalR token handling
        options.Events = new JwtBearerEvents
        {
            OnMessageReceived = context =>
            {
                var accessToken = context.Request.Query["access_token"];
                var path = context.HttpContext.Request.Path;
                if (!string.IsNullOrEmpty(accessToken) &&
                    path.StartsWithSegments("/notifications"))
                {
                    context.Token = accessToken;
                }
                return Task.CompletedTask;
            }
        };
    });

Angular OIDC Configuration

// Using angular-auth-oidc-client
import { AuthModule, LogLevel } from 'angular-auth-oidc-client';

@NgModule({
  imports: [
    AuthModule.forRoot({
      config: {
        authority: 'https://signin.syrf.org.uk',
        redirectUrl: window.location.origin,
        postLogoutRedirectUri: window.location.origin,
        clientId: 'syrf-web',
        scope: 'openid profile email offline_access',
        responseType: 'code',
        silentRenew: true,
        useRefreshToken: true,
        logLevel: LogLevel.Debug,
        secureRoutes: ['https://api.syrf.org.uk'],
      },
    }),
  ],
})
export class AppModule {}

Risk Assessment

Risk Impact Likelihood Mitigation
Password migration issues High Medium Bulk password reset emails, support process
Social login linking breaks Medium Medium Clear user communication, re-linking flow
Token validation issues High Low Thorough testing, gradual rollout
Performance degradation Medium Low Load testing before cutover
User confusion Medium High Clear communication, help documentation
Rollback needed High Low Keep Auth0 active during transition

Success Criteria

  1. Functional Parity: All Auth0 features replicated in OpenIddict
  2. User Migration: 100% of active users migrated successfully
  3. Zero Downtime: Seamless transition with no service interruption
  4. Performance: Token issuance < 200ms, validation < 50ms
  5. Security: Pass security review, proper key management
  6. Cost Savings: Auth0 subscription eliminated

Questions for User Decision

All Answered ✅

  • User Count: 5,711 users to migrate
  • Custom Domain: Keep signin.syrf.org.uk
  • Email Service: AWS SES for verification and password reset emails
  • Social Login: Google only, using custom OAuth credentials (reusable - no consent screen) ✅
  • Timeline: ASAP - targeting 6-8 week phased rollout
  • Weeks 1-3: Build identity service, staging deployment
  • Week 4: Frontend + API updates in staging
  • Week 5: User migration to staging, internal testing
  • Week 6: Production cutover
  • Weeks 7-8: Monitoring period
  • Rollback Plan: Keep Auth0 active for 4 weeks after production cutover
  • Monitor login success rates
  • All active users should log in at least once
  • Decommission Auth0 after validation period

Next Steps

  1. Review and approve this migration plan
  2. Answer clarifying questions
  3. Create identity service project structure
  4. Set up OpenIddict with MongoDB
  5. Implement basic authorization endpoints
  6. Configure AWS SES for email
  7. Begin frontend migration preparation

References