Skip to content

Latest commit

 

History

History
1699 lines (1170 loc) · 54.4 KB

File metadata and controls

1699 lines (1170 loc) · 54.4 KB

CoreIdent Developer Guide

This guide documents the CoreIdent codebase. It is intended to be the “one stop” developer reference for:

  • Getting a host app running quickly
  • Understanding how CoreIdent is structured
  • Configuring keys, routes, clients, and scopes
  • Using the OAuth/OIDC endpoints CoreIdent currently implements
  • Persisting CoreIdent state with EF Core
  • Writing unit/integration tests using the provided test infrastructure

You will achieve

  • Run a minimal CoreIdent host and exercise key endpoints
  • Configure signing keys, routes, clients, and scopes
  • Enable persistence with EF Core
  • Understand the implemented endpoint surface and how to map it

If you are looking for the high-level roadmap and design intent, start with:

  • docs/Project_Overview.md
  • docs/Technical_Plan.md
  • docs/DEVPLAN.md
  • Realms (future work): Realms — Draft realm-ready foundation for future multi-tenant / multi-issuer / per-realm keys+stores scenarios.
  • docs/Passkeys.md
  • docs/Aspire_Integration.md

What CoreIdent is today

CoreIdent currently provides a minimal, modular OAuth/OIDC foundation on .NET 10:

  • JWT token issuance (/auth/token)
    • client_credentials
    • refresh_token
    • authorization_code (with PKCE)
    • password (implemented but deprecated and logs a warning)
  • Token revocation (/auth/revoke, RFC 7009)
  • Token introspection (/auth/introspect, RFC 7662)
  • Discovery document (OIDC metadata)
  • JWKS publishing (public keys only)
  • Authorization endpoint (/auth/authorize) + minimal consent UI (/auth/consent) for authorization code flow
  • Resource-owner convenience endpoints (/auth/register, /auth/login, /auth/profile) for simple “first party app” workflows
  • Pluggable stores with in-memory defaults + EF Core implementations in CoreIdent.Storage.EntityFrameworkCore
  • A testing package (tests/CoreIdent.Testing) with fixtures and builders for integration tests

CoreIdent is not trying to be “everything at once” yet. The focus is secure defaults (asymmetric signing by default) and testable primitives.


OAuth 2.1 Compliance

CoreIdent targets full compliance with OAuth 2.1 (RFC 9725), which consolidates and strengthens OAuth 2.0 best practices. This means:

What CoreIdent enforces

  • PKCE required — All authorization code flows must include code_challenge and code_challenge_method=S256. This applies to both public and confidential clients.
  • No implicit grant — The response_type=token flow is not supported. Use authorization code + PKCE instead.
  • No hybrid flow — OIDC hybrid response types (code token, code id_token token) are not supported.
  • Exact redirect URI matching — Redirect URIs must exactly match a registered URI. No wildcard or partial matching.
  • Refresh token rotation — Refresh tokens are single-use with automatic rotation and theft detection via family tracking.

What this means for clients

  • All clients (including server-side/confidential) must implement PKCE
  • Use response_type=code exclusively
  • Register exact redirect URIs (no wildcards, no path prefixes)
  • Handle refresh token rotation (each refresh returns a new token)
  • JavaScript/SPA clients should use the authorization code flow with PKCE, not implicit

Migration from OAuth 2.0 patterns

If migrating from a system that uses implicit flow or ROPC (password grant):

  • Implicit flow → Use authorization code + PKCE. See the JS/TS Client Compatibility section.
  • ROPC / Password grant → Use authorization code + PKCE, or install CoreIdent.Legacy.PasswordGrant for migration support. ROPC is deprecated in OAuth 2.1.

Third-party extensibility opportunities

CoreIdent is designed so third parties can build “flavors” (membership systems, admin surfaces, hosted identity offerings, etc.) without forking CoreIdent.Core.

The primary rule is: prefer replacing behavior via DI + interfaces rather than editing CoreIdent code.

Replace storage (or add a storage package)

CoreIdent’s persistence is expressed as store interfaces. A third party can provide:

  • IUserStore (users + claim storage)
  • IClientStore (OAuth clients)
  • IScopeStore (scopes used by discovery + token issuance)
  • IAuthorizationCodeStore (authorization codes)
  • IRefreshTokenStore (refresh tokens, rotation, family revocation)
  • IUserGrantStore (consent grants)
  • ITokenRevocationStore (revoked access token tracking)
  • IPasswordlessTokenStore (passwordless token/OTP storage)

Notes:

  • IScopeStore is currently a read-only interface. If you need scope administration, own a write model (DB tables + admin API) and expose a read projection via IScopeStore.
  • Store defaults are registered with TryAdd* patterns, so registering your implementation before calling AddCoreIdent() takes precedence.

Customize token claims and identity surfaces

There are two main “claims seams”:

  • IUserStore.GetClaimsAsync(subjectId, ...) for user-owned claims (roles/groups/flags owned by your membership layer)
  • ICustomClaimsProvider for computed/derived claims added during token issuance

This enables membership and enterprise layers to inject authorization state into access tokens and ID tokens without modifying CoreIdent endpoints.

Customize signing keys and key management

Token signing and JWKS publishing use ISigningKeyProvider.

Third parties can integrate external key systems by implementing ISigningKeyProvider (for example, loading from an HSM, Key Vault, KMS, or a per-tenant certificate store) and registering it via AddSigningKey(...).

Customize and/or replace endpoint behavior

For the resource-owner convenience endpoints, you can override response behavior via CoreIdentResourceOwnerOptions:

  • RegisterHandler
  • LoginHandler
  • ProfileHandler

You can also avoid MapCoreIdentEndpoints() and map only the endpoints you want using the granular mapping extensions.

Integrate passwordless providers

For production deployments, third parties commonly replace the default delivery providers:

  • Implement IEmailSender to use an email API provider
  • Implement ISmsProvider to use an SMS provider

Passwordless storage is similarly replaceable via IPasswordlessTokenStore.

Delegate user identity to an existing system

If you already have a user database and credential validation, you can avoid forking by using CoreIdent.Adapters.DelegatedUserStore to delegate:

  • user lookup (FindUserByIdAsync, FindUserByUsernameAsync)
  • credential validation
  • optional GetClaimsAsync for membership/roles

Embedded Auth vs Membership (Guidance Placeholder)

This section is a placeholder for DEVPLAN 1.13.6.

  • Embedded auth ("I just need auth in my app"): Use the resource-owner convenience endpoints (/auth/register, /auth/login, /auth/profile) for first-party app workflows.
  • Membership/admin: For richer membership (profile fields, roles/groups, admin UI), prefer building on CoreIdent by plugging in your own stores and adding claims/profile behavior at the host layer.

Key extension points to expand on:

  • IUserStore (replace user persistence)
  • ICustomClaimsProvider (add claims to tokens)
  • CoreIdentResourceOwnerOptions handlers (RegisterHandler, LoginHandler, ProfileHandler)
  • Custom endpoints in the host app (e.g., GET /me) keyed by sub

CoreIdent.Client (Client Library)

CoreIdent.Client is a lightweight OAuth 2.0 / OpenID Connect client library intended to work against:

  • CoreIdent hosts
  • Third-party OIDC providers (e.g. Keycloak)

It implements the Authorization Code + PKCE flow, refresh token handling, discovery document fetching, and (when available) UserInfo.

Quick start

Create a client instance:

  • Set Authority to the provider base URL
  • Set ClientId
  • Use a loopback redirect URI for interactive flows (recommended)

CoreIdent.Client persists tokens via ISecureTokenStorage (in-memory by default).

Options

Key CoreIdentClientOptions fields:

  • Authority
  • ClientId
  • ClientSecret (optional; if set, client auth uses HTTP Basic)
  • RedirectUri
  • PostLogoutRedirectUri
  • Scopes
  • TokenRefreshThreshold

Token storage

Implementations included:

  • InMemoryTokenStorage (default)
  • FileTokenStorage (encrypted using ASP.NET Core Data Protection)

If you want tokens to persist across process restarts, provide a configured IDataProtectionProvider to FileTokenStorage.

Browser integration

For interactive login, CoreIdent.Client uses an IBrowserLauncher abstraction.

The default implementation (SystemBrowserLauncher) requires:

  • RedirectUri uses http or https
  • RedirectUri host is loopback (localhost, 127.0.0.1, ::1)

If your app uses a non-loopback redirect URI (mobile deep link, custom scheme, embedded web view, etc.), provide a custom IBrowserLauncher.

MAUI integration

For .NET MAUI apps, use CoreIdent.Client.Maui. It provides:

  • MauiSecureTokenStorage backed by SecureStorage
  • MauiBrowserLauncher backed by WebAuthenticator
  • UseCoreIdentClient() for MauiAppBuilder

See the full MAUI setup and sample app walkthrough in docs/MAUI_Client_Guide.md.

Discovery and third-party providers (Keycloak readiness)

CoreIdent.Client uses OIDC discovery (.well-known/openid-configuration) and follows the discovered endpoints.

Authorities with path bases are supported. Example:

  • Keycloak realm authority: https://kc.example/realms/myrealm

In this case discovery is fetched relative to that authority:

  • https://kc.example/realms/myrealm/.well-known/openid-configuration

ID token validation

When the discovery document includes jwks_uri, the client validates ID tokens using:

  • Signature verification via JWKS
  • Issuer validation (iss)
  • Audience validation (aud contains ClientId)
  • Lifetime validation (exp)

For interactive login, the client additionally validates the ID token nonce.

Known limitations

  • UseDPoP enables client-side DPoP proof generation and sends DPoP headers on supported requests. Server-side DPoP validation/binding depends on the authorization server (CoreIdent server support is planned under Feature 3.6).
  • Front-channel logout is attempted only if the provider advertises end_session_endpoint.

Repository structure

At a high level:

  • src/CoreIdent.Core/
    • Core models, options, store interfaces, in-memory stores, services, endpoint mapping extensions
  • src/CoreIdent.Storage.EntityFrameworkCore/
    • EF Core DbContext, entity models, and EF Core implementations of stores
  • src/CoreIdent.Adapters.DelegatedUserStore/
    • Adapter package for plugging in an external user store (if used)
  • tests/CoreIdent.Core.Tests/
    • Unit + integration tests for the core package
  • tests/CoreIdent.Testing/
    • Shared test infrastructure: WebApplicationFactory, fixture base class, builders, assertion extensions
  • tests/CoreIdent.TestHost/
    • A runnable host used by some tests and manual validation
  • src/CoreIdent.Cli/
    • CLI tool package (dotnet coreident) for project scaffolding, key generation, and database migrations

CLI Tool

CoreIdent ships a .NET global tool for common development tasks:

dotnet tool install -g CoreIdent.Cli

Commands:

  • dotnet coreident init — Scaffold appsettings.json and add package references
  • dotnet coreident keys generate <rsa|ecdsa> — Generate signing key pairs
  • dotnet coreident client add — Interactive client registration helper
  • dotnet coreident migrate — Apply database schema (SQLite, SQL Server, PostgreSQL)

See docs/CLI_Reference.md for full documentation.


Prerequisites

  • .NET 10 SDK (required)
  • Recommended:
    • SQLite tooling (optional; used by tests/fixtures)
    • OpenSSL (optional; useful for key generation)

1. Quick start

1.1 Minimal “OAuth server” host

This is the smallest “real” CoreIdent host app setup:

  • configure issuer/audience
  • configure a signing key (RSA or ECDSA for production)
  • map CoreIdent endpoints

Key points:

  • AddCoreIdent() registers all core services and in-memory stores by default.
  • AddSigningKey(...) registers an ISigningKeyProvider used by token services and JWKS.
  • MapCoreIdentEndpoints() maps all CoreIdent endpoints using CoreIdentRouteOptions.

Example (Program.cs)

using CoreIdent.Core.Extensions;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddCoreIdent(o =>
{
    o.Issuer = "https://issuer.example";
    o.Audience = "https://resource.example";
});

// Production: prefer RSA/ECDSA.
// Example: load RSA from PEM file
builder.Services.AddSigningKey(o => o.UseRsa("/path/to/private-key.pem"));

var app = builder.Build();

app.MapCoreIdentEndpoints();

app.Run();

1.2 OpenAPI documentation (Feature 1.13.10)

CoreIdent supports OpenAPI document generation via the CoreIdent.OpenApi package (built on .NET 10's Microsoft.AspNetCore.OpenApi).

CoreIdent does not ship a built-in UI (no Swashbuckle / Swagger UI). Instead, the host app can optionally add a UI such as Scalar.

1.2.1 Add OpenAPI services

Add the package reference:

<PackageReference Include="CoreIdent.OpenApi" Version="1.0.0" />

Register OpenAPI services:

using CoreIdent.OpenApi.Extensions;

builder.Services.AddCoreIdentOpenApi(options =>
{
    options.DocumentTitle = "My API";
    options.DocumentVersion = "v1";
    options.OpenApiRoute = "/openapi/v1.json";
});

1.2.2 Map the OpenAPI endpoint

using CoreIdent.OpenApi.Extensions;

app.MapCoreIdentOpenApi();

This maps an endpoint equivalent to MapOpenApi(...) so that GET /openapi/v1.json returns the OpenAPI JSON document.

1.2.3 Optional UI: Scalar (host-managed)

To serve an interactive API reference UI, install Scalar.AspNetCore in your host app and map it alongside the OpenAPI JSON:

using Scalar.AspNetCore;

app.MapCoreIdentOpenApi();
app.MapScalarApiReference();

What you get

With defaults, CoreIdent maps:

  • OpenID Connect discovery document: GET <issuerPath>/.well-known/openid-configuration
  • JWKS: GET <issuerPath>/.well-known/jwks.json
  • Authorization endpoint: GET /auth/authorize
  • Consent UI: GET/POST /auth/consent
  • Token endpoint: POST /auth/token
  • Token management:
    • POST /auth/revoke
    • POST /auth/introspect
  • Resource owner endpoints:
    • GET/POST /auth/register
    • GET/POST /auth/login
    • GET /auth/profile
  • Passwordless email magic link:
    • POST /auth/passwordless/email/start
    • GET /auth/passwordless/email/verify
  • Passwordless SMS OTP:
    • POST /auth/passwordless/sms/start
    • POST /auth/passwordless/sms/verify
  • Passkeys (WebAuthn): see docs/Passkeys.md (mapped via app.MapCoreIdentPasskeyEndpoints())

Note: discovery and JWKS are computed based on the Issuer URL’s path (see “Routing” below). They are not hardcoded to root.


1.2 Run the test host

A runnable host exists at tests/CoreIdent.TestHost/Program.cs. It configures:

  • AddCoreIdent(...)
  • AddSigningKey(o => o.UseSymmetric(...)) (development/testing only)
  • EF Core CoreIdentDbContext (SQLite)
  • AddEntityFrameworkCoreStores() to use EF-backed stores
  • ASP.NET authentication with a test header auth handler

This is a convenient way to manually poke endpoints.


1.3 Scaffold a host with dotnet new

CoreIdent ships a template pack (CoreIdent.Templates) that contains starter host projects.

Install the templates:

dotnet new install CoreIdent.Templates

Available templates:

  • coreident-api (C#)
    • Minimal host for CoreIdent endpoints.
    • Parameters:
      • --useEfCore <true|false> (default: true)
      • --usePasswordless <true|false> (default: true)
  • coreident-server (C#)
    • Full OAuth/OIDC server host (EF Core stores) with optional passkey endpoints.
    • Parameters:
      • --usePasskeys <true|false> (default: true)
      • --usePasswordless <true|false> (default: true)
  • coreident-api-fsharp (F#)
    • Minimal host for CoreIdent endpoints.
    • Parameters:
      • --useEfCore <true|false> (default: true)
      • --usePasswordless <true|false> (default: true)

Examples:

dotnet new coreident-api -n MyCoreIdentApi
dotnet new coreident-api -n MyCoreIdentApiNoEf --useEfCore false

dotnet new coreident-server -n MyCoreIdentServer
dotnet new coreident-server -n MyCoreIdentServerNoPasskeys --usePasskeys false

dotnet new coreident-api-fsharp -n MyCoreIdentApiFSharp
dotnet new coreident-api-fsharp -n MyCoreIdentApiFSharpNoEf --useEfCore false

Each template includes a sample appsettings.json with required CoreIdent configuration (issuer, audience, dev signing key) and, when applicable, a SQLite connection string.


CORS Configuration

Cross-Origin Resource Sharing (CORS) must be configured when JavaScript clients (SPAs, Blazor WASM) access CoreIdent endpoints from a different origin.

Quick setup with AddCoreIdentCors()

CoreIdent provides a convenience method that auto-configures CORS from registered client redirect URIs:

builder.Services.AddCoreIdentCors(); // Extracts origins from client redirect URIs

var app = builder.Build();
app.UseCoreIdentCors(); // Must be before MapCoreIdentEndpoints()
app.MapCoreIdentEndpoints();

Manual configuration

For full control, use ASP.NET Core's built-in CORS:

builder.Services.AddCors(options =>
{
    options.AddPolicy("CoreIdentPolicy", policy =>
    {
        policy.WithOrigins("https://myapp.example.com")
              .AllowAnyHeader()
              .WithMethods("GET", "POST")
              .AllowCredentials();
    });
});

app.UseCors("CoreIdentPolicy");

Endpoints that require CORS

Endpoint When CORS is needed
POST /auth/token Always (token requests from browser)
GET /auth/userinfo Always (user info from SPA)
POST /auth/revoke When revoking from browser
GET /.well-known/openid-configuration Discovery from browser
GET /.well-known/jwks.json Key fetching from browser

Note: The authorization endpoint (/auth/authorize) uses redirects, not CORS. CORS is not needed for redirect-based flows.


Using CoreIdent with JavaScript/TypeScript Clients

CoreIdent is a standard-compliant OAuth 2.1 / OpenID Connect server. Any OIDC-compliant JavaScript client library works with it. We recommend oidc-client-ts for most use cases.

oidc-client-ts Configuration

import { UserManager } from 'oidc-client-ts';

const userManager = new UserManager({
  authority: 'https://your-coreident-server.com',
  client_id: 'your-spa-client-id',
  redirect_uri: 'https://your-app.com/callback',
  post_logout_redirect_uri: 'https://your-app.com/',
  response_type: 'code',
  scope: 'openid profile email',
  automaticSilentRenew: true,
});

// Login
await userManager.signinRedirect();

// Handle callback
const user = await userManager.signinRedirectCallback();
console.log('Logged in:', user.profile.email);

// Get access token (auto-refreshed)
const user = await userManager.getUser();
const token = user?.access_token;

// Logout
await userManager.signoutRedirect();

Vanilla JavaScript (Authorization Code + PKCE)

// Generate PKCE challenge
async function generatePKCE() {
  const verifier = crypto.randomUUID() + crypto.randomUUID();
  const encoder = new TextEncoder();
  const data = encoder.encode(verifier);
  const hash = await crypto.subtle.digest('SHA-256', data);
  const challenge = btoa(String.fromCharCode(...new Uint8Array(hash)))
    .replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
  return { verifier, challenge };
}

// Start login
const { verifier, challenge } = await generatePKCE();
const state = crypto.randomUUID();
sessionStorage.setItem('pkce_verifier', verifier);
sessionStorage.setItem('oauth_state', state);

const authUrl = new URL('https://your-coreident-server.com/auth/authorize');
authUrl.searchParams.set('client_id', 'your-spa-client-id');
authUrl.searchParams.set('redirect_uri', 'https://your-app.com/callback');
authUrl.searchParams.set('response_type', 'code');
authUrl.searchParams.set('scope', 'openid profile email');
authUrl.searchParams.set('state', state);
authUrl.searchParams.set('code_challenge', challenge);
authUrl.searchParams.set('code_challenge_method', 'S256');

window.location.href = authUrl.toString();

CORS requirements

Ensure CORS is configured for your JavaScript client's origin. See CORS Configuration.

Client registration

Register your SPA as a public client (no client secret):

{
  "clientId": "your-spa-client-id",
  "clientType": "Public",
  "redirectUris": ["https://your-app.com/callback"],
  "postLogoutRedirectUris": ["https://your-app.com/"],
  "allowedScopes": ["openid", "profile", "email"],
  "defaultScopes": ["openid", "profile"],
  "allowedGrantTypes": ["authorization_code"],
  "requirePkce": true
}

defaultScopes behaviour:

defaultScopes value When client omits scope parameter
null (omitted) All allowedScopes are granted (backwards-compatible)
[] (empty array) Request is rejected — scope is required
["openid", "profile"] Only the listed scopes are granted

When the client explicitly requests scopes, defaultScopes is ignored and the request is validated against allowedScopes as usual.


2. Configuration and dependency injection

2.1 Core options (CoreIdentOptions)

CoreIdentOptions includes:

  • Issuer (required)
  • Audience (required)
  • AccessTokenLifetime (default: 15 minutes)
  • RefreshTokenLifetime (default: 7 days)

AddCoreIdent() registers a validator (CoreIdentOptionsValidator) and calls ValidateOnStart(). If required values are missing or invalid, the app will fail fast at startup.

Important behavior

  • Many endpoints and services assume Issuer and Audience are non-null once validated.
  • The ITokenService (JWT token creation) requires issuer/audience per call.

2.2 Route options (CoreIdentRouteOptions)

CoreIdentRouteOptions controls how endpoint routes are assembled.

Defaults:

  • BasePath = "/auth"
  • Relative paths under base:
    • AuthorizePath = "authorize"
    • TokenPath = "token"
    • RevocationPath = "revoke"
    • IntrospectionPath = "introspect"
    • ConsentPath = "consent"
    • UserInfoPath = "userinfo"
    • RegisterPath = "register"
    • LoginPath = "login"
    • ProfilePath = "profile"
    • PasswordlessEmailStartPath = "passwordless/email/start"
    • PasswordlessEmailVerifyPath = "passwordless/email/verify"
    • PasswordlessSmsStartPath = "passwordless/sms/start"
    • PasswordlessSmsVerifyPath = "passwordless/sms/verify"
    • PasskeyRegisterOptionsPath = "passkey/register/options"
    • PasskeyRegisterCompletePath = "passkey/register/complete"
    • PasskeyAuthenticateOptionsPath = "passkey/authenticate/options"
    • PasskeyAuthenticateCompletePath = "passkey/authenticate/complete"
  • Root-ish helper:
    • UserProfilePath = "/me" (host-friendly convenience route; not currently mapped by MapCoreIdentEndpoints())

How route composition works

  • CombineWithBase(path):
    • If path starts with /, it is treated as root-relative and returned normalized.
    • Otherwise it returns BasePath + "/" + path normalized.

Discovery and JWKS paths are derived from Issuer

If DiscoveryPath and JwksPath are not explicitly set:

  • GetDiscoveryPath(CoreIdentOptions) returns:
    • "<issuerPath>/.well-known/openid-configuration"
  • GetJwksPath(CoreIdentOptions) returns:
    • "<issuerPath>/.well-known/jwks.json"

Where issuerPath is the path component of Issuer:

  • If Issuer = "https://example.com" → issuerPath is empty → discovery is /.well-known/openid-configuration
  • If Issuer = "https://example.com/auth" → issuerPath is /auth → discovery is /auth/.well-known/openid-configuration

This makes it possible to host CoreIdent under a path while still generating correct issuer-relative discovery URLs.


2.3 AddCoreIdent and default registrations

builder.Services.AddCoreIdent(...) registers:

  • Options:
    • CoreIdentOptions
    • CoreIdentRouteOptions
    • CoreIdentResourceOwnerOptions
    • CoreIdentAuthorizationCodeOptions
    • PasswordlessEmailOptions
    • PasswordlessSmsOptions
    • SmtpOptions
  • Core services:
    • ITokenServiceJwtTokenService
    • IClientSecretHasherDefaultClientSecretHasher
    • IPasswordHasherDefaultPasswordHasher
    • ICustomClaimsProviderNullCustomClaimsProvider
    • TimeProviderTimeProvider.System
    • IEmailSenderSmtpEmailSender
  • ISmsProviderConsoleSmsProvider
    • PasswordlessEmailTemplateRenderer
  • In-memory stores (defaults):
    • IClientStoreInMemoryClientStore
    • IScopeStoreInMemoryScopeStore (pre-seeded with standard OIDC scopes)
    • IRefreshTokenStoreInMemoryRefreshTokenStore
    • IAuthorizationCodeStoreInMemoryAuthorizationCodeStore
    • IUserGrantStoreInMemoryUserGrantStore
    • ITokenRevocationStoreInMemoryTokenRevocationStore
    • IUserStoreInMemoryUserStore
    • IPasswordlessTokenStoreInMemoryPasswordlessTokenStore
  • Hosted service:
    • AuthorizationCodeCleanupHostedService

2.4 Metrics and observability

CoreIdent emits metrics using System.Diagnostics.Metrics.

Enable CoreIdent-specific metrics

By default, CoreIdent registers a no-op metrics sink.

To enable CoreIdent metrics emission:

builder.Services.AddCoreIdent(...);
builder.Services.AddCoreIdentMetrics();

CoreIdent metric names

Counters:

  • coreident.client.authenticated — Number of client authentication attempts
  • coreident.token.issued — Number of tokens issued
  • coreident.token.revoked — Number of tokens revoked

Histograms:

  • coreident.client.authentication.duration — Duration of client authentication (ms)
  • coreident.token.issuance.duration — Duration of token issuance (ms)

Metric tags

  • coreident.client.authenticated
    • client_type (public / confidential / unknown)
    • success (true / false)
  • coreident.token.issued
    • token_type (access_token / refresh_token / id_token)
    • grant_type (e.g. client_credentials, authorization_code, refresh_token, password)
  • coreident.token.revoked
    • token_type (access_token / refresh_token)

Filtering and sampling

You can configure filtering and sampling when enabling metrics:

builder.Services.AddCoreIdentMetrics(o =>
{
    o.SampleRate = 1.0;
    o.Filter = ctx => ctx.MetricName != "coreident.token.issued";
});

Built-in ASP.NET Core metrics

.NET 10 already emits metrics for:

  • aspnetcore.authentication.*
  • aspnetcore.identity.*

CoreIdent metrics are intended to complement these with OAuth/OIDC-specific counters.

Overriding defaults

All store and service registrations use TryAdd* patterns so you can override them by registering your own implementations before calling AddCoreIdent() (or by replacing registrations explicitly).

Delegated user store adapter (integrate existing user systems)

If you already have an existing user system (database + credential verification) and you want CoreIdent to delegate user lookup and credential validation, use CoreIdent.Adapters.DelegatedUserStore.

Registration:

using CoreIdent.Adapters.DelegatedUserStore.Extensions;

builder.Services.AddCoreIdentDelegatedUserStore(o =>
{
    o.FindUserByIdAsync = (id, ct) => myUsers.FindByIdAsync(id, ct);
    o.FindUserByUsernameAsync = (username, ct) => myUsers.FindByUsernameAsync(username, ct);
    o.ValidateCredentialsAsync = (user, password, ct) => myUsers.ValidatePasswordAsync(user, password, ct);

    // Optional: provide claims that will be emitted into tokens.
    o.GetClaimsAsync = (subjectId, ct) => myUsers.GetClaimsAsync(subjectId, ct);
});

Notes:

  • The adapter replaces IUserStore and IPasswordHasher registrations.
  • CoreIdent will not store password hashes when using this adapter (credential validation is delegated).
  • You are responsible for:
    • secure credential storage and verification (hashing, rate limiting, lockout, MFA, etc.)
    • preventing credential leakage in logs and telemetry
    • ensuring usernames are normalized consistently with your system

3. Signing keys and JWKS

CoreIdent uses ISigningKeyProvider to:

  • provide SigningCredentials for token creation
  • provide validation keys for token verification / JWKS publishing

3.1 Configure signing keys (AddSigningKey)

AddSigningKey(...) configures CoreIdentKeyOptions using CoreIdentKeyOptionsBuilder:

  • UseRsa(string keyPath)
  • UseRsaPem(string pemString)
  • UseEcdsa(string keyPath)
  • UseSymmetric(string secret) (development/testing only)

RSA provider notes

  • The RSA provider (RsaSigningKeyProvider) supports:
    • PEM string
    • PEM file
    • X509 certificate (PFX)
    • fallback: generate an ephemeral RSA key with a warning
  • Key IDs (kid) are computed from the SHA-256 hash of the public key.

ECDSA provider notes

  • The ECDSA provider supports:
    • PEM string
    • PEM file
    • X509 certificate
    • fallback: generate ephemeral P-256 key (ES256) with a warning

Symmetric (HS256) notes

  • SymmetricSigningKeyProvider logs a warning at startup.
  • Symmetric keys are not published via JWKS.
  • Secret must be at least 32 bytes.

3.2 JWKS endpoint

The JWKS endpoint returns a JSON Web Key Set:

  • RSA keys are output with kty=RSA, n, e, kid, alg=RS256
  • EC keys are output with kty=EC, crv, x, y, kid, alg=ES256
  • Symmetric keys are skipped (not published)

This is intentionally “safe by default” (no shared secrets exposed).


4. Endpoints and flows

4.1 Endpoint mapping overview

Map everything

app.MapCoreIdentEndpoints() maps all current endpoints.

Map only what you want

If you want a more controlled surface area, map individual endpoints:

  • MapCoreIdentOpenIdConfigurationEndpoint(coreOptions, routeOptions)
  • MapCoreIdentDiscoveryEndpoints(jwksPath)
  • MapCoreIdentAuthorizeEndpoint(authorizePath)
  • MapCoreIdentConsentEndpoints(consentPath)
  • MapCoreIdentTokenEndpoint(tokenPath)
  • MapCoreIdentTokenManagementEndpoints(revokePath, introspectPath)
  • MapCoreIdentResourceOwnerEndpoints(registerPath, loginPath, profilePath)
  • MapCoreIdentUserInfoEndpoint(userInfoPath)
  • MapCoreIdentPasswordlessEmailEndpoints(startPath, verifyPath)
  • MapCoreIdentPasswordlessSmsEndpoints(startPath, verifyPath)

4.2 OIDC discovery document

The discovery endpoint returns an OpenIdConfigurationDocument that includes:

  • issuer
  • jwks_uri
  • token_endpoint
  • revocation_endpoint
  • introspection_endpoint
  • scopes_supported (pulled from IScopeStore.GetAllAsync(), filtered by ShowInDiscoveryDocument)
  • id_token_signing_alg_values_supported (from ISigningKeyProvider.Algorithm)

grant_types_supported is currently returned as an empty list in the implementation (even though token endpoint supports multiple grants). This is an implementation detail worth revisiting, but the guide documents current behavior.


4.3 Token endpoint (POST /auth/token)

The token endpoint expects:

  • Content-Type: application/x-www-form-urlencoded
  • Client authentication:
    • Prefer Authorization: Basic base64(client_id:client_secret)
    • Or form params client_id, client_secret

Supported grant types

  • client_credentials
  • refresh_token
  • authorization_code
  • password (deprecated; logs warning)

Token response

The endpoint returns TokenResponse:

  • access_token (JWT)
  • token_type ("Bearer")
  • expires_in (seconds)
  • refresh_token (when applicable)
  • scope (space-delimited, when applicable)
  • id_token (only for authorization_code when scope includes openid)

Scopes behavior

  • If the request does not include scope, CoreIdent grants the client’s AllowedScopes.
  • If the request includes scope, CoreIdent intersects requested scopes with client’s AllowedScopes.

4.4 Refresh tokens

Refresh tokens are stored via IRefreshTokenStore as CoreIdentRefreshToken records.

Important security properties:

  • Refresh token rotation is implemented:
    • The old token is consumed (ConsumedAt set).
    • A new token is issued.
  • Theft detection is implemented:
    • If a consumed refresh token is reused, CoreIdent revokes the entire token family (FamilyId).

Offline access gating

Refresh tokens are only minted in some flows when:

  • client has AllowOfflineAccess = true, and
  • granted scopes include offline_access

4.5 Authorization code flow (PKCE)

CoreIdent implements:

  • Authorization endpoint: GET /auth/authorize
  • Consent endpoint (when client requires consent): GET/POST /auth/consent
  • Token exchange: POST /auth/token with grant_type=authorization_code

/auth/authorize requirements

  • client_id (required)
  • redirect_uri (required)
  • response_type=code (required)
  • state (required)
  • PKCE is required:
    • code_challenge (required)
    • code_challenge_method=S256 (required)
  • scope (optional)
  • nonce (optional; used for id_token issuance)

Authentication requirement

The authorize endpoint uses ASP.NET authentication:

  • If HttpContext.User.Identity.IsAuthenticated is false, it returns Results.Challenge().

That means your host app must configure authentication (cookies, external provider, etc.) for interactive login.

Consent

If client.RequireConsent == true:

  • CoreIdent checks IUserGrantStore.HasUserGrantedConsentAsync(...).
  • If consent is missing, it redirects to ConsentPath.
  • Consent UI is a minimal HTML form.

Authorization code store

Authorization codes are stored via IAuthorizationCodeStore.

  • Default is InMemoryAuthorizationCodeStore.
  • Codes are single-use (ConsumedAt is set on consume).
  • Cleanup runs periodically via AuthorizationCodeCleanupHostedService.

4.6 Resource-owner convenience endpoints

CoreIdent includes non-OIDC convenience endpoints for quick “auth for my app” workflows:

  • GET/POST /auth/register
  • GET/POST /auth/login
  • GET /auth/profile

Content negotiation

These endpoints return JSON when:

  • Accept: application/json, or
  • Content-Type: application/json

Otherwise they return minimal HTML.

/auth/profile token validation

/auth/profile validates bearer tokens itself using:

  • ISigningKeyProvider.GetValidationKeysAsync()
  • Microsoft.IdentityModel.JsonWebTokens.JsonWebTokenHandler.ValidateTokenAsync

It does not rely on ASP.NET JwtBearer middleware.

This makes it useful for quick starts, but if you’re building a larger system you’ll typically configure JWT authentication in the host instead.

Customizing resource-owner responses

You can override result handling using:

builder.Services.ConfigureResourceOwnerEndpoints(options =>
{
    options.RegisterHandler = (http, user, ct) => Task.FromResult<IResult?>(Results.Redirect("/welcome"));
    options.LoginHandler = (http, user, tokens, ct) => Task.FromResult<IResult?>(Results.Json(new { tokens.AccessToken }));
    options.ProfileHandler = (http, user, claims, ct) => Task.FromResult<IResult?>(Results.Ok());
});

Returning null falls back to CoreIdent defaults.


4.6.1 Error responses (RFC 7807) and correlation IDs

For endpoints that are not required to return OAuth 2.0 error objects (for example, the resource-owner and passwordless convenience endpoints), CoreIdent returns RFC 7807 Problem Details for JSON clients.

When you send Accept: application/json (or Content-Type: application/json where applicable), error responses are returned with:

  • Content-Type: application/problem+json
  • A standard Problem Details body containing:
    • type (currently about:blank)
    • title
    • status
    • detail
    • instance
    • extension members:
      • error_code
      • correlation_id
      • trace_id

CoreIdent also emits an X-Correlation-Id response header for requests handled by CoreIdent endpoints. If the client provides an X-Correlation-Id request header, CoreIdent will reuse it; otherwise it will fall back to the current activity id / trace identifier.

Notes

  • OAuth/OIDC endpoints such as /auth/token, /auth/revoke, and /auth/introspect continue to return OAuth 2.0 style error objects (error, error_description) as required by their respective RFCs.
  • Structured logging uses a scope that includes correlation_id and trace_id for easier log correlation.
  • Passwordless endpoints avoid logging raw email/phone values; values are masked/redacted.

4.7 Passwordless email magic link (Feature 1.1)

CoreIdent provides a simple passwordless flow using email magic links:

  • POST /auth/passwordless/email/start — request a sign-in link
  • GET /auth/passwordless/email/verify?token=... — verify the token, create/find the user, and issue tokens

4.7.1 Start endpoint (POST /auth/passwordless/email/start)

Request body (JSON):

{ "email": "user@example.com" }

Behavior:

  • Always returns 200 OK (does not leak whether a user exists)
  • Generates a secure random token and stores only a hash via IPasswordlessTokenStore
  • Enforces per-email rate limiting (PasswordlessEmailOptions.MaxAttemptsPerHour)
  • Sends an email using IEmailSender with a link to the verify endpoint

4.7.2 Verify endpoint (GET /auth/passwordless/email/verify)

Behavior:

  • Validates and consumes the token (single-use)
  • Creates the user if not found (IUserStore)
  • Issues an access token + refresh token
  • If PasswordlessEmailOptions.SuccessRedirectUrl is set, redirects with tokens using the configured TokenDelivery mode:
Mode Delivery Notes
QueryString (default) ?access_token=...&... Simplest; tokens may appear in server logs and Referer headers
Fragment #access_token=...&... More secure — fragments are never sent to the server, but require client-side JS to extract
AuthorizationCode Planned Server issues a short-lived code the client exchanges for tokens. See DEVPLAN.md

Note: Fragment delivery is a token delivery mechanism for the passwordless email verify endpoint only. It is not the OAuth implicit grant (response_type=token). No OAuth authorization endpoint, client_id, or response_type is involved.

If the token is invalid, expired, or already consumed, it returns 400 Bad Request.

4.7.3 Configuration (PasswordlessEmailOptions)

builder.Services.Configure<PasswordlessEmailOptions>(opts =>
{
    opts.TokenLifetime = TimeSpan.FromMinutes(15);
    opts.MaxAttemptsPerHour = 5;
    opts.EmailSubject = "Sign in to {AppName}";

    // Relative by default; combined with CoreIdentRouteOptions.BasePath
    opts.VerifyEndpointUrl = "passwordless/email/verify";

    // Optional redirect that receives tokens
    opts.SuccessRedirectUrl = "https://client.example/signed-in";

    // Token delivery mode: QueryString (default), Fragment, or AuthorizationCode (planned)
    opts.TokenDelivery = TokenDeliveryMode.Fragment;

    // Optional custom HTML template
    // opts.EmailTemplatePath = "EmailTemplates/passwordless.html";
});

4.7.4 SMTP configuration (default SmtpEmailSender)

CoreIdent defaults IEmailSender to an SMTP implementation. Configure it via SmtpOptions.

Example appsettings.json:

{
  "SmtpOptions": {
    "Host": "smtp.example.com",
    "Port": 587,
    "EnableTls": true,
    "UserName": "smtp-user",
    "Password": "smtp-password",
    "FromAddress": "no-reply@example.com",
    "FromDisplayName": "CoreIdent"
  }
}

And bind options:

builder.Services.Configure<SmtpOptions>(builder.Configuration.GetSection("SmtpOptions"));

4.7.5 Email template customization

CoreIdent renders a simple HTML email containing a verify link.

  • Default template is built into PasswordlessEmailTemplateRenderer.
  • To provide your own template, set PasswordlessEmailOptions.EmailTemplatePath.
    • If the path is relative, it is resolved relative to IHostEnvironment.ContentRootPath.

Available placeholders:

  • {AppName}IHostEnvironment.ApplicationName
  • {Email} — recipient email
  • {VerifyUrl} — absolute verify URL

4.7.6 Provider email APIs (recommended for production)

Recommendation:

  • SMTP is great for demos and small self-hosted deployments.
  • For production, many teams prefer provider email APIs (SendGrid, Postmark, Mailgun, AWS SES, Azure Communication Services) for deliverability and operational convenience.

CoreIdent is designed to be extended without forking:

  1. Implement IEmailSender in your host app or a separate package.
  2. Register it in DI to override the default SMTP sender.

Example:

public sealed class MyProviderEmailSender : IEmailSender
{
    public Task SendAsync(EmailMessage message, CancellationToken ct = default)
    {
        // Call your provider API here.
        throw new NotImplementedException();
    }
}

builder.Services.AddSingleton<IEmailSender, MyProviderEmailSender>();
builder.Services.AddCoreIdent(...);

Because CoreIdent uses TryAdd* for defaults, registering IEmailSender before AddCoreIdent() will take precedence.


4.8 Passwordless SMS OTP (Feature 1.3)

CoreIdent provides a simple passwordless flow using SMS one-time passcodes:

  • POST /auth/passwordless/sms/start — request an OTP
  • POST /auth/passwordless/sms/verify — verify the OTP, create/find the user, and issue tokens

4.8.1 Start endpoint (POST /auth/passwordless/sms/start)

Request body (JSON):

{ "phone_number": "+15551234567" }

Optionally, you can provide a message_prefix that will be prepended to the OTP message:

{ "phone_number": "+15551234567", "message_prefix": "Your login code:" }

For form posts, the corresponding field names are:

  • phone_number
  • message_prefix

Phone numbers must be in E.164 format (for example: +15551234567). CoreIdent applies minimal normalization before validation (trims whitespace, removes spaces/hyphens/parentheses, and converts a leading 00 prefix to +).

Behavior:

  • Always returns 200 OK (does not leak whether a user exists)
  • If the phone number is missing or invalid, CoreIdent still returns 200 OK with the same response message
  • Generates a 6-digit numeric OTP and stores only a hash via IPasswordlessTokenStore
  • Enforces per-phone rate limiting (PasswordlessSmsOptions.MaxAttemptsPerHour)
  • Sends an SMS using ISmsProvider

4.8.2 Verify endpoint (POST /auth/passwordless/sms/verify)

Request body (JSON):

{ "phone_number": "+15551234567", "otp": "123456" }

Behavior:

  • Validates and consumes the OTP (single-use)
  • Creates the user if not found (IUserStore)
  • Issues an access token + refresh token

If the phone number is missing or invalid, or if the OTP is invalid, expired, or already consumed, it returns 400 Bad Request.

4.8.3 Configuration (PasswordlessSmsOptions)

builder.Services.Configure<PasswordlessSmsOptions>(opts =>
{
    opts.OtpLifetime = TimeSpan.FromMinutes(5);
    opts.MaxAttemptsPerHour = 5;
});

4.8.4 Providing an SMS provider

CoreIdent registers a default ConsoleSmsProvider for development.

To use a real provider, register your own ISmsProvider implementation:

builder.Services.AddSingleton<ISmsProvider, MySmsProvider>();

Account Recovery / Password Reset

CoreIdent provides built-in password reset functionality for apps that use password-based authentication.

Flow

  1. User requests reset: POST /auth/account/recover with email address
  2. CoreIdent generates a secure token, stores it hashed, and sends a reset link via IEmailSender
  3. User clicks link: GET /auth/account/reset-password?token=xxx displays a reset form
  4. User submits new password: POST /auth/account/reset-password with token and new password
  5. CoreIdent validates the token, hashes the new password, and updates the user

Configuration

builder.Services.AddCoreIdent(options => { ... })
    .AddPasswordReset(reset =>
    {
        reset.TokenLifetime = TimeSpan.FromMinutes(30);
        reset.MaxAttemptsPerHour = 3;
        reset.EmailSubject = "Reset your password";
    });

Security notes

  • The recover endpoint always returns success regardless of whether the email exists (prevents email enumeration)
  • Reset tokens are single-use and time-limited
  • Rate limiting is applied per email address
  • The reset form can be replaced by providing a custom handler

4.9 UserInfo endpoint (GET /auth/userinfo, Feature 1.10)

CoreIdent exposes a minimal OpenID Connect UserInfo endpoint:

  • GET /auth/userinfo

Behavior:

  • Requires a valid bearer access token
  • Requires that the access token includes the openid scope
  • Returns a JSON object that always includes:
    • sub

Scope to claims mapping

CoreIdent returns claims based on granted scopes:

  • openid
    • sub
  • profile
    • name, family_name, given_name, middle_name, nickname, preferred_username, profile, picture, website, gender, birthdate, zoneinfo, locale, updated_at
  • email
    • email, email_verified
  • address
    • address
  • phone
    • phone_number, phone_number_verified

Claims are sourced from:

  • IUserStore.GetClaimsAsync(subjectId)
  • ICustomClaimsProvider.GetIdTokenClaimsAsync(...)

Claims not granted by scope are omitted.


5. Token revocation and resource server enforcement

5.1 Revocation endpoint (POST /auth/revoke, RFC 7009)

  • Requires form content type
  • Requires client authentication for confidential clients
  • Returns 200 OK even for invalid/unknown tokens (privacy semantics)

Revocation behavior:

  • If token looks like a JWT (or token_type_hint=access_token):
    • CoreIdent validates signature (without issuer/audience checks), extracts jti, and stores revocation via ITokenRevocationStore.
  • Otherwise (or token_type_hint=refresh_token):
    • CoreIdent attempts refresh token revocation via IRefreshTokenStore.RevokeAsync.

Client ownership checks:

  • If token belongs to a different client, CoreIdent returns 200 OK but does not revoke.

5.2 Enforcing revocation for JWTs

JWTs are stateless; revocation only works if resource servers check revocation status.

CoreIdent provides middleware:

  • app.UseCoreIdentTokenRevocation()

This middleware checks:

  • If the current request is authenticated
  • If the principal contains a jti claim
  • If ITokenRevocationStore.IsRevokedAsync(jti) is true → respond 401 Unauthorized

Typical resource server pipeline

app.UseAuthentication();
app.UseCoreIdentTokenRevocation();
app.UseAuthorization();

Notes:

  • You still need JWT validation (AddAuthentication().AddJwtBearer(...)) in the resource server.
  • Revocation middleware adds the “online check” layer.

6. Token introspection (POST /auth/introspect, RFC 7662)

  • Requires form content type
  • Requires client authentication (resource server credentials)
  • Returns:
    • { "active": false } for unknown/invalid/expired/revoked tokens
    • RFC 7662-compatible payload for active tokens

Introspection supports:

  • Access tokens (JWT): validates signature, checks expiry, checks revocation store
  • Refresh tokens: checks store state (IsRevoked, ConsumedAt, ExpiresAt)

7. Clients and scopes

7.1 Clients (CoreIdentClient)

CoreIdentClient is the central configuration for OAuth clients.

Notable fields:

  • ClientId
  • ClientSecretHash (confidential clients)
  • ClientType (Public or Confidential)
  • AllowedGrantTypes
  • AllowedScopes
  • RedirectUris
  • RequirePkce
  • RequireConsent
  • AllowOfflineAccess
  • Token lifetimes:
    • AccessTokenLifetimeSeconds
    • RefreshTokenLifetimeSeconds

Client authentication

  • Confidential clients must authenticate and must have a secret.
  • Public clients should use PKCE and typically don’t use a client secret.

7.2 Scopes (CoreIdentScope and StandardScopes)

CoreIdent includes standard scope constants:

  • openid
  • profile
  • email
  • address
  • phone
  • offline_access

Default in-memory scope store seeds these standard scopes.


8. Persistence with EF Core

The EF Core package provides:

  • CoreIdentDbContext
  • Entities:
    • ClientEntity, ScopeEntity, RefreshTokenEntity, AuthorizationCodeEntity, UserGrantEntity, UserEntity, RevokedToken
  • Store implementations

8.1 Registration order

When using EF-backed stores, use this order:

  1. AddCoreIdent(...)
  2. AddDbContext<CoreIdentDbContext>(...)
  3. AddEntityFrameworkCoreStores()

Example:

builder.Services.AddCoreIdent(o =>
{
    o.Issuer = "https://issuer.example";
    o.Audience = "https://resource.example";
});

builder.Services.AddSigningKey(o => o.UseRsa("/path/to/private-key.pem"));

builder.Services.AddDbContext<CoreIdentDbContext>(options =>
    options.UseSqlite(builder.Configuration.GetConnectionString("DefaultConnection")));

builder.Services.AddEntityFrameworkCoreStores();

8.2 Migrations

CoreIdent does not run migrations for you. Your host app owns migrations and schema management.

  • Use EnsureCreated() for quick dev prototypes
  • Use migrations for production

9. Testing CoreIdent

CoreIdent includes a dedicated testing package:

  • tests/CoreIdent.Testing

9.1 Core fixtures

CoreIdentWebApplicationFactory

  • Uses SQLite in-memory
  • Registers EF Core stores via AddEntityFrameworkCoreStores()
  • Seeds standard scopes via StandardScopes.All

CoreIdentTestFixture

Provides:

  • Client (HttpClient)
  • Services (IServiceProvider)
  • Helpers:
    • CreateUserAsync(...)
    • CreateClientAsync(...)
    • AuthenticateAsAsync(user) (sets test headers for the test auth scheme)

9.2 Builders

  • UserBuilder
    • .WithEmail(...), .WithPassword(...), .WithClaim(...)
  • ClientBuilder
    • .WithClientId(...), .WithSecret(...), .WithGrantTypes(...), .WithScopes(...), .WithRedirectUris(...), .RequireConsent(...), .AsPublicClient(), etc.

9.3 Assertion helpers

  • HttpResponseAssertionExtensions
    • .ShouldBeSuccessful()
    • .ShouldBeSuccessfulWithContent<T>()
    • .ShouldBeUnauthorized()
    • .ShouldBeBadRequest(contains: ...)

There is also a JwtAssertionExtensions helper (see tests/CoreIdent.Testing/Extensions/JwtAssertionExtensions.cs).


10. Tutorials

10.1 Tutorial: issue a client_credentials token

  1. Register a confidential client in the store with:
    • AllowedGrantTypes containing client_credentials
    • AllowedScopes containing your API scope (e.g. api)
  2. Call:
POST /auth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)

grant_type=client_credentials&scope=api
  1. Validate the returned access_token using your JWT validation stack + the published JWKS.

10.2 Tutorial: refresh a token

  1. Ensure client supports refresh_token and (when minted from interactive/user flows) has AllowOfflineAccess=true.
  2. Exchange:
POST /auth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)

grant_type=refresh_token&refresh_token=<handle>

CoreIdent will rotate refresh tokens and detect reuse.

10.3 Tutorial: authorization code + PKCE (with consent)

  1. Configure your host app authentication (cookies, external provider, etc.) so /auth/authorize can authenticate a user.
  2. Configure client:
    • AllowedGrantTypes includes authorization_code
    • RedirectUris includes your redirect URI
    • RequirePkce=true
    • Optionally RequireConsent=true
  3. Start auth request:
GET /auth/authorize?
  response_type=code&
  client_id=...&
  redirect_uri=https%3A%2F%2Fapp.example%2Fcallback&
  scope=openid%20profile&
  state=...&
  code_challenge=...&
  code_challenge_method=S256
  1. If consent is required, CoreIdent redirects to /auth/consent.
  2. After consent + auth, CoreIdent redirects to your redirect URI with code and state.
  3. Exchange the code:
POST /auth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic base64(client_id:client_secret)

grant_type=authorization_code&code=...&redirect_uri=...&code_verifier=...

If the granted scopes include openid, CoreIdent also returns an id_token.


11. Troubleshooting

Common startup failures

  • Issuer/Audience not configured

    • CoreIdentOptions validation fails on start.
  • Signing key not configured

    • RSA/ECDSA providers will generate ephemeral keys with warnings.
    • Symmetric provider requires a 32+ byte secret.

Common endpoint issues

  • /auth/token returns 400

    • Ensure Content-Type is application/x-www-form-urlencoded.
  • /auth/token returns 401 invalid_client

    • Ensure the client exists and is enabled.
    • For confidential clients, ensure the secret is correct.
  • /auth/authorize returns challenge

    • Your host app hasn’t configured authentication, or the user is not signed in.
  • JWT revocation appears to “not work”

    • Ensure the resource server uses UseCoreIdentTokenRevocation() (or performs introspection).

12. Security guidance (practical)

  • Use RSA (RS256) or ECDSA (ES256) in production.
  • Keep access tokens short-lived.
  • Prefer refresh token rotation and revoke on suspicion.
  • Never publish symmetric signing secrets in JWKS (CoreIdent does not).
  • Do not log secrets, tokens, or private keys.

Appendix A: Where to look in code

  • DI registration:
    • src/CoreIdent.Core/Extensions/ServiceCollectionExtensions.cs
  • Endpoint aggregation:
    • src/CoreIdent.Core/Extensions/EndpointRouteBuilderExtensions.cs
  • Token endpoint:
    • src/CoreIdent.Core/Endpoints/TokenEndpointExtensions.cs
  • Revocation / introspection:
    • src/CoreIdent.Core/Endpoints/TokenManagementEndpointsExtensions.cs
  • Discovery/JWKS:
    • src/CoreIdent.Core/Endpoints/DiscoveryEndpointsExtensions.cs
  • Authorize/consent:
    • src/CoreIdent.Core/Endpoints/AuthorizationEndpointExtensions.cs
    • src/CoreIdent.Core/Endpoints/ConsentEndpointExtensions.cs
  • Revocation enforcement middleware:
    • src/CoreIdent.Core/Middleware/TokenRevocationMiddleware.cs
  • EF Core:
    • src/CoreIdent.Storage.EntityFrameworkCore/CoreIdentDbContext.cs
    • src/CoreIdent.Storage.EntityFrameworkCore/Extensions/ServiceCollectionExtensions.cs
  • Test infrastructure:
    • tests/CoreIdent.Testing/Fixtures/CoreIdentTestFixture.cs
    • tests/CoreIdent.Testing/Fixtures/CoreIdentWebApplicationFactory.cs