API reference — Migration & configuration
Nuvora.Nexus.Sentinel.Importers
Migration importer suite for Sentinel: ASP.NET Core Identity, Keycloak realm-export, Auth0 bulk export and Duende client-config importers, foreign-hash coexistence (bcrypt), and shadow-mode authorization for cutover gating. Library-only — callable from CLIs and hosts, no web framework.
dotnet add package Nuvora.Nexus.Sentinel.Importers
Nuvora.Nexus.Sentinel.Importers
AspNetIdentityImporter
public sealed class AspNetIdentityImporter
ASP.NET Core Identity importer. Users map by email; password hashes carry over verbatim tagged aspnet-identity-v3 and verify via AspNetIdentityV3PasswordHashAlgorithm until rehash-on-login; active lockouts become Suspended; roles become realm-level Sentinel roles under the imported: prefix; claims land in the user’s ABAC attribute bag.
Constructors
AspNetIdentityImporter(IImportTarget target, ISentinelClock? clock = null)
ASP.NET Core Identity importer. Users map by email; password hashes carry over verbatim tagged aspnet-identity-v3 and verify via AspNetIdentityV3PasswordHashAlgorithm until rehash-on-login; active lockouts become Suspended; roles become realm-level Sentinel roles under the imported: prefix; claims land in the user’s ABAC attribute bag.
Methods
Task<ImportReport> ImportAsync(IAspNetIdentitySource source, ImportOptions options, CancellationToken cancellationToken = default(CancellationToken))
AspNetRoleRow
public sealed record AspNetRoleRow : IEquatable<AspNetRoleRow>
Defines a generalized method that a value type or class implements to create a type-specific method for determining equality of instances.
Constructors
AspNetRoleRow(string Id, string? Name)
Properties
string Id { get; init; }
string? Name { get; init; }
AspNetUserClaimRow
public sealed record AspNetUserClaimRow : IEquatable<AspNetUserClaimRow>
Defines a generalized method that a value type or class implements to create a type-specific method for determining equality of instances.
Constructors
AspNetUserClaimRow(string UserId, string? ClaimType, string? ClaimValue)
Properties
string UserId { get; init; }
string? ClaimType { get; init; }
string? ClaimValue { get; init; }
AspNetUserRoleRow
public sealed record AspNetUserRoleRow : IEquatable<AspNetUserRoleRow>
Defines a generalized method that a value type or class implements to create a type-specific method for determining equality of instances.
Constructors
AspNetUserRoleRow(string UserId, string RoleId)
Properties
string RoleId { get; init; }
string UserId { get; init; }
AspNetUserRow
public sealed record AspNetUserRow : IEquatable<AspNetUserRow>
Defines a generalized method that a value type or class implements to create a type-specific method for determining equality of instances.
Constructors
AspNetUserRow(string Id, string? UserName, string? Email, bool EmailConfirmed, string? PasswordHash, bool LockoutEnabled = false, DateTimeOffset? LockoutEnd = null)
Properties
DateTimeOffset? LockoutEnd { get; init; }
bool EmailConfirmed { get; init; }
bool LockoutEnabled { get; init; }
string Id { get; init; }
string? Email { get; init; }
string? PasswordHash { get; init; }
string? UserName { get; init; }
Auth0Importer
public sealed class Auth0Importer
Auth0 bulk-export importer: one JSON user per line (ndjson). Deliberately minimal, honest scope: users, email_verified, and bcrypt password hashes — via the import-format custom_password_hash shape ({"algorithm":"bcrypt","hash":{"value":…}}) or the export-format top-level passwordHash ($2a$/$2b$). Roles/permissions exports are not consumed in v1. Non-bcrypt hashes are recorded as issues and the user imports password-less.
Constructors
Auth0Importer(IImportTarget target)
Auth0 bulk-export importer: one JSON user per line (ndjson). Deliberately minimal, honest scope: users, email_verified, and bcrypt password hashes — via the import-format custom_password_hash shape ({"algorithm":"bcrypt","hash":{"value":…}}) or the export-format top-level passwordHash ($2a$/$2b$). Roles/permissions exports are not consumed in v1. Non-bcrypt hashes are recorded as issues and the user imports password-less.
Methods
Task<ImportReport> ImportAsync(string ndjson, ImportOptions options, CancellationToken cancellationToken = default(CancellationToken))
BcryptPasswordHashAlgorithm
public sealed class BcryptPasswordHashAlgorithm : IPasswordHashAlgorithm
Verify-only bcrypt for imported credentials: Keycloak bcrypt credentials and Auth0 exports carry $2a$/$2b$/$2y$ strings, which verify here until the owner’s next login rehashes them to argon2id. Lives in the Importers package (not Core) so the BCrypt.Net-Next dependency stays out of Core; register it into PasswordHasher’s legacy list — see ImporterHashAlgorithms.
Properties
string Name { get; }
Stable machine name stored on the credential row (e.g. argon2id).
Methods
bool Verify(string password, string encodedHash)
string Hash(string password)
Import-only: Sentinel never mints new bcrypt hashes.
Fields
const string AlgorithmName = "bcrypt"
DuendeConfigImporter
public sealed class DuendeConfigImporter
Duende/IdentityServer client-configuration importer: maps a JSON array of Client config objects (PascalCase or camelCase) onto Sentinel OidcClients.
Secrets are rotation-on-migration by necessity: Duende persists sha256(secret), which cannot be converted into Sentinel’s own hash format, so each confidential client gets a freshly generated secret — hashed with the current hasher, with the plaintext surfaced once in ImportReport.GeneratedClientSecrets and the client flagged ImportedClient.SecretResetRequired. Note that this means every import run rotates imported-client secrets again.
Constructors
DuendeConfigImporter(IImportTarget target, PasswordHasher hasher)
Duende/IdentityServer client-configuration importer: maps a JSON array of Client config objects (PascalCase or camelCase) onto Sentinel OidcClients. Secrets are rotation-on-migration by necessity: Duende persists sha256(secret), which cannot be converted into Sentinel’s own hash format, so each confidential client gets a freshly generated secret — hashed with the current hasher, with the plaintext surfaced once in ImportReport.GeneratedClientSecrets and the client flagged ImportedClient.SecretResetRequired. Note that this means every import run rotates imported-client secrets again.
Methods
Task<ImportReport> ImportAsync(string clientsJson, ImportOptions options, CancellationToken cancellationToken = default(CancellationToken))
IAspNetIdentitySource
public interface IAspNetIdentitySource
Row-level view of the standard ASP.NET Core Identity schema (AspNetUsers / AspNetRoles / AspNetUserRoles / AspNetUserClaims). An ADO-free seam on purpose: a CLI wraps a DbDataReader over the source database in this; tests feed lists. Only the columns the import consumes appear here.
Properties
IEnumerable<AspNetRoleRow> Roles { get; }
IEnumerable<AspNetUserClaimRow> UserClaims { get; }
IEnumerable<AspNetUserRoleRow> UserRoles { get; }
IEnumerable<AspNetUserRow> Users { get; }
IImportTarget
public interface IImportTarget
Where importer output lands. One bulk operation, deliberately not the per-entity login-store ports: imports write users, credentials, roles, assignments, groups, memberships and clients together, idempotently by natural key — re-running an import updates rather than duplicates. The EF Core adapter ships EfImportTarget; tests or exotic hosts can implement their own.
Methods
ValueTask ApplyAsync(ImportBatch batch, ImportOptions options, ImportReport report, CancellationToken cancellationToken = default(CancellationToken))
Upserts the batch into ImportOptions.TargetRealmId, adding per-entity created/updated/skipped counts to report. Under ImportOptions.DryRun the same matching and counting runs but nothing is persisted.
ImportBatch
public sealed class ImportBatch
The normalized output of one importer run, keyed entirely by natural keys (email, role/group key, client_id) so the IImportTarget can upsert idempotently. Importers translate their source format into this; they never touch storage directly.
Properties
List<ImportedClient> Clients { get; }
List<ImportedGroup> Groups { get; }
List<ImportedRole> Roles { get; }
Realm-level roles; keys carry the imported: prefix (see ImportKeys).
List<ImportedUser> Users { get; }
ImportCounts
public sealed class ImportCounts
Properties
int Created { get; set; }
int Skipped { get; set; }
int Total { get; }
int Updated { get; set; }
ImportIssue
public sealed record ImportIssue : IEquatable<ImportIssue>
One per-item finding: an entity that could not be fully imported, or a mapping caveat.
Constructors
ImportIssue(ImportIssueSeverity Severity, string Entity, string Key, string Message)
One per-item finding: an entity that could not be fully imported, or a mapping caveat.
Properties
ImportIssueSeverity Severity { get; init; }
string Entity { get; init; }
string Key { get; init; }
string Message { get; init; }
ImportIssueSeverity
public enum ImportIssueSeverity
Provides the base class for enumerations.
Values
Info— Imported, with a caveat the operator should know about.Warning— Partially imported (e.g. user imported password-less).Error— Not imported.
ImportKeys
public static class ImportKeys
Key normalization shared by all importers, so re-imports and cross-importer runs agree.
Methods
static string GroupKey(string sourceName)
static string RoleKey(string sourceName)
Fields
const string RolePrefix = "imported:"
Imported roles are namespaced (imported:) so they can never collide with built-ins.
ImportOptions
public sealed class ImportOptions
Options every importer honors. ImportOptions.DryRun produces the complete report — same parsing, same natural-key matching, same counts — without persisting anything, so operators can rehearse a migration against the live target.
Properties
OrgMappingStrategy OrgMapping { get; init; }
bool DryRun { get; init; }
Full report, zero writes.
required Guid TargetRealmId { get; init; }
The Sentinel realm everything lands in. The realm itself is not created by imports.
ImportReport
public sealed class ImportReport
What an import run did (or, under ImportOptions.DryRun, would do): per-entity created/updated/skipped counts plus per-item issues. Re-running the same import yields updates, never duplicates — the counts make that visible.
Properties
Dictionary<string, string> GeneratedClientSecrets { get; }
Rotation-on-migration secrets: when a source stores secrets in a form Sentinel cannot carry over (Duende’s sha256), the importer mints a fresh secret per client and surfaces the plaintext here — the only place it ever exists. Hand these to the client owners, then discard the report.
ImportCounts Clients { get; }
ImportCounts Credentials { get; }
ImportCounts GroupMemberships { get; }
ImportCounts Groups { get; }
ImportCounts RoleAssignments { get; }
ImportCounts Roles { get; }
ImportCounts Users { get; }
List<ImportIssue> Issues { get; }
bool DryRun { get; set; }
Methods
void AddIssue(ImportIssueSeverity severity, string entity, string key, string message)
ImportedClient
public sealed class ImportedClient
Properties
List<string> AllowedScopes { get; init; }
List<string> PostLogoutRedirectUris { get; init; }
List<string> RedirectUris { get; init; }
OidcClientType ClientType { get; init; }
bool RequireConsent { get; init; }
bool SecretResetRequired { get; init; }
The source’s secret could not be carried over (Duende stores sha256(secret)): a fresh secret was generated (see ImportReport.GeneratedClientSecrets) or the operator must set one before the client can authenticate.
bool Suspended { get; init; }
required string ClientId { get; init; }
string? SecretAlgorithm { get; init; }
string? SecretHash { get; init; }
Already hashed by the importer via the current PasswordHasher; null = no secret imported.
ImportedCredential
public sealed record ImportedCredential : IEquatable<ImportedCredential>
An algorithm-tagged hash in that algorithm’s native text format.
Constructors
ImportedCredential(string Algorithm, string Hash)
An algorithm-tagged hash in that algorithm’s native text format.
Properties
string Algorithm { get; init; }
string Hash { get; init; }
ImportedGroup
public sealed class ImportedGroup
Properties
required string DisplayName { get; init; }
required string Key { get; init; }
ImportedRole
public sealed class ImportedRole
Properties
required string DisplayName { get; init; }
required string Key { get; init; }
ImportedUser
public sealed class ImportedUser
Properties
Dictionary<string, object?> Attributes { get; init; }
Merged into the user’s ABAC attribute bag; imported keys overwrite.
ImportedCredential? Credential { get; init; }
Null imports the user password-less (unsupported foreign credential).
List<string> GroupKeys { get; init; }
List<string> RoleKeys { get; init; }
Sentinel role keys (already imported:-prefixed) to assign at realm level.
bool EmailVerified { get; init; }
bool Suspended { get; init; }
True imports the user as UserStatus.Suspended; false creates Active but never reactivates an already-suspended existing user on re-import.
required string Email { get; init; }
Normalized (trimmed, lowercased) — the realm-scoped natural key.
string? DisplayName { get; init; }
ImporterHashAlgorithms
public static class ImporterHashAlgorithms
The foreign algorithms a migrating host should register as PasswordHasher legacy algorithms, e.g. new PasswordHasher(new Argon2idPasswordHashAlgorithm(), [new Pbkdf2PasswordHashAlgorithm(), .. ImporterHashAlgorithms.All()]). Without this, imported users exist but their passwords cannot verify until reset.
Methods
static IReadOnlyList<IPasswordHashAlgorithm> All()
KeycloakRealmImporter
public sealed class KeycloakRealmImporter
Keycloak realm-export importer: parses the well-known realm-export JSON (realm, users[] with credentials[], groups[], roles.realm[], clients[]).
Credentials: pbkdf2-sha256 (both the legacy hashIterations/salt/ hashedSaltedValue fields and the newer secretData/credentialData pair) map to Core’s PHC pbkdf2-sha256 format; bcrypt values carry over verbatim under the bcrypt tag. Anything else is recorded as an issue and the user imports password-less. Confidential clients whose export contains the plaintext secret get it re-hashed with the current hasher — no rotation needed for those.
Constructors
KeycloakRealmImporter(IImportTarget target, PasswordHasher hasher)
Keycloak realm-export importer: parses the well-known realm-export JSON (realm, users[] with credentials[], groups[], roles.realm[], clients[]). Credentials: pbkdf2-sha256 (both the legacy hashIterations/salt/ hashedSaltedValue fields and the newer secretData/credentialData pair) map to Core’s PHC pbkdf2-sha256 format; bcrypt values carry over verbatim under the bcrypt tag. Anything else is recorded as an issue and the user imports password-less. Confidential clients whose export contains the plaintext secret get it re-hashed with the current hasher — no rotation needed for those.
Methods
Task<ImportReport> ImportAsync(string realmExportJson, ImportOptions options, CancellationToken cancellationToken = default(CancellationToken))
OrgMappingStrategy
public enum OrgMappingStrategy
How source-side tenant/organization structure maps into Sentinel. v1 ships exactly one strategy; the enum exists so the option surface is stable when org-per-tenant mapping lands.
Values
FlattenToRealm— Everything imports at realm level: roles and groups are realm-scoped, users get no org memberships. Source tenant structure that cannot be represented is recorded as issues.
ShadowAuthzRecorder
public sealed class ShadowAuthzRecorder
Shadow-mode authorization for migrating hosts: while the legacy stack keeps enforcing, every check also runs through Sentinel’s evaluator; divergences are counted and emitted as authz.shadow_divergence events, and cutover is gated on ShadowAuthzReport.ReadyForCutover — zero divergences over a non-empty sample. Thread-safe; one instance is meant to live for the whole shadow period.
Constructors
ShadowAuthzRecorder(ISentinelEventSink events, ISentinelClock? clock = null)
Shadow-mode authorization for migrating hosts: while the legacy stack keeps enforcing, every check also runs through Sentinel’s evaluator; divergences are counted and emitted as authz.shadow_divergence events, and cutover is gated on ShadowAuthzReport.ReadyForCutover — zero divergences over a non-empty sample. Thread-safe; one instance is meant to live for the whole shadow period.
Methods
ShadowAuthzReport Report()
Totals so far — the cutover gate reads this.
bool Compare(bool legacyDecision, SubjectSnapshot subject, in AccessCheck check)
Records one shadow comparison and returns legacyDecision unchanged — during shadow mode the legacy stack stays authoritative, so callers can inline this into their existing check path: if (recorder.Compare(legacyAllows, snapshot, check)) ….
Fields
const string DivergenceEventKind = "authz.shadow_divergence"
Stable event kind emitted on every legacy/Sentinel disagreement.
ShadowAuthzReport
public readonly record struct ShadowAuthzReport : IEquatable<ShadowAuthzReport>
Shadow-period totals.
Constructors
ShadowAuthzReport(long Agreements, long Divergences)
Shadow-period totals.
Properties
bool ReadyForCutover { get; }
Zero divergences over a non-empty sample — the cutover gate.
long Agreements { get; init; }
long Divergences { get; init; }
long Total { get; }