Skip to content

joesdu/EasilyNET.IdentityServer

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

48 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

EasilyNET.IdentityServer

A lightweight, OAuth 2.1 / OpenID Connect authorization server for .NET 11, with modern client authentication (private_key_jwt, mTLS, DPoP), interactive authorization, pluggable persistence, and an accompanying admin console.

English | 中文


Table of contents


Overview

EasilyNET.IdentityServer is a layered OAuth 2.1 / OIDC server built around the OAuth 2.1 draft defaults:

  • authorization code with mandatory PKCE (S256 only by default)
  • refresh-token rotation with family replay revocation
  • client credentials and the device authorization grant (RFC 8628)
  • OpenID Connect discovery, JWKS, userinfo, and RP-Initiated / Back-Channel logout
  • dynamic client registration (RFC 7591)
  • modern client authentication: private_key_jwt (RFC 7523), mutual TLS (RFC 8705), and DPoP (RFC 9449)
  • RFC 9068 JWT access tokens (typ: at+jwt, space-delimited scope)

The repository also ships:

  • a Razor Pages admin console (EF Core + SQLite) for managing clients, resources, scopes, identity resources, persisted grants, and the interactive authorization screens (login / consent / device verification)
  • unit and integration tests for protocol behavior, security rules, and core services
  • a Chinese translation of the OAuth 2.1 draft under docs/

Status: The Host project is a development sample wired with in-memory stores and a deliberately simple "logged-in user" convention (see Known limitations). The protocol engine, client authentication, token service, and persistence layers are production-shaped; swap the in-memory stores for EF Core or MongoDB and provide a real IProfileService / user-session integration for production.


What's implemented

Supported grants

Grant Notes
authorization_code PKCE required; S256 only unless plain is explicitly enabled per-server and per-client
refresh_token Rotation + sliding/absolute lifetime; reuse revokes the whole token family
client_credentials Confidential clients only
urn:ietf:params:oauth:grant-type:device_code RFC 8628 with slow_down / authorization_pending polling

Implicit, password, and hybrid flows are not supported (per OAuth 2.1).

Supported client authentication methods

client_secret_basic, client_secret_post, private_key_jwt, tls_client_auth, self_signed_tls_client_auth, and none (public clients).

OpenID Connect

Discovery document, JWKS, userinfo (GET/POST), id_token with nonce and real auth_time, RP-Initiated Logout and Back-Channel Logout endpoints, and a scope→claims IProfileService abstraction.


Security posture

The implementation enforces the OAuth 2.1 / OIDC security model:

  • PKCE mandatory, S256 only by default; a code_verifier without a matching code_challenge (and vice-versa) is rejected.
  • Exact redirect_uri matching (RFC 8252 loopback variable-port is the only exception). client_id and redirect_uri are validated before any error is redirected, so an unregistered/mismatched redirect_uri is never used as an open-redirect target (RFC 6749 §4.1.2.1).
  • Refresh-token rotation with family replay revocation and absolute-lifetime caps.
  • Authorization-code single use: replay revokes the access/refresh tokens already issued from that code; consumption is atomic with optimistic concurrency.
  • Client secrets hashed with PBKDF2 (SHA-256, ≥100k iterations, per-secret random salt), verified in constant time; legacy SHA-256 hashes still validate for backward compatibility.
  • RFC 9068 access tokens: typ: at+jwt header, space-delimited scope claim, aud/iss/exp/iat/jti/client_id/sub.
  • DPoP (RFC 9449): proof typ/htm/htu/iat/jti/ath validation, asymmetric-alg allow-list, key-thumbprint (jkt) binding, and a per-(jkt,jti) replay cache.
  • mTLS (RFC 8705): certificate-bound clients via subject-DN and/or SHA-256 (x5t#S256) thumbprint, with certificate validity-period checks; discovery advertises tls_client_certificate_bound_access_tokens.
  • private_key_jwt (RFC 7523): audience pinned to the called endpoint, sub == client_id, required jti, and replay protection retained for the assertion's full lifetime.
  • Token introspection / revocation enforce client ownership (a client cannot introspect or revoke another client's token).
  • Resource errors follow RFC 6750 (WWW-Authenticate: Bearer/DPoP …); access tokens in the query string are rejected.
  • Rate limiting, audit logging, forwarded client-certificate handling, and security response headers (CSP, X-Frame-Options: DENY, nosniff, etc.).

Solution layout

Apps          │ Host (OAuth/OIDC server) │ Admin.UI (Razor Pages admin + interaction UI)
Core          │ Token, authorization, client-auth, DPoP, mTLS, DCR, audit, rate limiting
Abstractions  │ Domain models, store/service interfaces, options
Persistence   │ EF Core (Sqlite / SqlServer / MySQL / PostgreSQL) │ MongoDB
Project Purpose
src/EasilyNET.IdentityServer.Abstractions Domain models, store/service contracts, IdentityServerOptions
src/EasilyNET.IdentityServer.Core Token issuance/validation, client auth, authorization, DPoP, mTLS, dynamic registration, auditing, rate limiting
src/EasilyNET.IdentityServer.DataAccess.Abstractions Persistence contracts (IIdentityServerDbContext)
src/EasilyNET.IdentityServer.DataAccess.EFCore EF Core entities, DbContext, and store implementations (IDbContextFactory-based)
src/EasilyNET.IdentityServer.DataAccess.EFCore.{Sqlite,SqlServer,MySQL,PostgreSQL} Provider wiring
src/EasilyNET.IdentityServer.DataAccess.MongoDB MongoDB store implementations
src/EasilyNET.IdentityServer.Host The OAuth/OIDC server app with in-memory development stores
src/EasilyNET.IdentityServer.Admin.UI Razor Pages admin console (clients, resources, grants, device) and interactive authorization screens
tests/EasilyNET.IdentityServer.IntegrationTests End-to-end protocol tests (57)
tests/EasilyNET.IdentityServer.Core.Tests Unit tests for core services (30)

Endpoints

Endpoint Purpose
GET /.well-known/openid-configuration Discovery metadata
GET /.well-known/jwks JSON Web Key Set
GET /connect/authorize Authorization endpoint
GET /connect/authorize/context/{requestId} Retrieve interaction context (login / select-account / consent)
GET /connect/authorize/interaction/page/{requestId} Stable same-origin entry that redirects to the UI
POST /connect/authorize/interaction Continue login / account-selection / consent
POST /connect/token Token endpoint (all grants)
GET/POST /connect/userinfo OIDC UserInfo
POST /connect/introspect Token introspection (RFC 7662)
POST /connect/revocation Token revocation (RFC 7009)
POST /connect/register Dynamic client registration (RFC 7591)
POST /connect/device_authorization Device authorization (RFC 8628)
POST /connect/device_verify Device user-code verification (requires an authenticated user)
GET/POST /connect/logout RP-Initiated Logout
POST /connect/backchannel-logout Back-Channel Logout (verified logout_token)
POST /connect/verify Resource-server-facing access-token validation (bearer or DPoP)
GET /health Health check

Quick start

Prerequisites

  • .NET 11 SDK (preview)

Build & test

dotnet build EasilyNET.IdentityServer.slnx -v minimal
dotnet test  EasilyNET.IdentityServer.slnx -v minimal     # 30 unit + 57 integration tests

Run the server

cd src/EasilyNET.IdentityServer.Host
dotnet run

The development profile listens on https://localhost:7020 and http://localhost:5093; the issuer is https://localhost:7020.

Try it — client credentials

curl -k -X POST https://localhost:7020/connect/token \
  -d grant_type=client_credentials \
  -d client_id=console -d client_secret=secret \
  -d scope=api1

Try it — authorization code + PKCE

# 1. Build a PKCE pair, then open the authorize URL (the dev host accepts subject_id for the demo "logged-in" user):
GET https://localhost:7020/connect/authorize?response_type=code&client_id=spa
    &redirect_uri=http://localhost:3000/callback&scope=openid%20profile%20api1
    &code_challenge=<S256>&code_challenge_method=S256&subject_id=alice

# 2. Exchange the returned code:
curl -k -X POST https://localhost:7020/connect/token \
  -d grant_type=authorization_code -d client_id=spa \
  -d code=<code> -d redirect_uri=http://localhost:3000/callback \
  -d code_verifier=<verifier>

Seeded development clients

Client ID Type Purpose
console confidential client credentials (secret / secret)
mvc confidential authorization code + refresh token
spa public PKCE sign-in
interactive public consent-focused interaction demo
restricted-github public identity-provider restrictions
prompt-restricted public restricted prompt values
device public device authorization grant

Configuration (IdentityServerOptions)

Configure the server through AddIdentityServer:

builder.Services.AddIdentityServer(options =>
{
    options.Issuer = "https://localhost:7020";       // must exactly match the public URL
    options.AccessTokenLifetime = 3600;              // seconds
    options.RefreshTokenLifetime = 86400;            // seconds (sliding)
    options.AbsoluteRefreshTokenLifetime = 2592000;  // 30 days (hard cap)
    options.EnableAbsoluteRefreshTokenLifetime = true;
    options.AuthorizationCodeLifetime = 300;
    options.DeviceCodeLifetime = 300;
    options.RequirePkce = true;                      // OAuth 2.1
    options.AllowPlainTextPkce = false;              // S256 only
    options.RequireConsent = true;

    // Modern client authentication
    options.EnablePrivateKeyJwtClientAuthentication = true;
    options.EnableMutualTlsClientAuthentication = true;
    options.EnableDpop = true;
    options.DpopProofLifetimeSeconds = 300;

    // Dynamic client registration
    options.EnableDynamicClientRegistration = true;
    options.RequireInitialAccessTokenForDynamicClientRegistration = false;
    options.DynamicClientRegistrationInitialAccessToken = null;

    // Signing-algorithm allow-lists (asymmetric only)
    options.AllowedAccessTokenSigningAlgorithms   = ["RS256", "RS384", "RS512"];
    options.AllowedIdentityTokenSigningAlgorithms = ["RS256", "RS384", "RS512"];
    options.AllowedClientAssertionSigningAlgorithms = ["RS256", "RS384", "RS512"];
    options.AllowedDpopSigningAlgorithms          = ["RS256", "RS384", "RS512"];
});

Security note: keep the signing-algorithm allow-lists asymmetric (RS/ES/PS). Adding HS* would expose the JWT validators to key-confusion attacks. For production, also require an initial access token for dynamic registration (or front it with rate limiting).


Registering clients & resources

Clients are Client records served by an IClientStore. Secrets are stored hashed via SecretHasher.HashSecret:

new Client
{
    ClientId = "mvc",
    ClientName = "MVC Client",
    ClientType = ClientType.Confidential,
    AllowedGrantTypes = [GrantType.AuthorizationCode, GrantType.RefreshToken],
    RedirectUris = ["https://app.example.com/signin-oidc"],
    PostLogoutRedirectUris = ["https://app.example.com/signout-callback-oidc"],
    AllowedScopes = ["openid", "profile", "email", "api1"],
    ClientSecrets = [new() { Value = SecretHasher.HashSecret("secret") }],
    RequirePkce = true,
    RequireClientSecret = true,
    RequireConsent = false,
    // private_key_jwt: set TokenEndpointAuthMethod = "private_key_jwt" + Jwks / JwksUri
    // mTLS:            set TokenEndpointAuthMethod = "tls_client_auth" + TlsClientAuthSubjectDn / TlsClientAuthThumbprint
    // DPoP:            set RequireDpopProof = true
};

Resources (ApiResource, ApiScope, IdentityResource) are served by an IResourceStore. The openid identity resource is always advertised in scopes_supported.


Grant types & flows

Authorization code + PKCE

GET /connect/authorize validates the request, then either returns an interaction contract (HTTP 401/403/409 with an interaction_required body describing whether login, account selection, or consent is needed) or, once the user is established and consent satisfied, issues a code and redirects with code, state, and the RFC 9207 iss. The token exchange enforces PKCE, redirect_uri match, and single-use semantics.

Refresh tokens

Confidential clients receive a rotated refresh token on each use; presenting a consumed token revokes the entire token family. Requested scope on refresh must be a subset of the originally granted scope. Sliding and absolute lifetimes are enforced.

Client credentials

Confidential clients only; scopes are validated against AllowedScopes.

Device authorization (RFC 8628)

POST /connect/device_authorization returns device_code, user_code, verification_uri(_complete), expires_in, and interval. The token endpoint returns authorization_pending / slow_down until the user approves at /connect/device_verify. Approval derives the subject from the authenticated user context — never from the request body.


Client authentication methods

Method How to configure
client_secret_basic / client_secret_post RequireClientSecret = true + hashed ClientSecrets
private_key_jwt TokenEndpointAuthMethod = "private_key_jwt" + Jwks (inline JWK set) or JwksUri
tls_client_auth TokenEndpointAuthMethod = "tls_client_auth" + TlsClientAuthSubjectDn and/or TlsClientAuthThumbprint (40-hex SHA-1 or 64-hex SHA-256)
self_signed_tls_client_auth TokenEndpointAuthMethod = "self_signed_tls_client_auth" + registered cert binding
none public clients (ClientType.Public or RequireClientSecret = false)

The client certificate for mTLS is read from the TLS connection or a forwarded header (X-ARR-ClientCert etc.) via the client-certificate-forwarding middleware. DPoP can be combined with any method by setting RequireDpopProof = true.


Persistence

The development Host uses in-memory stores (registered as singletons). For real deployments, swap in one of the persistence packages:

EF Core (relational)

// e.g. SQLite
builder.Services.AddIdentityServerSqlite("Data Source=identityserver.db");
// also available: AddIdentityServerSqlServer / AddIdentityServerMySql / AddIdentityServerPostgreSql

EF stores resolve a short-lived DbContext per operation through IDbContextFactory<IdentityServerDbContext>, so they are safe to consume from the singleton core services (no captive DbContext / cross-request thread-safety hazard). Read paths use AsNoTracking, bulk cleanup uses ExecuteDelete/ExecuteUpdate, grant consumption uses optimistic concurrency, and hot lookup columns are indexed.

MongoDB

builder.Services.AddIdentityServerMongoDB(connectionString, databaseName: "IdentityServer");

Customization (profile, signing, stores)

  • User claims — implement IProfileService (GetProfileDataAsync / IsActiveAsync). The sample InMemoryProfileService returns scope-filtered claims for the demo users; UserInfo and the id_token draw from this service rather than fabricating claims.
  • Signing keys — the default registration uses PersistentSigningService (DB-backed, 30-day rotation, cached active key, JWKS exposure). For ephemeral dev keys, register DefaultSigningService.
  • Stores — implement IClientStore, IResourceStore, IPersistedGrantStore, IDeviceFlowStore, IUserConsentStore, ISigningKeyStore, IAuditLogStore, or use the provided EF Core / MongoDB implementations.
  • Account candidates — implement IAuthorizationAccountService to drive the select_account interaction.

Admin console

EasilyNET.IdentityServer.Admin.UI is a Razor Pages app with direct EF Core (SQLite) access. On first run it creates IdentityServer.db automatically.

cd src/EasilyNET.IdentityServer.Admin.UI
dotnet run            # https://localhost:49207 / http://localhost:49208

It provides CRUD for clients (/Clients), API resources (/Resources), API scopes (/Scopes), identity resources (/IdentityResources), and persisted grants (/Grants, with expired-grant cleanup), plus the interactive authorization screens: login (/Account/Login, demo admin/admin), consent (/Consent), and device verification (/Device). The console is built on Razor Pages + Bootstrap 5 (no Node/JS build step); management pages require an authenticated admin session (cookie authentication).


Testing

dotnet test EasilyNET.IdentityServer.slnx -v minimal
  • Integration tests (57) — discovery/JWKS, client credentials, authorization code + PKCE, refresh rotation & family replay, device flow, interaction (login / select-account / consent), introspection, revocation, resource verification, dynamic registration, private_key_jwt, mTLS, DPoP, and rate limiting.
  • Unit tests (30)SecretHasher (PBKDF2 + legacy compatibility) and RateLimitService.

Known limitations

The Host is a development sample, not a turnkey production deployment:

  • It has no real server-side login session. The authorization and device-verification endpoints establish the "authenticated user" from the principal, then fall back to an X-Subject-Id header / subject_id query parameter for the demo. Replace this with your real authentication/session integration before deploying.
  • InMemoryProfileService returns sample claims; provide a real IProfileService over your user store.
  • The logout endpoints fail closed: RP-Initiated Logout only redirects to a registered PostLogoutRedirectUris entry, and Back-Channel Logout requires a fully verified logout_token. Because the dev host has no session cookies, there is nothing to clear beyond the token grants it revokes.
  • In-memory replay/rate-limit caches are per-process; for horizontal scale-out, back them with a shared store (e.g. Redis) or use sticky routing.
  • Dynamic client registration is open by default in the sample; enable RequireInitialAccessTokenForDynamicClientRegistration for production.

License

MIT License. See LICENSE.

About

No description, website, or topics provided.

Resources

License

Stars

3 stars

Watchers

0 watching

Forks

Releases

No releases published

Packages

 
 
 

Contributors