Skip to content

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:

  1. If auth0| user with ?linkSocial query param: Links the social identity to the existing auth0 account. Sends "login added" email.
  2. If auth0| user (normal login): Checks for conflicting accounts. Syncs identity metadata (loginIdentities in app_metadata tracking provider, connected_at, picture per identity).
  3. If social user (first-time Google login):
  4. Checks for existing matching accounts
  5. Redirects to /auth/external-account page in Angular
  6. 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.


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

  1. 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.

  2. 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.

  3. 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 (AspNetUserLogins collection). This is simpler but means the migration (Phase 4) needs to correctly create the login links.

  4. 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_id is 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.