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; }