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.DryRunproduces 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
Issueswith 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
- Dry-run the importer against a production export; triage
Issues. - Import for real into a staging realm; spot-check with the authz inspector.
- Deploy shadow mode; run days-to-weeks until divergence flatlines at zero.
- Rotate the flagged client secrets from the import report.
- Flip — Sentinel decides, legacy in shadow if you want the reverse safety net.
- 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.