CoreDesign.Identity.Server 1.0.7

dotnet add package CoreDesign.Identity.Server --version 1.0.7
                    
NuGet\Install-Package CoreDesign.Identity.Server -Version 1.0.7
                    
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="CoreDesign.Identity.Server" Version="1.0.7" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="CoreDesign.Identity.Server" Version="1.0.7" />
                    
Directory.Packages.props
<PackageReference Include="CoreDesign.Identity.Server" />
                    
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 CoreDesign.Identity.Server --version 1.0.7
                    
#r "nuget: CoreDesign.Identity.Server, 1.0.7"
                    
#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 CoreDesign.Identity.Server@1.0.7
                    
#: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=CoreDesign.Identity.Server&version=1.0.7
                    
Install as a Cake Addin
#tool nuget:?package=CoreDesign.Identity.Server&version=1.0.7
                    
Install as a Cake Tool

CoreDesign.Identity.Server

A lightweight, self-contained OIDC identity server library for ASP.NET Core. It provides a complete set of OIDC-compatible endpoints (discovery, JWKS, authorization, token issuance, and userinfo) as minimal API routes that can be mounted on any ASP.NET Core application in a few lines of code.

Intended for development and testing only. Passwords are stored in plaintext, the RSA signing key is persisted to %APPDATA%\coredesign-identity\ across restarts, and CORS is left open. Do not use in production.

What it provides

Endpoint Purpose
GET /.well-known/openid-configuration OIDC discovery document
GET /.well-known/jwks.json Public signing key (JWKS)
GET /connect/authorize Renders the login form for browser-based OIDC flows
POST /connect/authorize Processes login form submission, issues authorization code
POST /connect/token Token issuance via password grant or authorization code grant (application/x-www-form-urlencoded)
GET /connect/userinfo Returns claims for a valid bearer token
POST /get-token Convenience JSON token endpoint for tooling (non-standard, no client_id required)
POST /auth/login Direct JSON login endpoint for non-OIDC frontends (no client_id required)

Tokens are RS256-signed JWTs containing sub, email, preferred_username, name, given_name, family_name, oid, permissions, and any custom claims defined on the identity record.

Login flows

Browser login: Authorization Code with PKCE

This is the standard flow for Blazor and other browser-based apps. The browser redirects to /connect/authorize, the user enters credentials in the hosted login form, and the server redirects back with a short-lived authorization code. The client then exchanges the code for tokens at /connect/token.

Step 1 — browser redirects to the authorization endpoint

GET /connect/authorize
  ?response_type=code
  &client_id=my-blazor-app
  &redirect_uri=https%3A%2F%2Flocalhost%3A7070%2Fsignin-oidc
  &scope=openid%20profile%20email
  &state={opaque-value}
  &code_challenge={S256-hash-of-verifier}
  &code_challenge_method=S256

The server returns an HTML login form. When the user submits it, the server validates the credentials and redirects back:

302 https://localhost:7070/signin-oidc?code={code}&state={state}

If credentials are invalid the form is re-rendered with a 401 status and an error message.

Step 2 — client exchanges the code for tokens

POST /connect/token
Content-Type: application/x-www-form-urlencoded

grant_type=authorization_code
&client_id=my-blazor-app
&code={code}
&redirect_uri=https%3A%2F%2Flocalhost%3A7070%2Fsignin-oidc
&code_verifier={original-verifier}

Response (200)

{
  "access_token": "<jwt>",
  "id_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 28800,
  "scope": "openid profile email"
}

The access_token audience is the API resource (CoreDesign:Identity:Audience). The id_token audience is the client_id. These are distinct JWTs so that an API can validate the access token without knowing any client IDs.

Direct JSON login

POST /auth/login is a non-OIDC shortcut for frontends that manage their own token storage rather than going through an authorization server redirect. It accepts JSON credentials and returns a signed JWT directly.

Request

POST /auth/login
Content-Type: application/json

{
  "username": "alice@example.local",
  "password": "Password1!"
}

Response (200)

{
  "access_token": "<jwt>",
  "id_token": "<jwt>",
  "token_type": "Bearer",
  "expires_in": 28800,
  "scope": "openid profile email"
}

Response (401) on invalid credentials.

The identity server must have CORS enabled for browser clients to reach this endpoint. Call UseIdentityServerCors() before MapIdentityEndpoints() (see Setup).

Tooling token generation

POST /get-token accepts the same JSON credentials as /auth/login and returns the same response. Use it for tooling such as Scalar, Postman, or curl when you need a quick token without going through the browser flow.

If the credentials are invalid the endpoint returns 400 Bad Request:

{
  "error": "invalid_grant",
  "error_description": "Invalid username or password"
}

Setup

There are two hosting patterns depending on whether you want a fully self-contained web host or need to embed the identity endpoints in a larger application.

Use AddIdentityServerWebHost and MapIdentityServerWebHost. This reads all configuration from a single CoreDesign:IdentityWebHost section, registers the JSON file stores, enables CORS, and adds a simple landing page at /.

using CoreDesign.Identity.Server;

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ContentRootPath = AppContext.BaseDirectory
});

builder.Services.AddIdentityServerWebHost(builder.Configuration);

var app = builder.Build();

app.MapIdentityServerWebHost();

app.Run();

Add a CoreDesign:IdentityWebHost section to appsettings.json:

{
  "CoreDesign": {
    "IdentityWebHost": {
      "Issuer": "https://localhost:5003",
      "Audience": "https://api.example.local",
      "TokenLifetimeHours": 8,
      "IdentitiesFilePath": "identities.json",
      "ClientsFilePath": "clients.json"
    }
  }
}
Key Default Description
Issuer (required) Value placed in the iss claim and returned by the discovery endpoint. Must match the URL at which the identity server is reachable.
Audience (required) Value placed in the aud claim of access tokens. Typically your API's base URL.
KeyId coredesign-dev-signing-key kid header on the JWT and JWKS entry. Change this if you need multiple signing keys.
TokenLifetimeHours 8 Token validity window in hours.
IdentitiesFilePath identities.json Path to the identities file, relative to the output directory.
ClientsFilePath clients.json Path to the clients file, relative to the output directory.

Option B: Custom host

Use AddIdentityServer and MapIdentityEndpoints when you need to embed the identity endpoints in an existing application or want finer control over registration. In this case you register stores separately.

using CoreDesign.Identity.Server;

var builder = WebApplication.CreateBuilder(new WebApplicationOptions
{
    Args = args,
    ContentRootPath = AppContext.BaseDirectory
});

builder.Services.AddIdentityServer(builder.Configuration, sectionName: "CoreDesign:Identity");
builder.Services.AddJsonFileIdentityStore("identities.json");
builder.Services.AddJsonFileClientStore("clients.json");

var app = builder.Build();

app.UseIdentityServerCors(); // required for browser clients
app.MapIdentityEndpoints();

app.Run();

The CoreDesign:Identity section uses the same keys as CoreDesign:IdentityWebHost except IdentitiesFilePath and ClientsFilePath, which are passed directly to AddJsonFileIdentityStore and AddJsonFileClientStore.

Clients file

Add a clients.json file and set it to copy to the output directory:

<None Update="clients.json">
  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

The file is a JSON array of registered client records. A typical setup includes one client for browser-based login (authorization code with PKCE) and one for service-to-service calls (password grant):

[
  {
    "clientId": "my-blazor-app",
    "tokenEndpointAuthMethod": "none",
    "allowedGrantTypes": [ "authorization_code" ],
    "allowedRedirectUris": [ "https://localhost:7070/signin-oidc" ],
    "allowedPostLogoutRedirectUris": [
      "https://localhost:7070/signout-callback-oidc",
      "https://localhost:7070/"
    ],
    "allowedScopes": [ "openid", "profile", "email" ],
    "requirePkce": true
  },
  {
    "clientId": "my-api-dev",
    "tokenEndpointAuthMethod": "none",
    "allowedGrantTypes": [ "password" ],
    "allowedScopes": [ "openid", "profile", "email" ],
    "requirePkce": false
  }
]
Field Type Description
clientId string Unique identifier for the client. Case-sensitive. Must be sent as client_id in every /connect/token request.
clientSecret string or null Optional shared secret. Null for public clients (SPAs, CLI tools, PKCE flows).
tokenEndpointAuthMethod string "none" for public clients, "client_secret_post" for confidential clients.
allowedGrantTypes string[] Grant types this client may use: "authorization_code" for browser flows, "password" for service-to-service.
allowedRedirectUris string[] Pre-registered redirect URIs. Required for authorization code clients. Exact string match.
allowedPostLogoutRedirectUris string[] Pre-registered post-logout redirect URIs.
allowedScopes string[] Scopes this client is permitted to request.
requirePkce bool When true, /connect/authorize rejects requests without a valid code_challenge. Always set to true for browser-based clients.

Identities file

Add an identities.json file and set it to copy to the output directory:

<None Update="identities.json">
  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

The file is a JSON array of identity records:

[
  {
    "userId": "11111111-1111-1111-1111-111111111111",
    "username": "admin@example.local",
    "password": "Password1!",
    "email": "admin@example.local",
    "name": "Admin User",
    "givenName": "Admin",
    "familyName": "User",
    "permissions": [ "items:read", "items:write" ],
    "customClaims": {}
  }
]
Field Type Description
userId string (GUID) Value used for the sub and oid claims
username string Login username. Comparison is case-insensitive.
password string Plaintext password. Comparison is case-sensitive.
email string email claim
name string name claim
givenName string given_name claim
familyName string family_name claim
permissions string[] Each value emitted as a separate permissions claim
customClaims object Arbitrary key-value pairs added as additional claims

Blazor app integration

A Blazor Server app authenticates against this identity server using ASP.NET Core's built-in OIDC middleware. The app gets an auth cookie containing the access token, which it then attaches to outbound API requests.

1. Register authentication

builder.Services
    .AddAuthentication(options =>
    {
        options.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
        options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme;
    })
    .AddCookie(options =>
    {
        options.LoginPath = "/account/login";
        options.LogoutPath = "/account/logout";
    })
    .AddOpenIdConnect(options =>
    {
        options.Authority = "https://localhost:5003"; // identity server URL
        options.ClientId = "my-blazor-app";
        options.ResponseType = "code";
        options.UsePkce = true;
        options.SaveTokens = true;
        options.GetClaimsFromUserInfoEndpoint = true;
        options.RequireHttpsMetadata = false; // dev only
        options.Scope.Clear();
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("email");
    });

2. Map login and logout endpoints

app.MapGet("/account/login", (string? returnUrl, HttpContext ctx) =>
    ctx.ChallengeAsync(OpenIdConnectDefaults.AuthenticationScheme,
        new AuthenticationProperties { RedirectUri = returnUrl ?? "/" }))
    .AllowAnonymous();

app.MapGet("/account/logout", async (HttpContext ctx) =>
{
    await ctx.SignOutAsync(CookieAuthenticationDefaults.AuthenticationScheme);
    await ctx.SignOutAsync(OpenIdConnectDefaults.AuthenticationScheme);
});

3. Forward the access token to downstream APIs

public class BearerTokenHandler(IHttpContextAccessor accessor) : DelegatingHandler
{
    protected override async Task<HttpResponseMessage> SendAsync(
        HttpRequestMessage request, CancellationToken cancellationToken)
    {
        var token = await accessor.HttpContext!
            .GetTokenAsync("access_token");
        if (token is not null)
            request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", token);
        return await base.SendAsync(request, cancellationToken);
    }
}

// Registration:
builder.Services.AddTransient<BearerTokenHandler>();
builder.Services.AddHttpClient<MyApiClient>(c => c.BaseAddress = new Uri("https://my-api"))
    .AddHttpMessageHandler<BearerTokenHandler>();

4. Register the Blazor client in clients.json

The redirect_uri must exactly match what the OIDC middleware sends. For a local Blazor app on port 7070:

{
  "clientId": "my-blazor-app",
  "tokenEndpointAuthMethod": "none",
  "allowedGrantTypes": [ "authorization_code" ],
  "allowedRedirectUris": [ "https://localhost:7070/signin-oidc" ],
  "allowedPostLogoutRedirectUris": [
    "https://localhost:7070/signout-callback-oidc",
    "https://localhost:7070/"
  ],
  "allowedScopes": [ "openid", "profile", "email" ],
  "requirePkce": true
}

Authority and port stability

The Issuer in appsettings.json must match the URL at which the identity server is actually reachable, including port. If you run under .NET Aspire, pin the identity server to a fixed port so the Blazor app always finds it at the same address:

// In AppHost
builder.AddProject<SampleApi_Identity_Web>("SampleApiIdentityApi")
    .WithHttpsEndpoint(port: 5003, name: "https", isProxied: false);

// In Blazor app setup, read the authority from Aspire service discovery:
var authority =
    configuration["services:SampleApiIdentityApi:https:0"]  // Aspire-injected
    ?? configuration["IdentityApi:BaseUrl"]                 // appsettings fallback
    ?? throw new InvalidOperationException("OIDC authority not configured");

Custom stores

Custom identity store

AddJsonFileIdentityStore is a convenience wrapper around IIdentityStore. To use a different backing source (database, in-memory list, etc.) implement the interface and register it directly:

public class MyIdentityStore : IIdentityStore
{
    public Task<IdentityRecord?> FindByCredentialsAsync(string username, string password) { ... }
    public Task<IdentityRecord?> FindByIdAsync(string userId) { ... }
}

builder.Services.AddSingleton<IIdentityStore, MyIdentityStore>();

Custom client store

AddJsonFileClientStore is a convenience wrapper around IClientStore. For a different backing source implement the interface and register it directly:

public class MyClientStore : IClientStore
{
    public Task<ClientRecord?> FindByClientIdAsync(string clientId) { ... }
}

builder.Services.AddSingleton<IClientStore, MyClientStore>();

Template customization

The login form and the identity web host landing page are rendered from HTML template files shipped as embedded resources inside the library. Both pages can be restyled or restructured without modifying the library by placing override files in an identity-templates folder at your host project's content root.

How overrides work

When the library renders a page it checks {ContentRoot}/identity-templates/{filename} first. If the file exists it is used as-is. If not, the embedded default is loaded. No configuration is required.

Overriding the login form

  1. Create identity-templates/login.html in your host project.
  2. Set it to copy to the output directory:
<None Update="identity-templates\login.html">
  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

The embedded default is a good starting point. Copy it from CoreDesign.Identity.Server/Templates/login.html in the library source. The following {{placeholder}} tokens are substituted at render time:

Placeholder Value
{{response_type}} OIDC response_type parameter (HTML-encoded)
{{client_id}} OIDC client_id parameter (HTML-encoded)
{{redirect_uri}} OIDC redirect_uri parameter (HTML-encoded)
{{scope}} OIDC scope parameter (HTML-encoded)
{{state}} OIDC state parameter (HTML-encoded)
{{nonce}} OIDC nonce parameter (HTML-encoded)
{{code_challenge}} PKCE code_challenge (HTML-encoded)
{{code_challenge_method}} PKCE code_challenge_method, e.g. S256 (HTML-encoded)
{{error_alert}} The fully rendered error banner HTML (see below); empty string when there is no error

All eight OIDC parameters must appear as hidden <input> fields in the form. The form must POST to /connect/authorize.

Overriding the error banner

The error banner shown after a failed login attempt is rendered from its own template, login-error.html. This keeps the error markup separate from the login form so each can be customized independently.

When a login attempt fails, the library renders login-error.html with the error message substituted, then inserts the resulting HTML into {{error_alert}} in login.html. When login succeeds or the form is shown for the first time, {{error_alert}} is replaced with an empty string and nothing is emitted.

login-error.html supports one placeholder:

Placeholder Value
{{error_message}} The HTML-encoded error text, e.g. Invalid username or password

To override it:

  1. Create identity-templates/login-error.html in your host project.
  2. Set it to copy to the output directory:
<None Update="identity-templates\login-error.html">
  <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
</None>

The default markup (from CoreDesign.Identity.Server/Templates/login-error.html) is:

<div class="id-error" role="alert">
  <svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
       aria-hidden="true">
    <path fill-rule="evenodd"
          d="M18 10a8 8 0 1 1-16 0 8 8 0 0 1 16 0zm-8-5a.75.75 0 0 1 .75.75v4.5a.75.75 0 0 1-1.5 0v-4.5A.75.75 0 0 1 10 5zm0 10a1 1 0 1 0 0-2 1 1 0 0 0 0 2z"
          clip-rule="evenodd"/>
  </svg>
  <span>{{error_message}}</span>
</div>

You can replace this with any markup you like. The .id-error CSS class is defined in login.html's stylesheet, so if you rename the class in your custom error template you will also need to add the corresponding style to your custom login.html.

Overriding the landing page

Create identity-templates/landing.html using the same copy and CopyToOutputDirectory steps. The landing page has no dynamic placeholders; it is rendered as a static file.

Theming without replacing the template

Both default templates define all colors as CSS custom properties inside :root. You can restyle them by injecting a small <style> block that overrides the --id-* variables, or by replacing the entire stylesheet section in your copy of the file. The variable names are documented in comments inside each template.

Advanced registration

AddIdentityServer accepts an optional Action<IdentityOptions> to override individual values after configuration binding:

builder.Services.AddIdentityServer(builder.Configuration, configure: opts =>
{
    opts.TokenLifetimeHours = 1;
});

The sectionName parameter controls which configuration section is bound:

builder.Services.AddIdentityServer(builder.Configuration, sectionName: "MyApp:Auth");

Feedback

Feedback on this package is welcome. If you run into a missing feature, an unexpected behavior, or something that required more effort than it should have, open an issue at github.com/codyskidmore/CoreDesign/issues or tag @codyskidmore. Suggestions about missing features and priority input are especially appreciated.

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
1.0.7 89 5/17/2026
1.0.5 99 5/3/2026

Added support for frontends authenticating in addiction to using the get-token end point for directly testing APIs.