Auth0 Custom Actions → OpenIddict Migration Mapping¶
Overview¶
Auth0 uses a post-login Actions pipeline — a chain of JavaScript functions that run after authentication but before the token is issued. SyRF has 5 custom actions (4 deployed). OpenIddict doesn't have an "actions pipeline" — the equivalent logic lives in:
- AuthorizationController (consent, authorization code exchange)
- IOpenIddictServerHandler implementations (token generation events)
- UserClaimsService (custom claims — already implemented)
- Account Razor Pages (user-facing redirects for profile completion, email verification)
- ASP.NET Core Identity events (post-sign-in hooks)
Action-by-Action Mapping¶
1. Transform Token (CRITICAL — deployed)¶
What it does in Auth0:
- Looks up the user's email in the SyRF API (/api/account/email-lookup) to find matching investigator IDs
- Blocks login if conflicting Auth0 accounts or multiple investigators share the same email
- Blocks login if the account is a bare social account (must be linked to an auth0| account)
- Generates or retrieves syrf_id (maps Auth0 user → pmInvestigator UUID)
- Sets custom claims on access token and ID token:
- https://claims.syrf.org.uk/email
- https://claims.syrf.org.uk/user_id (the syrf_id, NOT the Auth0 user_id)
- https://claims.syrf.org.uk/syrf_groups
- https://claims.syrf.org.uk/preferred_name
- https://claims.syrf.org.uk/given_name
- https://claims.syrf.org.uk/family_name
- https://claims.syrf.org.uk/picture
- https://claims.syrf.org.uk/identities (on ID token only)
OpenIddict equivalent:
- UserClaimsService — ✅ Already implemented. Adds all SyRF namespace claims from ApplicationUser fields.
- Conflict detection — Needs implementation. In Auth0, this calls back to the SyRF API during login. In OpenIddict, the Identity Service can query the pmInvestigator collection directly (it has MongoDB access) or call the API's email-lookup endpoint.
- syrf_id generation — Needs implementation. The Auth0 action generates a UUID and stores it in app_metadata.syrf_id. In OpenIddict, this maps to ApplicationUser.SyrfUserId. During user import (Phase 4), this will be populated from Auth0 export data. For new users, it should be generated at registration time.
- Social-only account blocking — Not needed. OpenIddict's external login flow (Google) always creates or links to a local ApplicationUser. There's no concept of a bare social account without a local identity.
Phase: Partially done (Phase 1). Conflict detection and syrf_id generation needed in Phase 2.
Implementation location: UserClaimsService.AddSyrfClaimsAsync() + new conflict check in AuthorizationController
2. Check Email Verification (CRITICAL — deployed)¶
What it does in Auth0:
- Runs after login for auth0| users whose email is not verified
- Uses Auth0 Management API to generate an email verification ticket URL
- Sends verification email via AWS SES (using WelcomeEmail or EmailConfirmation template)
- Tracks whether welcome email was already sent (app_metadata.welcome_email_sent)
- Redirects user to /auth/welcome-email-sent or /auth/confirmation-email-sent
- Does NOT block login — just redirects
OpenIddict equivalent:
- Email verification — ASP.NET Core Identity has built-in email confirmation via UserManager.GenerateEmailConfirmationTokenAsync() and UserManager.ConfirmEmailAsync().
- SES email sending — ✅ Already implemented in AwsIdentityEmailService.
- Welcome vs confirmation flow — Can be handled in the Login Razor page or a post-login middleware. Check if EmailConfirmed == false, generate token, send email, redirect.
- Template emails — The SES templates (WelcomeEmail, EmailConfirmation, LoginAdded) exist in AWS SES. The Identity Service can use the same templates.
Phase: Phase 2 (#2428 — Register and Password Reset pages)
Implementation location: Login.cshtml.cs OnPostAsync() — after successful sign-in, check email verification status. If unverified, send email and redirect.
3. Upgrade Social to Auth0 User (COMPLEX — deployed)¶
What it does in Auth0: This is the most complex action. It handles the flow when a user logs in with Google:
- If auth0| user with
?linkSocialquery param: Links the social identity to the existing auth0 account. Sends "login added" email. - If auth0| user (normal login): Checks for conflicting accounts. Syncs identity metadata (
loginIdentitiesin app_metadata tracking provider, connected_at, picture per identity). - If social user (first-time Google login):
- Checks for existing matching accounts
- Redirects to
/auth/external-accountpage in Angular - On return (
onContinuePostLogin): creates a new auth0| user, links the social identity to it, sends welcome + verification emails, redirects to welcome page
OpenIddict equivalent:
- External login flow — ✅ Partially implemented in ExternalLogin.cshtml.cs. ASP.NET Core Identity handles the Google callback, checks for existing linked accounts via UserManager.FindByLoginAsync(), and can create new users with UserManager.CreateAsync() + UserManager.AddLoginAsync().
- The key difference: In Auth0, a social login creates a separate social user that must be explicitly linked to an auth0| user. In OpenIddict + ASP.NET Identity, external logins (Google) are always linked to a local ApplicationUser — the linking is built into the framework. A user either:
- Has an existing account → FindByLoginAsync matches → sign in
- Has an account with same email → prompt to link → AddLoginAsync
- Is new → create local account + link → CreateAsync + AddLoginAsync
- Identity metadata tracking — The loginIdentities metadata (provider, connected_at, picture) can be stored on ApplicationUser as a JSON property or in a separate collection. Lower priority — mainly used for the UI's identity list display.
- Emails — Same SES templates, called from the external login handler.
Phase: Phase 2 (ExternalLogin page enhancement) + Phase 4 (migration of linked identities)
Implementation location: ExternalLogin.cshtml.cs — enhance the existing page model to handle:
- First-time Google users: create local account, link, send welcome email
- Returning Google users: find linked account, sign in
- Email conflicts: prompt to link or create new
4. Request Extra Profile Info (MODERATE — deployed)¶
What it does in Auth0:
- After login, checks if an auth0| user is missing family_name, given_name, or nickname
- If missing, redirects to /auth/complete-profile-info in Angular
- On return (onContinuePostLogin): updates the Auth0 user profile with the provided names
OpenIddict equivalent:
- Profile completion redirect — Can be handled in the Identity Service's login flow. After sign-in, check if ApplicationUser has null FirstName/LastName/PreferredName. If so, redirect to a profile completion page.
- Or: Handle this in the Angular app post-login. The BFF /api/auth/me endpoint returns the user's profile. If fields are missing, Angular shows the completion form and calls a profile update endpoint.
Phase: Phase 2
Implementation location: Either:
- Option A: Identity Service Login.cshtml.cs — post-sign-in check → redirect to Profile completion Razor page
- Option B: Angular — BFF auth/me returns profile, Angular checks for missing fields → shows completion dialog
Option A is cleaner because it happens before token issuance, matching the Auth0 behaviour.
5. Link Accounts (NOT DEPLOYED — empty)¶
What it does: Nothing. The handler is empty. Not deployed.
OpenIddict equivalent: Not needed. Skip entirely.
Summary Table¶
| Auth0 Action | Status | OpenIddict Equivalent | Already Done | Phase |
|---|---|---|---|---|
| Transform Token | ✅ Deployed | UserClaimsService + conflict check | Claims: ✅ Conflict: ❌ | 1 (partial) + 2 |
| Check Email Verification | ✅ Deployed | Login page + SES email | SES service: ✅ Flow: ❌ | 2 |
| Upgrade Social to Auth0 | ✅ Deployed | ExternalLogin page + Identity linking | Page exists: ✅ Full flow: ❌ | 2 + 4 |
| Request Extra Profile Info | ✅ Deployed | Login page profile completion check | ❌ | 2 |
| Link Accounts | ❌ Not deployed | N/A | N/A | Skip |
Key Architectural Differences¶
-
No post-login pipeline in OpenIddict. Logic that runs "after auth but before token" goes in the AuthorizationController's authorize/token endpoints, or in custom
IOpenIddictServerHandler<ProcessSignInContext>handlers. -
No API callback during login. The Auth0 actions call back to the SyRF API (
/api/account/email-lookup) during every login to check for conflicts. The Identity Service can either query MongoDB directly (it has the connection string) or expose its own internal endpoint. Querying directly is simpler and avoids circular dependencies. -
External login is fundamentally different. Auth0 creates separate user records per provider and links them. ASP.NET Identity creates one local user with linked external logins (
AspNetUserLoginscollection). This is simpler but means the migration (Phase 4) needs to correctly create the login links. -
Redirects during login. Auth0 actions use
api.redirect.sendUserTo()to pause the login flow and redirect to Angular pages. OpenIddict can do the same via the AuthorizationController — return a redirect during the authorize flow and resume after. But it's cleaner to handle it in Razor pages within the Identity Service itself (email verification, profile completion).
Risks¶
- Conflict detection gap: During Phase 2-3 parallel auth, both Auth0 and OpenIddict process logins. The conflict detection logic must work the same way or users could be blocked on one path but not the other.
- syrf_id consistency: The
syrf_idis the critical mapping between Auth0 users and pmInvestigator records. If this is generated differently in OpenIddict, data integrity breaks. The Phase 4 import must preserve these exactly. - SES email templates: The identity service uses the same SES templates (WelcomeEmail, EmailConfirmation, LoginAdded). These must be deployed in the SES region the Identity Service uses.