Auth0 to OpenIddict Migration¶
Superseded — read this before anything below it.
This is the original December 2025 migration plan. Its last content change was 2026-05-04 (
14f2dd375) and its front matter still claimedupdated: 2025-12-16until 2026-08-25. The migration it describes is live and ongoing; this document is not the plan being followed. Several statements below are now contradicted by the merged code, and it is the document a newcomer is most likely to find first — which is why it is corrected here rather than quietly edited or deleted.The authoritative documents are:
- Auth0 behaviour → native Identity/OpenIddict parity gate — the acceptance matrix.
- M005-ROADMAP.md — the Phase 5 cutover/decommission plan.
- M005-CONTEXT.md — locked operational boundaries.
- M005-VALIDATION.md — the validation contract and what is actually evidenced.
The design record below is retained because the options it weighs (a separate Identity service, MongoDB stores, forced password reset) are the decisions that were taken, and the reasoning is still worth having.
Corrections — where this document and the code disagree (2026-08-27)¶
Reconciled against current SyRF main through #2905 (14755a8b2). Test totals
below are measured on that final stacked candidate rather than carried forward
from an earlier merge.
| This document says | What the code does |
|---|---|
| "Identity Linking — Not currently used — N/A, can add later if needed" | External identity linking is implemented, and is one of the four native-parity workstreams (PR C, #2808). Link and unlink require a five-minute account/security-stamp-bound proof, now bound to the exact operation target by #2902 (src/services/identity/SyRF.Identity.Endpoint/Services/StepUpProofService.cs:40-52,94-120), run through Services/ExternalIdentityLifecycleService.cs, and are exposed at Pages/Account/Manage/ExternalLogins.cshtml.cs. A provider account links to at most one SyRF account (Data/IdentityUserIndexInitializer.cs:82-90), and email alone never links. |
Stage 6: "Update DNS for signin.syrf.org.uk" and "Custom Domain — signin.syrf.org.uk — DNS + certificate setup" |
Superseded and now a locked boundary in the opposite direction: signin.syrf.org.uk remains the Auth0 custom domain until the final authorised S22 action. OpenIddict is planned to be served at identity.staging.syrf.org.uk and identity.syrf.org.uk (M005-CONTEXT, "Locked Boundaries"). Neither host is deployed: cluster-gitops@e25ca2ed declares and pins disabled staging Identity 1.24.0 / SyRF 8053842, but the selector creates no Argo Application while service.enabled: false; read-only inspection on 2026-08-27 found no Identity workload, Service, Ingress or DNS record, and no production Identity registration exists. The pin predates #2905, so it is not the eventual S08 candidate. |
Stage 4 and the Angular snippet: replace @auth0/auth0-angular with angular-auth-oidc-client |
The browser gets no OIDC client library. The target flow is Angular → same-origin BFF → HttpOnly session cookie (src/services/api/SyRF.API.Endpoint/Auth/BffAuthController.cs, Auth/SessionAuthenticationHandler.cs), with provider, canonical issuer and an immutable generation in the session namespace (Auth/SessionKeySpace.cs:7-38). @auth0/auth0-angular is still a dependency (src/services/web/package.json:44) and is removed at S23, after the 28-day observation gate — not now. |
The API snippet: point options.Authority at signin.syrf.org.uk |
The API keeps Auth0 JWT validation and adds OpenIddict introspection alongside it, selected per token by wire format (src/services/api/SyRF.API.Endpoint/Program.cs:342,407; Auth/DirectBearerSchemeSelector.cs:13). Introspection enforces the exact API audience and syrf_api, and fails closed. |
The custom-claims snippet: add user_id from a local userId |
The invariant is stricter. sub is the immutable Identity account id; the namespaced user_id is a verified InvestigatorId, is never substituted, and issuance fails closed when the mapping is missing or ambiguous (SyRF.Identity.Endpoint/Controllers/AuthorizationController.cs:108,250,339-349; Services/UserClaimsService.cs:21-25; SyRF.ProjectManagement.Core/Services/ApplicationService.cs:111-140). One caveat on that citation: :339-349 is the /connect/userinfo gate (:339 guards a null user or absent SyrfUserId; :344-349 adds the account-version match and the admission evaluation), and userinfo enforces the same fail-closed rule but does not share the claim producer — it allocates its own dictionary at :352 instead of calling UserClaimsService, which is recorded as an open divergence in M005-VALIDATION.md. The transitional identities claim this document's export sample implies was decided against and exists nowhere in the code. |
Required collections: IdentityUsers plus the four OpenIddict stores |
The Identity database also holds IdentityInvestigatorMappings — a permanent one-row-per-InvestigatorId tombstone that survives account deletion — and IdentityEmailReservations, which serialises claims on canonical email addresses. |
| Stage 2: "Social login providers (Google, Apple, Facebook)" | Google only. A provider that does not assert a verified email cannot create an account at all today; that path refuses and directs the user to local registration (Pages/Account/ExternalLogin.cshtml.cs:209-212). |
| Stage 5: import users, send reset emails, "all users migrated" | Migration additionally preserves per-source verification provenance, the historical app_metadata.syrf_id answers, and Auth0 blocked independently of Investigator Deactivated (SyRF.Identity.Migration/UserImporter.cs:210-213,542,589-596; InvestigatorReconciler.cs:182-184). Unconfirmed and profile-incomplete users are held in an Identity-only middle step rather than admitted (SyRF.Identity.Endpoint/Services/IdentityAdmissionMiddleware.cs:31-72). No production import has run. |
| "Next Steps: create identity service project structure / set up OpenIddict / implement basic authorization endpoints" | All done. src/services/identity/ exists and its final stacked suite runs 1,789 tests with zero failures (1,112 endpoint + 677 migration at 14755a8b2, 2026-08-27). The live work is Phase 5 cutover and decommission, which has not reached staging: the staging Identity registration in cluster-gitops is deliberately disabled and no production registration exists. |
The user-count, timeline and success-criteria figures below were not re-verified for this correction; treat them as the December 2025 record.
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 (as at 2025-12-16 — superseded)¶
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 | ||
| 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¶
Option A: New Identity Service (Recommended)¶
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¶
Option 1: MongoDB (Recommended)¶
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:
- Password Reset Required (Recommended)
- Import users with email only
- Force password reset on first login
-
Send bulk password reset emails
-
Lazy Migration with Auth0 Fallback
- Keep Auth0 running temporarily
- On login failure in OpenIddict, try Auth0
- If Auth0 succeeds, migrate password hash
-
Eventually disable Auth0 fallback
-
Magic Link Only Initially
- Disable password login temporarily
- Use email magic links for authentication
- 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
- Create new
src/services/identity/service structure - Set up OpenIddict with MongoDB stores
- Configure ASP.NET Core Identity with MongoDB
- Implement basic endpoints:
/.well-known/openid-configuration/connect/authorize/connect/token/connect/userinfo- Create login/logout views (Razor Pages or MVC)
- Configure development certificates
- 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
- Email verification flow
- Password reset flow
- Profile management endpoints
- Custom claims configuration
- Refresh token support
- 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
- Update JWT Bearer configuration to use OpenIddict
- Remove Auth0-specific services
- Update authorization handlers (minimal changes)
- Update claim extraction (if namespace changes)
- Remove Auth0 sync handlers
- Update Swagger OAuth configuration
Deliverables: - API validates tokens from OpenIddict - All endpoints working with new auth
Stage 4: Frontend Updates¶
Duration: 1-2 weeks
- Replace
@auth0/auth0-angularwith OIDC client library - Update login/logout flows
- Update token handling
- Update HTTP interceptor
- Update user profile management
- 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
- Export users from Auth0
- Import users to OpenIddict database
- Send password reset emails
- Monitor migration progress
- Handle edge cases
Deliverables: - All users migrated - Password reset emails sent - Login working for all users
Stage 6: Cutover and Decommission¶
Duration: 1 week
- Update DNS for
signin.syrf.org.uk - Switch production to OpenIddict
- Monitor for issues
- Keep Auth0 in read-only mode temporarily
- 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¶
- Functional Parity: All Auth0 features replicated in OpenIddict
- User Migration: 100% of active users migrated successfully
- Zero Downtime: Seamless transition with no service interruption
- Performance: Token issuance < 200ms, validation < 50ms
- Security: Pass security review, proper key management
- 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¶
- Review and approve this migration plan
- Answer clarifying questions
- Create identity service project structure
- Set up OpenIddict with MongoDB
- Implement basic authorization endpoints
- Configure AWS SES for email
- Begin frontend migration preparation