Skopka.Identity.Infrastructure 0.14.0

dotnet add package Skopka.Identity.Infrastructure --version 0.14.0
                    
NuGet\Install-Package Skopka.Identity.Infrastructure -Version 0.14.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Skopka.Identity.Infrastructure" Version="0.14.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Skopka.Identity.Infrastructure" Version="0.14.0" />
                    
Directory.Packages.props
<PackageReference Include="Skopka.Identity.Infrastructure" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Skopka.Identity.Infrastructure --version 0.14.0
                    
#r "nuget: Skopka.Identity.Infrastructure, 0.14.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Skopka.Identity.Infrastructure@0.14.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Skopka.Identity.Infrastructure&version=0.14.0
                    
Install as a Cake Addin
#tool nuget:?package=Skopka.Identity.Infrastructure&version=0.14.0
                    
Install as a Cake Tool

Skopka.Identity

CI License

Skopka.Identity is a transport-neutral identity library for ASP.NET Core. It owns user lifecycle, password credentials, verification, sessions, roles and persistence while leaving HTTP endpoints, UI, delivery channels and application authorization to the host.

The project is an alternative set of identity building blocks. It is not API-compatible with Microsoft.AspNetCore.Identity.

The API is pre-1.0 and currently targets .NET 10. Validate the library against your threat model and deployment requirements before using it in production.

Features

  • Generic application profile stored as PostgreSQL jsonb or SQLite JSON text.
  • Nullable user name, email and phone for external-login-only users.
  • Atomic password and external-login registration workflows.
  • External login resolve, list, link and unlink lifecycle.
  • Sign-in-method snapshots for stricter host-owned unlink policy.
  • Optimistic concurrency through a numeric user version.
  • Soft delete, restore and permanent or temporary blocking.
  • PBKDF2-HMAC-SHA256 and peppered Argon2id password verifiers.
  • Configurable password bounds and application-defined asynchronous validators.
  • Password authentication with account/client rate-limiting extension points.
  • Exact normalized active-user lookup for trusted account-message workflows.
  • Email, phone and password-reset action tokens based on ASP.NET Core Data Protection.
  • OTP challenge/proof orchestration with HMAC-protected generated codes.
  • Short-lived JWT access tokens with signing-key overlap and rotating opaque refresh sessions.
  • Active-session listing, device labels and user-scoped session revocation.
  • Persistent, single-consumption device-authorization requests for host-owned cross-device sign-in flows.
  • Optional online access-token/session validation.
  • Role CRUD, direct memberships, bounded role queries and role projection into session claims.
  • Bounded cursor-based user and role queries for administrative interfaces.
  • Structured security-event observer hooks for host-side audit pipelines.
  • Step-up verification decisions separated from normal application authorization.
  • EF Core stores with PostgreSQL and SQLite mappings and packaged migrations.

OAuth/OIDC protocol clients, WebAuthn/passkeys and UI/endpoints are not implemented here. A host validates the provider response and passes only the trusted provider/subject pair to the identity services.

Packages

Package Purpose
Skopka.Identity.Abstractions Public contracts, commands, models and error codes
Skopka.Identity.Core Validation and identity orchestration
Skopka.Identity Consumer-facing dependency injection and composition
Skopka.Identity.Ef Provider-neutral EF Core entities and stores
Skopka.Identity.Ef.PostgreSql PostgreSQL mappings, stores and migrations
Skopka.Identity.Ef.Sqlite SQLite mappings, stores and migrations
Skopka.Identity.Infrastructure Password hashers, tokens, OTP, rate limiting and JWT

A typical ASP.NET Core application references:

dotnet add package Skopka.Identity
dotnet add package Skopka.Identity.Ef.PostgreSql
dotnet add package Skopka.Identity.Infrastructure

Quick Start

Define one profile type for the application:

public sealed record AppProfile(
    string DisplayName,
    string? Locale);

Register the identity services, PostgreSQL store and one password hasher:

using Microsoft.Extensions.DependencyInjection;

var identity = builder.Services
    .AddSkopkaIdentity<AppProfile>()
    .ConfigurePasswordPolicy(options =>
    {
        options.MinimumLength = 15;
        options.MaximumLength = 128;
    })
    .UsePostgreSql(
        builder.Configuration.GetConnectionString("Identity")
        ?? throw new InvalidOperationException(
            "The Identity connection string is missing."))
    .UsePbkdf2PasswordHasher();

The default password policy is already 15..128 Unicode code points. The minimum can be lowered to 8 for applications that enforce multi-factor authentication. Passwords are never trimmed or normalized, and there are no character-class composition rules.

Apply packaged migrations during deployment. For local development, a host can apply them explicitly:

using Microsoft.EntityFrameworkCore;
using Skopka.Identity.Ef.PostgreSql;

await using var scope = app.Services.CreateAsyncScope();
var identityDb = scope.ServiceProvider.GetRequiredService<
    PostgreSqlIdentityDbContext<AppProfile>>();
await identityDb.Database.MigrateAsync();

Do not let every application replica race to run migrations in production. Prefer a dedicated deployment or migrator step.

For SQLite, reference Skopka.Identity.Ef.Sqlite, register .UseSqlite(connectionString) and resolve SqliteIdentityDbContext<AppProfile> when applying migrations. Enable foreign-key enforcement in externally managed connections; for example, Data Source=identity.db;Foreign Keys=True. SQLite timestamps are stored as UTC ticks so session expiry, rate-limit windows and cursor ordering retain chronological query semantics.

Register a password user atomically through the public registration service:

using Skopka.Identity.Registration;
using Skopka.Identity.Users.Commands;

var registration = scope.ServiceProvider.GetRequiredService<
    IIdentityRegistrationService<AppProfile>>();

var created = await registration.RegisterPasswordAsync(
    new RegisterPasswordUserCommand<AppProfile>(
        new CreateUserCommand<AppProfile>(
            "alice",
            "alice@example.com",
            null,
            new AppProfile("Alice", "en")),
        submittedPassword),
    cancellationToken);

if (!created.IsSuccess)
{
    return Results.BadRequest(created.Errors);
}

RegisterPasswordAsync persists the user and password verifier in one EF SaveChangesAsync. Use IIdentityUserService<TProfile>.CreateAsync only when creating a user without a credential is intentional.

Commands return OperationResult instead of throwing for expected domain failures. Persist and submit the latest IdentityUser.Version for mutations that require optimistic concurrency.

Authenticate with an explicit user name, email or phone, or let Identity resolve a bounded automatic candidate set:

using Skopka.Identity.Authentication;

var passwords = scope.ServiceProvider.GetRequiredService<
    IPasswordAuthenticationService<AppProfile>>();

var authentication = await passwords.AuthenticateAsync(
    new AuthenticatePasswordCommand(
        PasswordLoginHandle.Automatic,
        "alice@example.com",
        submittedPassword,
        clientKey),
    cancellationToken);

clientKey is trusted transport context created by the host, such as a protected IP partition key. Do not accept it directly from an untrusted request body.

Automatic lookup normalizes the input as a user name and email, and as a phone only when the input contains 8-15 digits plus common phone separators. It succeeds only when the matching registry rows identify one active user. No match and cross-handle ambiguity follow the same invalid-credentials and dummy-password-verification path.

The same default phone shape is required when a phone handle is created, changed, confirmed, looked up exactly or used with PasswordLoginHandle.Phone. Applications with another numbering plan can override IIdentityNormalizer.NormalizePhoneLoginIdentifier; hosts should call that contract instead of duplicating phone rules in transport validation.

For source/binary compatibility, the new phone and automatic members on IIdentityUserLookupStore<TProfile> have safe no-match defaults. Custom persistence adapters must implement both members before enabling these modes, query automatic candidates in one bounded operation, and enforce one active owner for every normalized alias across handle types.

After successful authentication, create a session from the returned user and its current security stamp:

using Skopka.Identity.Sessions;

var sessions = scope.ServiceProvider.GetRequiredService<
    IIdentitySessionService<AppProfile>>();

var issued = await sessions.CreateAsync(
    new CreateIdentitySessionCommand(
        authentication.Value.Id,
        authentication.Value.SecurityStamp,
        new IdentitySessionMetadata("web", "Alice's laptop")),
    cancellationToken);

The host decides how to return the access token and protect the refresh token. For a browser, prefer a Secure, HttpOnly, SameSite cookie and add CSRF protection to state-changing endpoints.

For an RFC 8628-inspired approval flow that creates a separate session on a new browser, see device authorization requests.

OAuth/OIDC and other protocol adapters that own their token storage can create a common logical session without issuing an Identity refresh token:

var registry = scope.ServiceProvider.GetRequiredService<
    IIdentitySessionRegistry<AppProfile>>();

var registered = await registry.RegisterAsync(
    new RegisterIdentitySessionCommand(
        authentication.Value.Id,
        authentication.Value.SecurityStamp,
        new IdentitySessionMetadata("native", "Alice's phone")),
    cancellationToken);

Store the returned SessionId only in protected protocol state. Use ValidateIdentitySessionCommand for online validation and the existing user-scoped revoke/list commands for account management.

Argon2id With Pepper

Load pepper keys from a secret manager, not configuration committed to source:

using Skopka.Identity.Credentials;

var pepperKey = Convert.FromBase64String(
    builder.Configuration["Identity:PasswordPepper"]
    ?? throw new InvalidOperationException("Password pepper is missing."));

var pepperProvider = new StaticPasswordPepperProvider(
    currentKeyId: "2026-01",
    currentKey: pepperKey);

builder.Services
    .AddSkopkaIdentity<AppProfile>()
    .UsePostgreSql(connectionString)
    .UseArgon2idPepperedPasswordHasher(pepperProvider);

Keep old pepper keys available during rotation until every verifier has been upgraded. The stored verifier contains only the key id and KDF parameters, never the pepper.

JWT Sessions

JWT sessions are optional. Each signing key must contain at least 32 random bytes. All issuing and validating instances must share the current key id and the overlapping key set during rotation:

var signingKeys = LoadVersionedSigningKeys(builder.Configuration);

builder.Services
    .AddSkopkaIdentity<AppProfile>()
    .UsePostgreSql(connectionString)
    .UseJwtSessions(
        signingKeys.CurrentKeyId,
        signingKeys.Keys,
        jwt =>
        {
            jwt.Issuer = "https://identity.example.com";
            jwt.Audience = "example-api";
        })
    .UseJwtBearerAuthentication(options =>
    {
        options.ValidateSessionOnEveryRequest = false;
    });

Stateless validation accepts a correctly signed access token until its short expiry. Set ValidateSessionOnEveryRequest to true when immediate refresh-session revocation is worth a database lookup on every request. Role changes appear in newly created or refreshed access tokens; existing stateless JWTs retain their embedded claims.

New access tokens carry the current key id in the protected kid header. Validators accept configured historical ids, and also try the bounded overlapping set for legacy tokens issued before kid support. Retain an old key for at least the access-token lifetime plus clock skew and the maximum rolling-deployment interval. The single-key UseJwtSessions(byte[], ...) overload remains available for deployments that do not need overlap.

Account UIs can call ListAsync and RevokeByIdAsync. Revocation is scoped by both user id and session id, so knowing another user's session id is not sufficient.

External Login Boundary

The host owns OAuth/OIDC redirects, state, nonce, PKCE and provider token validation. After validation, pass the provider name and stable provider subject:

using Skopka.Identity.ExternalLogins;

var externalLogins = scope.ServiceProvider.GetRequiredService<
    IExternalLoginService<AppProfile>>();

var resolved = await externalLogins.ResolveAsync(
    new ExternalLoginKey("github", validatedProviderSubject),
    cancellationToken);

Provider names are canonicalized; subjects are case-sensitive and preserved exactly. Never use an unverified client-supplied subject, email or access token as the login key. Use IIdentityRegistrationService<TProfile>.RegisterExternalAsync for a new account and LinkAsync only for an authenticated user after the host's required step-up check.

Before a self-service unlink, read IIdentitySignInMethodQueryService<TProfile>.GetAsync. Reject removal when the host would leave no enabled password flow or enabled external provider, then pass the snapshot's Version unchanged to UnlinkAsync. Identity reports persisted links; the host must intersect them with its current provider catalog. Do not expose the returned provider subject in HTTP or UI. A concurrency conflict starts a fresh policy and step-up flow; do not automatically retry an already authorized mutation.

Optional Modules

All optional modules compose through IdentityBuilder<TProfile>:

identity
    .UseDataProtectionActionTokens()
    .UseDataProtectionTotp()
    .UseWebAuthn(options => options.RelyingPartyId = "example.test")
    .UseHmacOneTimeCodes("otp-2026-01", otpHmacKey)
    .UseHmacRateLimiting(
        currentVersion: "rate-limit-2026-07",
        new Dictionary<string, byte[]>
        {
            ["rate-limit-2026-07"] = currentRateLimitKey,
            ["rate-limit-2026-01"] = previousRateLimitKey,
        })
    .AddRoles()
    .AddStepUpAuthorization<ApplicationStepUpPolicyProvider>();

Use separate random keys for password peppering, JWT signing, OTP verification and rate-limit partition hashing. Persist and share the ASP.NET Core Data Protection key ring in multi-instance deployments. A custom non-HMAC partition strategy can be registered with UseRateLimiting(customPartitionHasher).

UseDataProtectionTotp() adds RFC 6238 authenticator enrollment and the totp verification method. It deliberately uses the broadly compatible HMAC-SHA1, six-digit, 30-second profile with one adjacent time step of clock drift. The generated Base32 secret is Data Protection-encrypted in the factor store. Enrollment becomes active only after a correct code; that code is immediately burned for replay protection. Confirmation also returns one-use recovery codes whose stored values are hashes. Persist the Data Protection key ring before enabling this module, and apply the packaged TOTP-factor migrations.

UseWebAuthn() adds the WebAuthn credential lifecycle and ceremony verifier, and the EF provider adds the credential store. The relying party id and allowed origins are configuration, never request fields. The library keeps public-key credentials and checks what a browser sends against them; it does not issue challenges or decide what a verified assertion may do, which belong to the host. Attestation statements are read but not verified, because judging authenticator models needs a trusted metadata set and a policy that belong to an application. ES256 and RS256 are verified. See WebAuthn credentials.

During rate-limit key rotation, every replica must temporarily expose the same old and new versions while selecting the new version as current. The limiter checks and writes all configured versions so old and new replicas share active counters. Remove the old key only after no old-only replica remains and the longest active rate-limit window has elapsed.

Design and Operations

License

Skopka.Identity is licensed under the Apache License 2.0.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.14.0 137 9/3/2026
0.13.0 192 8/31/2026
0.12.0 148 8/29/2026
0.11.0 291 8/14/2026
0.10.0 208 8/7/2026
0.9.0 152 8/4/2026
0.8.0 152 8/3/2026
0.7.0 132 8/3/2026
0.6.0 97 8/3/2026
0.5.0 115 8/1/2026
0.4.0 108 7/30/2026
0.3.0 110 7/30/2026
0.2.0 103 7/30/2026
0.1.0 107 7/29/2026