How to use IdentityServer in ASP.NET Core with certificate signing

Programming, error messages and sample code > ASP.NET
The ASP.NET Core templates offer authentication in Single Page Apps (SPAs) using the support for API authorization. ASP.NET Core Identity for authenticating and storing users is combined with Duende Identity Server for implementing OpenID Connect.
Duende Software might require you to pay a license fee for production use of Duende Identity Server.

Development

Create an app with API authorization support

Angular:
dotnet new angular -au Individual
React:
dotnet new react -au Individual
Blazor WebAssembly:
dotnet new blazorwasm -ho -au Individual
 
Program.cs
 
using Microsoft.AspNetCore.Authentication;
using Microsoft.AspNetCore.ResponseCompression;
using Microsoft.EntityFrameworkCore;

var builder = WebApplication.CreateBuilder(args);

// register database context
var connectionString = builder.Configuration.GetConnectionString("DefaultConnection") ?? throw new InvalidOperationException("Connection string 'DefaultConnection' not found.");
builder.Services.AddDbContext<ApplicationDbContext>(options =>
    options.UseSqlite(connectionString));
builder.Services.AddDatabaseDeveloperPageExceptionFilter();

// Identity with the default UI:
builder.Services.AddDefaultIdentity<ApplicationUser>(options => options.SignIn.RequireConfirmedAccount = true)
    .AddEntityFrameworkStores<ApplicationDbContext>();

// IdentityServer with an additional AddApiAuthorization helper method that sets up some default ASP.NET Core conventions on top of IdentityServer:
builder.Services.AddIdentityServer()
    .AddApiAuthorization<ApplicationUser, ApplicationDbContext>();

// Authentication with an additional AddIdentityServerJwt helper method that configures the app to validate JWT tokens produced by IdentityServer:
builder.Services.AddAuthentication()
    .AddIdentityServerJwt();

builder.Services.AddControllersWithViews();
builder.Services.AddRazorPages();

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseMigrationsEndPoint();
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

// this is for Blazor WebAssembly
app.UseBlazorFrameworkFiles();
app.UseStaticFiles();

app.UseRouting();

// The authentication middleware that is responsible for validating the request credentials and setting the user on the request context:
app.UseAuthentication();

// The IdentityServer middleware that exposes the OpenID Connect endpoints:
app.UseIdentityServer();
app.UseAuthorization();


app.MapRazorPages();
app.MapControllers();
app.MapFallbackToFile("index.html");

app.Run();
appsettings.json
In the appsettings.json file of the project root, there's a new IdentityServer section that describes the list of configured clients. In the following example, there's a single client.
 
"IdentityServer": {
    "Clients": {
        "{CLIENT PROJECT NAME}": {
            "Profile": "IdentityServerSPA"
        }
    }
}
appsettings.Development.json
In the appsettings.Development.json file of the project root, there's an IdentityServer section that describes the key used to sign tokens. When deploying to production, a key needs to be provisioned and deployed alongside the app, as explained in the Production section.
 
"IdentityServer": {
    "Key": {
        "Type": "Development"
    }
}

Production

To deploy the app to production, the following resources need to be provisioned:
  • A database to store the Identity user accounts and the IdentityServer grants.
  • A production certificate to use for signing tokens.
  • There are no specific requirements for this certificate; it can be a self-signed certificate or a certificate provisioned through a CA authority.
  • It can be generated through standard tools like PowerShell or OpenSSL.
  • It can be deployed as a .pfx file with a strong password.
 
In the app's appsettings.json file, modify the IdentityServer section to include the key details:
 
"IdentityServer": {
    "Clients": {
        "{CLIENT PROJECT NAME}": {
            "Profile": "IdentityServerSPA"
        }
    },
    "Key": {
        "Type": "File",
        "FilePath": "H:\\root\\home\\xxx-001\\www\\site1\\certificate.pfx",
        "Password": "123456"
    }
}

Verify if the IdentiyServer is configured properly

navigate to https://your.domain.com/.well-known/openid-configuration in your browser, you should see the discovery document.

{
    "issuer": "https://your.domain.com",
    "jwks_uri": "https://your.domain.com/.well-known/openid-configuration/jwks",
    "authorization_endpoint": "https://your.domain.com/connect/authorize",
    "token_endpoint": "https://your.domain.com/connect/token",
    "userinfo_endpoint": "https://your.domain.com/connect/userinfo",
    "end_session_endpoint": "https://your.domain.com/connect/endsession",
    "check_session_iframe": "https://your.domain.com/connect/checksession",
    "revocation_endpoint": "https://your.domain.com/connect/revocation",
    "introspection_endpoint": "https://your.domain.com/connect/introspect",
    "device_authorization_endpoint": "https://your.domain.com/connect/deviceauthorization",
    "backchannel_authentication_endpoint": "https://your.domain.com/connect/ciba",
    "frontchannel_logout_supported": true,
    "frontchannel_logout_session_supported": true,
    "backchannel_logout_supported": true,
    "backchannel_logout_session_supported": true,
    "scopes_supported": [
        "openid",
        "profile",
        "aAPI",
        "offline_access"
    ],
    "claims_supported": [
        "sub",
        "name",
        "family_name",
        "given_name",
        "middle_name",
        "nickname",
        "preferred_username",
        "profile",
        "picture",
        "website",
        "gender",
        "birthdate",
        "zoneinfo",
        "locale",
        "updated_at"
    ],
    "grant_types_supported": [
        "authorization_code",
        "client_credentials",
        "refresh_token",
        "implicit",
        "password",
        "urn:ietf:params:oauth:grant-type:device_code",
        "urn:openid:params:grant-type:ciba"
    ],
    "response_types_supported": [
        "code",
        "token",
        "id_token",
        "id_token token",
        "code id_token",
        "code token",
        "code id_token token"
    ],
    "response_modes_supported": [
        "form_post",
        "query",
        "fragment"
    ],
    "token_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post"
    ],
    "id_token_signing_alg_values_supported": [
        "RS256"
    ],
    "subject_types_supported": [
        "public"
    ],
    "code_challenge_methods_supported": [
        "plain",
        "S256"
    ],
    "request_parameter_supported": true,
    "request_object_signing_alg_values_supported": [
        "RS256",
        "RS384",
        "RS512",
        "PS256",
        "PS384",
        "PS512",
        "ES256",
        "ES384",
        "ES512",
        "HS256",
        "HS384",
        "HS512"
    ],
    "authorization_response_iss_parameter_supported": true,
    "backchannel_token_delivery_modes_supported": [
        "poll"
    ],
    "backchannel_user_code_parameter_supported": true
}

Troubleshoot

Error: NullReferenceException: Object reference not set to an instance of an object.
Microsoft.AspNetCore.ApiAuthorization.IdentityServer.IdentityServerJwtBearerOptionsConfiguration.ResolveAuthorityAndKeysAsync(MessageReceivedContext messageReceivedContext)
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerHandler.HandleAuthenticateAsync()
Microsoft.AspNetCore.Authentication.AuthenticationHandler<TOptions>.AuthenticateAsync()
Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, string scheme)
Microsoft.AspNetCore.Authentication.AuthenticationHandler<TOptions>.AuthenticateAsync()
Microsoft.AspNetCore.Authentication.AuthenticationService.AuthenticateAsync(HttpContext context, string scheme)
Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
Description: missing definition of the Type of IdentityServer in appsettings.json
 
Error: Unhandled exception. System.InvalidOperationException: Invalid certificate store location ''.
Description: the Key:Type of IdentityServer is not File
 
Error: Unhandled exception. System.ArgumentNullException: Value cannot be null. (Parameter 'path2')
Description: missing certificate file
 
Error: Unhandled exception. System.InvalidOperationException: There was an error loading the certificate. No password was provided.
Description: no certificate password was provided
 
Error: Unhandled exception. System.InvalidOperationException: There was an error loading the certificate. Either the password is incorrect or the process does not have permisions to store the key in the Keyset 'DefaultKeySet'
 ---> System.Security.Cryptography.CryptographicException: The specified network password is not correct.
Description: incorrect certificate password
 
Error: Unhandled exception. System.InvalidOperationException: There was an error loading the certificate. Either the password is incorrect or the process does not have permisions to store the key in the Keyset 'DefaultKeySet'
 ---> System.Security.Cryptography.CryptographicException: The system cannot find the file specified.
Description: require "Load user profile"
 
Error: Unhandled exception. System.InvalidOperationException: There was an error loading the certificate. Either the password is incorrect or the process does not have permisions to store the key in the Keyset 'DefaultKeySet'
 ---> System.Security.Cryptography.CryptographicException: The profile for the user is a temporary profile.
Description: require ApplicationPoolIdentity
 
 
Reference: