Skip to content

Auth0 Behaviour → Native Identity/OpenIddict Parity Gate

Decision and scope

This is a required pre-rehearsal workstream. The objective is not to reproduce Auth0 Actions line by line. ASP.NET Core Identity must own confirmation, reset, external-login and claims behaviour natively, while OpenIddict owns OAuth/OIDC. The objective is to preserve the underlying SyRF domain invariants and safe user-facing behaviour.

No preview or staging rehearsal may begin until the required items below are implemented, reviewed and covered by automated acceptance tests. This document does not authorise deployment, real-user migration, Auth0 changes, production traffic, live email, or live secret/configuration changes.

The checked-in Action files are behavioural evidence, not an authoritative copy of the live Auth0 tenant. PR #2639 records known source drift after incident patches and remains intentionally held. No checked-in Auth0 Rules or Hooks were found, so absence in this repository is not proof that the live tenant has none. A read-only tenant inventory remains an operator gate before any real migration.

Identity invariants

  1. InvestigatorId/SyrfUserId is the enduring SyRF person identifier. It must not change during authentication migration.
  2. The OAuth/OIDC sub is the immutable Identity account identifier. The namespaced user_id claim is the verified InvestigatorId. These identifiers serve different purposes and must not be substituted for one another.
  3. Only a verified, immutable Identity-to-Investigator mapping may emit namespaced user_id. Email and other provider claims are discovery hints only; they may never create or change the mapping.
  4. Missing, duplicate, or contradictory mappings fail closed before normal application access or token issuance. Operators receive restricted, privacy-preserving diagnostics; users receive non-enumerating errors.
  5. A new InvestigatorId may be allocated only as part of an authorised, transactional new-person registration flow. It must not be generated to resolve an existing-person conflict.
  6. External identities are linked only after proof of control and recent reauthentication. Matching email alone never links or merges accounts.
  7. At least one permitted recovery or alternative sign-in method remains after linking or unlinking.
  8. Return URLs are local/allowlisted. Forwarded scheme/host values are trusted only from configured proxies because they determine OAuth callbacks and email links behind ingress.

Current evidence

Primary code evidence:

Area Current implementation evidence State
Native account UI Account Razor Pages implement password login, logout, registration, forgot/reset/change password, email verification, TOTP/recovery codes and passkey management. Implemented foundations
Token claims UserClaimsService and AuthorizationController emit namespaced user_id, groups/roles, names, picture and email when present. AuthorizationController uses the Identity account ID for sub. Partial
Registration Register and AdminSignUp allocate a new SyrfUserId. Identity is configured with RequireUniqueEmail. Unsafe until allocation and uniqueness are transactional with the Investigator record
Token issuance AuthorizationController omits namespaced user_id when SyrfUserId is null. Gap: must fail closed
Userinfo/BFF resolution Userinfo independently rebuilds claims; BffAuthController passes user_id and sub to ApplicationService.ResolveInvestigatorIdAsync, whose GUID-sub fallback can create/select an Investigator from the Identity account ID. Gap: userinfo and the application boundary must share the same fail-closed mapping invariant
Mapping administration AdminApiController prevents replacing one user's different existing SyrfUserId. Gap: no proven global one-to-one constraint or ambiguity check
External login ExternalLogin looks up only provider plus provider key, rejects unknown/unmapped users, and validates local return URLs. Safe default, incomplete lifecycle
Confirmation Register, VerifyEmail, ForgotPassword and ResetPassword use native Identity tokens and SES integration. RequireConfirmedEmail is false. Mechanism implemented; admission policy unresolved
Profile completion Legacy Angular complete-profile-info exists. No native pre-token profile-completion gate exists. Gap
Migration Export parsing, reconciliation, dry-run/import/verify/readiness and durable campaign controls exist. Tooling implemented; preservation defects/gaps remain; no live run
Forwarded headers IdentityForwardedHeadersTests and startup/chart tests cover configured forwarding. Foundation implemented; ingress acceptance still required

Specific migration evidence requiring correction before rehearsal:

  • InvestigatorReconciler groups duplicate Investigator Auth0 IDs and takes the first record, rather than rejecting the ambiguous mapping.
  • Unmatched manifest entries can retain a null SyrfUserId and still be imported.
  • UserImporter hard-codes EmailConfirmed=true instead of preserving the manifest verification state.
  • MigrationManifestEntry carries one aggregate EmailVerified boolean and InvestigatorReconciler derives it with Any across grouped identities. It does not preserve which password/provider identity asserted verification.
  • UserImporter intentionally creates no password hash, requires password reset for password users, and sets TwoFactorEnabled=false. Forced reset is the approved password approach; MFA and passkeys require explicit re-enrolment.
  • The export model preserves core Auth0 identities, but does not currently model MFA enrolment/use or the full login-identities metadata. Preferred name is not written by UserImporter.

Auth0 behaviour mapping

Auth0 artefact Underlying behaviour Native owner Current state Required closure
transform-token.js (deployed) Stable SyRF ID, conflict checks and namespaced claims Verified mapping service plus UserClaimsService/OpenIddict issuance Claims mostly implemented; identities claim absent; missing mapping does not block issuance Enforce one-to-one mapping, fail closed, cover conflicts, and decide the compatibility shape/lifetime of identities
check-email-verification.js (deployed) Verification delivery and user routing without account enumeration ASP.NET Core Identity confirmation/resend plus constrained application UX Confirmation/reset mechanics exist; global confirmation is not required Preserve imported state and implement the approved admission policy
upgrade-social-to-auth0-user.js (deployed) External identity lifecycle, account choice, link notification and metadata ASP.NET Core Identity external logins and account management Existing provider-key sign-in only Implement safe create/link/unlink/recovery/outage/notification flows
request-extra-profile-info.js (deployed) Require names before ordinary application use Native Identity profile-completion page/gate Missing Add completion gate before normal token/application use
link-accounts.js (empty, undeployed) None None No behaviour No replacement is required

Lifecycle and conflict matrix

All failure responses in this table must be non-enumerating. Detailed conflict information is restricted to privacy-safe operator evidence.

Entry point Existing safe behaviour Required behaviour before rehearsal
New password registration Checks Identity email and creates a local user with a new SyrfUserId Atomically create the new Investigator and immutable mapping; reject duplicate email, duplicate Investigator, mapping collision and concurrent registration; never attach an existing Investigator by email
Password login Native password/lockout/2FA/passkey path Resolve exactly one immutable mapping before normal access; block missing or ambiguous mappings; apply confirmation/profile gates without revealing whether another account exists
Known external login Provider-key lookup signs in an already-linked mapped user Preserve this path; re-check mapping, confirmation/profile state, provider status and recovery-method invariant
New external login Unknown provider-key is rejected Offer explicit create-new-account or sign-in-to-link choices; creation follows the same new-Investigator transaction as password registration
Same-email local account Email is not currently used to link Keep email-only auto-link forbidden; require sign-in plus recent reauthentication/proof of control before AddLogin
Authenticated linking No account-management flow Reauthenticate, validate provider callback/state, check provider-key uniqueness and mapping consistency, link atomically, record audit metadata and send a security notification
Unlinking/recovery No flow Require recent reauthentication; refuse removal of the last permitted recovery/sign-in method; notify the user; offer support recovery without account enumeration
Provider outage/error Remote error is shown inline Preserve local password/passkey/recovery alternatives for an OAuth error callback, upstream HTTP failure or callback timeout; show a safe retry path and avoid exposing provider or account-match details
Migration import Auth0 rows are grouped by email; Investigator is reconciled by Auth0 ID Fail closed on duplicate email groups, duplicate Auth0 IDs, duplicate Investigator IDs, contradictory provider keys, missing stable mappings and unknown providers; never use email to manufacture a link
Token issuance/userinfo/BFF callback Claims are emitted from ApplicationUser; missing SyrfUserId is omitted; email_verified is added only by Userinfo; the BFF resolver can fall back from user_id to GUID-shaped sub Require one verified, immutable mapping and required profile/confirmation state; reject ambiguous/missing mapping before authorization code, token, userinfo or BFF account resolution; never treat Identity sub as InvestigatorId

Claims contract

The current native claim mapping covers:

  • https://claims.syrf.org.uk/user_id from the verified SyrfUserId;
  • https://claims.syrf.org.uk/syrf_groups and role;
  • names, preferred name, picture and email;
  • standard sub from the Identity account ID; and
  • standard email_verified from Identity confirmation state in userinfo only.

The authorization and refresh principals do not currently add email_verified, and the claim destination logic has no email_verified case. Token-path parity is therefore a gap, not an implemented capability.

Before rehearsal:

  1. Claims must be produced by one tested service for authorization code, refresh, ID token, access token and /connect/userinfo paths.
  2. A normal end-user token without a verified SyrfUserId is forbidden.
  3. Group/role and name claims must reflect the preserved authoritative records, not untrusted external claims.
  4. The current Angular consumer of https://claims.syrf.org.uk/identities needs an explicit compatibility decision: either emit a bounded native representation temporarily or migrate the account UI to an authenticated account-management endpoint.
  5. Tests must prove that an unverified email or altered provider claim cannot create, replace, or select an Investigator mapping.
  6. BffAuthController and ApplicationService.ResolveInvestigatorIdAsync must require the verified user_id mapping. Missing/invalid user_id must not fall back to GUID-shaped sub or create an Investigator implicitly.
  7. Token-destination tests must prove email_verified is present with the correct boolean wherever the approved client contract requires it, as well as in userinfo.

Email confirmation and resend matrix

RequireConfirmedEmail must not be enabled globally for the existing population until imported state and a staged confirmation campaign prevent lockout.

User cohort State to preserve Proposed launch behaviour requiring product approval
New password user Starts unconfirmed Confirm before normal application use; allow only confirmation/resend/sign-out/support UX
Migrated confirmed user Auth0 email_verified=true Import as confirmed after provenance validation
Migrated unconfirmed user Auth0 email_verified=false Do not mark confirmed; admit only to constrained confirmation/resend/support UX initially rather than creating a surprise hard lockout
Password-reset user Existing confirmation state Reset changes the credential only; it does not implicitly confirm email unless separately approved
External-provider user Provider email and verification claim are provider assertions Preserve source state; decide which provider/claim assurance may confirm the local address; never use it to auto-link

Resend must be non-enumerating, rate-limited, idempotent, auditable without email addresses in public logs, and use a local/allowlisted return path. Preview and staging tests use an email sink and synthetic addresses only.

Profile completion

Migrated and external users missing required names must complete a native Identity page before normal token issuance/application use. The page must:

  • state the required fields without exposing migration/account-match details;
  • validate and store canonical given, family and preferred names;
  • use a short-lived, one-account continuation bound to the original local return URL;
  • be safe to retry and resume; and
  • prevent external claims from overwriting user-approved profile values on later sign-ins.

Migration preservation matrix

Datum Required handling Current state
SyrfUserId/InvestigatorId Preserve exactly; one-to-one verified mapping; no email fallback Reconciled from Investigator Auth0 ID, but ambiguity and null import are not fail-closed
OIDC sub New immutable Identity account ID; never replace InvestigatorId Implemented architecture
External provider links Preserve provider plus immutable provider key; validate uniqueness Google link import exists; full lifecycle/conflict verification incomplete
Roles/groups Preserve from the authoritative Investigator record Imported as SyrfGroups
Email verification Preserve each source identity's provider, immutable provider subject, asserted email_verified value and export observation time; derive local confirmation only under the approved assurance policy Manifest currently collapses grouped identities to one Any-derived boolean; importer then incorrectly forces true
Blocked/deactivated state Preserve Auth0 blocked and Investigator Deactivated as indefinite native lockout until an authorised operator changes the authoritative source state Reconciler/importer carry both states into LockoutEnd, but the parity fixtures do not yet make this an explicit gate
Profile metadata Preserve canonical names/preferred name; provider pictures remain non-authoritative Partial; preferred name and richer identity metadata are gaps
Password Do not export Auth0 hashes; forced reset Implemented migration intent
MFA Do not claim portability; aggregate read-only inventory and target optional re-enrolment/recovery communications Import disables 2FA; inventory/enrolment-use evidence is external
Passkeys/WebAuthn Biometrics remain on device; no credential import; require secure re-enrolment and alternative recovery Native capability exists; migration state/communication remains
Password-reset state Mark password cohorts for forced reset; campaign is durable and idempotent Implemented foundations; no live campaign

No real export may be taken until the user approves the proposed restricted retention/access schedule. Public evidence remains aggregate and redacted. The user is the sole human operator/approver; no additional access is inferred.

Focused implementation PRs

These are vertical, independently reviewable PR boundaries. Each PR must include its automated tests and must merge before the rehearsal PR.

PR A — Identity mapping and issuance invariant

  • Introduce one authoritative verified Identity-to-Investigator mapping service.
  • Enforce global one-to-one and provider-key uniqueness, including concurrency.
  • Make new-person allocation transactional with Investigator creation.
  • Make password/external login, import and token issuance fail closed on missing or contradictory mappings.
  • Apply the same fail-closed rule to /connect/userinfo, BffAuthController and ApplicationService.ResolveInvestigatorIdAsync; remove the GUID-sub-to-Investigator fallback.
  • Preserve Identity account ID as sub and stable InvestigatorId as namespaced user_id.
  • Emit email_verified consistently under the approved token/userinfo contract.

Acceptance: tests cover duplicate email, duplicate Investigator, duplicate provider key, concurrent registration, unlinked/null mapping, attempted email fallback, altered external claims, authorization code issuance, refresh, /connect/userinfo and the BFF callback/application resolver. Tests prove a GUID-shaped Identity sub can never create or select an Investigator.

PR A2 — Identity display compatibility (decision-controlled)

This companion is separate from PR A's mapping invariant. Before it starts, the user chooses either:

  • a temporary bounded identities claim containing only the fields the current Angular account UI requires, with an explicit deprecation/removal gate; or
  • an authenticated native account-management endpoint plus Angular migration.

If no decision exists, PR A can still merge, but A2 remains a mandatory R17 pre-rehearsal item. Neither option may expose provider credentials or use an email/provider claim to create a link.

Acceptance: account-management UI tests cover the selected contract, linked and unlinked states, multiple providers, missing metadata and non-enumerating failure. The unselected compatibility path is absent.

PR B — Confirmation and profile-completion gates

  • Preserve imported verification state.
  • Implement the approved new/migrated/password/external admission matrix.
  • Add non-enumerating, throttled resend and constrained verification UX.
  • Add native required-profile completion before normal access.
  • Keep return URLs local/allowlisted and provider claims non-authoritative.

Acceptance: tests cover every cohort in the confirmation matrix, resend enumeration/throttling/idempotency, reset-not-confirm behavior, incomplete profiles, continuation replay and forwarded scheme/host handling.

PR C — External identity lifecycle

  • Add explicit create-new versus sign-in-to-link choice.
  • Require step-up reauthentication no more than five minutes before linking or unlinking. A long-lived BFF session alone is insufficient: password or passkey proves the primary factor and an MFA-enabled account must also complete its configured second factor. Expired evidence returns to a local allowlisted continuation after a fresh step-up.
  • Implement provider-key uniqueness, safe unlinking and last-recovery-method protection.
  • Send security notifications after link/unlink.
  • Preserve local password/passkey/recovery paths during provider outage.
  • Rotate the Identity security stamp and session generation after link, unlink or support recovery; revoke refresh tokens and all BFF sessions, and reject outstanding access tokens through the chosen revocation/version mechanism.

Acceptance: integration/E2E tests cover new external user, known linked user, same-email local account, authenticated linking, unauthenticated/email-only linking rejection, five-minute step-up expiry and acceptable factors, unlink/recovery, OAuth error callback, upstream HTTP failure, callback timeout, session/refresh/access-token invalidation, notification and safe return URLs.

PR D — Migration parity and readiness

  • Reject ambiguous/unmapped rows and unknown provider states.
  • Preserve SyrfUserId, provider links, roles/groups, names, per-source verification provenance, Auth0 blocked state and Investigator Deactivated state exactly.
  • Retain forced password reset.
  • Represent MFA/passkey re-enrolment explicitly without pretending credentials were migrated.
  • Extend verify/readiness evidence for all preserved fields and conflict cases.

Acceptance: fixture-based import/verify/readiness tests cover every row in the migration matrix, including mixed-verification merged accounts and independently blocked/deactivated accounts. Reruns are idempotent, no secret or user identity appears in argv/GitOps/public evidence, and rollback leaves the source/Auth0 path unchanged.

Dependencies: PR A establishes the mapping contract first. PRs B and C can then proceed in parallel. PR A2 waits for the identities-contract decision. PR D may proceed in parallel after A, but its confirmation and external-state schema must follow the approved outcomes of B and C. The rehearsal is serial after A–D and A2 are merged and all acceptance suites are green.

Isolated synthetic rehearsal

The BFF synthetic rehearsal is a separate SyRF plus cluster-gitops change after PRs A–D, A2 and the Identity-only S08 dark launch:

  1. Pin immutable, validated API and Identity artifacts.
  2. Add an isolated preview/staging namespace using established SyRF chart/package ownership and cluster-gitops Argo/Helm/Kustomize/ExternalSecret/operator patterns only. Do not introduce Terraform or manual cloud configuration.
  3. Use a dedicated Identity database, Redis namespace, secret references, encryption keys, OAuth clients and allowlisted callbacks.
  4. Use synthetic identities and an email sink. Do not send real-user email or copy production/Auth0 user data.
  5. Keep Auth0 live and the default browser/API authentication path. Expose the rehearsal only through an explicit restricted test route/client.
  6. Run the full matrix for password, confirmation/resend, forced reset, passkeys, optional MFA, external login/link/unlink, profile completion, claims, API/BFF/Swagger authorization, sessions, SignalR and rollback.
  7. Test trusted and untrusted X-Forwarded-Proto/X-Forwarded-Host cases through the actual ingress; assert generated callback and email URLs.
  8. Roll back by removing only the isolated Argo application/revision and its synthetic resources. Auth0, production traffic, live users and live data remain unchanged.

Real Google-provider validation requires a later, explicit sole-operator action to provision/approve a non-production client and exact callback. Secret values must never be committed or included in public evidence.

Remaining product decisions

These decisions are required before the affected PR can be final:

  1. Approve or amend the proposed confirmation/admission matrix, especially the constrained UX for migrated unverified users.
  2. Decide whether and under which provider assurance a verified external email may confirm the local email. This never authorises email-only account linking.
  3. Decide whether successful password reset confirms email; recommendation: no.
  4. Define the required profile fields and whether any cohort may defer them.
  5. Choose temporary identities-claim compatibility or migration to a native authenticated account-management endpoint.
  6. Define who may authorise new Investigator creation during external signup.
  7. Approve link/unlink notification and support-recovery wording/channels.

Already settled: forced password reset; optional non-mandatory MFA; passkeys at launch with recovery; preservation of social providers/links; stable InvestigatorId; OpenIddict-compatible Swagger OAuth; Auth0 Actions PR #2639 held; GitOps/operator delivery only; and no replacement for the empty undeployed Link Accounts Action.