Reference

Migration & importers

Importers for ASP.NET Core Identity, Keycloak, Auth0 and Duende; foreign password hashes that verify from day one with rehash-on-login; and shadow-mode authorization that gates cutover on zero divergence.

Nobody adopts an identity platform on an empty database. Sentinel’s migration story has three legs: importers that read your current system’s export, foreign-hash coexistence so nobody resets a password, and shadow mode so you cut over on evidence instead of hope.

The importers

Nuvora.Nexus.Sentinel.Importers ships four, matching the systems Sentinel most often replaces:

Importer Reads Notes
AspNetIdentityImporter ASP.NET Core Identity tables via the IAspNetIdentitySource port users, roles, role assignments, claims; hashes tagged aspnet-identity-v3
KeycloakRealmImporter Keycloak realm-export JSON users + credentials, groups, realm roles, clients; pbkdf2 and bcrypt credentials mapped
Auth0Importer Auth0 bulk-export ndjson one user per line; bcrypt hashes imported verbatim
DuendeConfigImporter Duende IdentityServer client config client registry mapping; non-portable secrets flagged for rotation

All four share one model: ImportAsync(source, ImportOptions, ct)ImportReport { Users, Credentials, Roles, Groups, Clients, Issues, GeneratedClientSecrets }.

The behaviors that matter in practice — each pinned by a test:

  • Dry-run first. ImportOptions.DryRun produces the full report with zero writes (Reimport_and_dry_run_behave).
  • Re-import is idempotent. Run the importer again after a delta export; existing rows update instead of duplicating (Reimport_is_idempotent).
  • Issues are data, not exceptions. Unmappable rows land in Issues with a severity, and the import continues — you triage a report, not a stack trace.
  • Non-portable secrets trigger rotation. A Duende SHA-256 client secret can’t be verified by Sentinel’s hasher, so the importer generates a replacement and reports it (Sha256_secret_triggers_rotation_on_migration) — the migration produces a rotation checklist instead of silently broken clients.
  • Imported roles are prefixed (imported:) so they never collide with your declared catalog.

Foreign hashes: nobody resets a password

Every credential row is tagged with its algorithm. Bcrypt ($2a$/$2b$), PBKDF2 variants and ASP.NET Identity v3 hashes verify natively on day one — and on each successful login, PasswordHasher.VerifyAndUpgrade transparently rehashes the credential to Sentinel’s current default (argon2id). Your imported hash population ages out one login at a time (Pbkdf2_credential_maps_to_phc_format_and_verifies), with no reset-password email blast and no flag-day.

Shadow-mode authorization

The riskiest migration step isn’t moving the users — it’s trusting a new engine’s answers. Shadow mode makes that step empirical. Keep your legacy authorization authoritative, and mirror every decision into Sentinel:

private readonly ShadowAuthzRecorder _shadow = new(eventSink);

bool Authorize(User user, Resource resource)
{
    var legacy = _legacyAuthz.Can(user, "records.read", resource);

    // Evaluates the Sentinel snapshot, counts agreement/divergence,
    // emits authz.shadow_divergence on mismatch — and ALWAYS returns
    // the legacy decision. Sentinel is observing, not deciding.
    return _shadow.Compare(legacy, sentinelSnapshot, new AccessCheck(
        PermissionId.Parse("records:org:read"),
        resourceOrganizationId: resource.OrgId));
}

Each divergence emits authz.shadow_divergence with both verdicts — wire it to a webhook and every mismatch becomes a reviewable finding: a grant you haven’t modeled yet, or a bug the legacy system had that Sentinel doesn’t.

Cutover is a gate, not a feeling:

var report = _shadow.Report();
// ReadyForCutover == Divergences == 0 && Total > 0
if (report.ReadyForCutover) FlipToSentinel();

Zero divergences over an actually-nonempty sample — the recorder refuses to declare victory on no data, and its counters are thread-safe under production traffic (Cutover_gate_opens_on_zero_divergences_over_a_nonempty_sample, Counters_are_thread_safe).

A migration, end to end

  1. Dry-run the importer against a production export; triage Issues.
  2. Import for real into a staging realm; spot-check with the authz inspector.
  3. Deploy shadow mode; run days-to-weeks until divergence flatlines at zero.
  4. Rotate the flagged client secrets from the import report.
  5. Flip — Sentinel decides, legacy in shadow if you want the reverse safety net.
  6. Decommission, and let rehash-on-login retire the foreign hashes.

Article 008 — Shadow-mode migration runs steps 3–5 against a real divergence and shows the event payloads.