This is the full developer documentation for Duende Docs ----- # Duende Software
Docs > Get started building your .NET applications with IdentityServer, User Management, Backend-for-Frontend (BFF) and our open-source tools. Install templates Install the [Duende templates](/identityserver/overview/packaging/#templates) to get started quickly. Terminal ```bash dotnet new install Duende.Templates dotnet new duende-is -o DuendeIdentityServer dotnet run --project DuendeIdentityServer ``` Demo Server Experience IdentityServer in action with our live demo server. Test OAuth 2.0, OpenID Connect, and SAML flows, explore client configurations, and see how security tokens work in practice. [Try it out](https://demo.duendesoftware.com) IdentityServer The most flexible and standards-compliant OpenID Connect, OAuth 2.0, and SAML framework for ASP.NET Core. [Learn more](/identityserver/) User Management Native user storage, passwordless-first authentication, and full lifecycle management (profiles, roles, groups, multi-tenancy). [Learn more](/identityserver/identity/user-management/) Backend-for-Frontend (BFF) Securing SPAs and Blazor WASM Applications once and for all, without storing tokens in the browser. [Learn more](/bff/) Access Token Management OSS .NET identity library for access token management. [Learn more](/accesstokenmanagement/) Identity Model OSS .NET identity library for OAuth 2.0 related protocol operations. [Learn more](/identitymodel/) OIDC Client OSS .NET identity libraries for OpenID Connect related protocol operations. [Learn more](/identitymodel-oidcclient/) Introspection for ASP.NET Core OSS ASP.NET Core authentication handler for OAuth 2.0 token introspection. [Learn more](/introspection/) *** Subscribe To Our Newsletter! Stay ahead in the world of identity and access management! * Latest security best practices * Product updates and releases * Technical tips and implementation guides * Industry news and trends Join our community of developers and security professionals building secure applications. ----- # 404 Not Found > Page not found. Check the URL, try using the search bar, or visit the Duende Developer Community. ----- # Access Token Management > The Duende.AccessTokenManagement library provides automatic access token management features for .NET applications The `Duende.AccessTokenManagement` library provides automatic access token management features for .NET worker and ASP.NET Core web applications: * Automatic acquisition and lifetime management of client credentials based access tokens for machine-to-machine communication (using the `Duende.AccessTokenManagement` package) * Automatic access token lifetime management using a refresh token for API calls on behalf of the currently logged-in user (using the `Duende.AccessTokenManagement.OpenIdConnect` package) * Revocation of access tokens ## Machine-To-Machine Token Management [Section titled “Machine-To-Machine Token Management”](#machine-to-machine-token-management) To get started, install the NuGet Package: ```bash dotnet add package Duende.AccessTokenManagement ``` See [Service Workers and Background Tasks](/accesstokenmanagement/workers/) for more information on how to get started. [GitHub Repository](https://github.com/DuendeSoftware/foss/tree/main/access-token-management)View the source code for this library on GitHub. [NuGet Package](https://www.nuget.org/packages/Duende.AccessTokenManagement/)View the package on NuGet.org. ## User Token Management [Section titled “User Token Management”](#user-token-management) To get started, install the NuGet Package: ```bash dotnet add package Duende.AccessTokenManagement.OpenIdConnect ``` See [Web Applications](/accesstokenmanagement/web-apps/) for more information on how to get started. [GitHub Repository](https://github.com/DuendeSoftware/foss/tree/main/access-token-management)View the source code for this library on GitHub. [NuGet Package](https://www.nuget.org/packages/Duende.AccessTokenManagement.OpenIdConnect/)View the package on NuGet.org. ## License And Feedback [Section titled “License And Feedback”](#license-and-feedback) **Duende.AccessTokenManagement** is released as open source under the Apache 2.0 license. Bug reports, feature requests and contributions are welcome via the [Duende community discussions](https://duende.link/community). ----- # Client Assertions > Learn how to use client assertions instead of shared secrets for token client authentication in Duende.AccessTokenManagement. If your token client is using a client assertion instead of a shared secret, you can provide the assertion in two ways: * Use the request parameter mechanism to pass a client assertion to the management * Implement the `IClientAssertionService` interface to centralize client assertion creation Here’s a sample client assertion service using the Microsoft JWT library: * V4 ClientAssertionService.cs ```csharp using Duende.AccessTokenManagement; using Duende.IdentityModel; using Duende.IdentityModel.Client; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; public class ClientAssertionService(IOptionsSnapshot options) : IClientAssertionService { public Task GetClientAssertionAsync( ClientCredentialsClientName? clientName = null, TokenRequestParameters? parameters = null, CancellationToken ct = default) { if (clientName == "invoice") { var options1 = options.Get(clientName); var descriptor = new SecurityTokenDescriptor { Issuer = options1.ClientId!.ToString(), // Set the audience to the url of identity server. Do not use the tokenurl to build the autority. Audience = "https://--url-to-authority-here--", Expires = DateTime.UtcNow.AddMinutes(1), SigningCredentials = GetSigningCredential(), Claims = new Dictionary { { JwtClaimTypes.JwtId, Guid.NewGuid().ToString() }, { JwtClaimTypes.Subject, options1.ClientId.ToString()! }, { JwtClaimTypes.IssuedAt, DateTimeOffset.UtcNow.ToUnixTimeSeconds() } }, AdditionalHeaderClaims = new Dictionary { { JwtClaimTypes.TokenType, "client-authentication+jwt" } } }; var handler = new JsonWebTokenHandler(); var jwt = handler.CreateToken(descriptor); return Task.FromResult(new ClientAssertion { Type = OidcConstants.ClientAssertionTypes.JwtBearer, Value = jwt }); } return Task.FromResult(null); } private SigningCredentials GetSigningCredential() { throw new NotImplementedException(); } } ``` * V3 ClientAssertionService.cs ```csharp using Duende.AccessTokenManagement; using Duende.IdentityModel; using Duende.IdentityModel.Client; using Microsoft.Extensions.Options; using Microsoft.IdentityModel.JsonWebTokens; using Microsoft.IdentityModel.Tokens; public class ClientAssertionService(IOptionsSnapshot options) : IClientAssertionService { public Task GetClientAssertionAsync( string? clientName = null, TokenRequestParameters? parameters = null) { if (clientName == "invoice") { var options1 = options.Get(clientName); var descriptor = new SecurityTokenDescriptor { Issuer = options1.ClientId, // Set the audience to the url of identity server. Do not use the tokenurl to build the autority. Audience = "https://--url-to-authority-here--", Expires = DateTime.UtcNow.AddMinutes(1), SigningCredentials = GetSigningCredential(), Claims = new Dictionary { { JwtClaimTypes.JwtId, Guid.NewGuid().ToString() }, { JwtClaimTypes.Subject, options1.ClientId! }, { JwtClaimTypes.IssuedAt, DateTime.UtcNow.ToEpochTime() } }, AdditionalHeaderClaims = new Dictionary { { JwtClaimTypes.TokenType, "client-authentication+jwt" } } }; var handler = new JsonWebTokenHandler(); var jwt = handler.CreateToken(descriptor); return Task.FromResult(new ClientAssertion { Type = OidcConstants.ClientAssertionTypes.JwtBearer, Value = jwt }); } return Task.FromResult(null); } private SigningCredentials GetSigningCredential() { throw new NotImplementedException(); } } ``` Note You need to explicitly set the `Audience` to the authorization server’s issuer URL (usually the URL of identity server). Don’t set the audience to the `TokenUrl`. Setting the `Audience` value to the token endpoint leaves you vulnerable to these vulnerabilities: (CVE-2025-27370/CVE-2025-27371). For a complete working example, see the [WebClientAssertions sample](https://github.com/DuendeSoftware/foss/tree/main/access-token-management/samples/WebClientAssertions). ----- # Customizing Client Credentials Token Management > Learn how to customize client credentials token management including client options, backchannel communication, and token caching configurations. The most common way to use access token management is for [machine-to-machine communication](/accesstokenmanagement/workers/). However, you may want to customize certain aspects of it. ## Client Options [Section titled “Client Options”](#client-options) You can add token client definitions to your host while configuring the DotNet service provider, e.g.: * V4 Program.cs ```csharp services.AddClientCredentialsTokenManagement() .AddClient("invoices", client => { client.TokenEndpoint = new Uri("https://sts.company.com/connect/token"); client.ClientId = ClientId.Parse("4a632e2e-0466-4e5a-a094-0455c6105f57"); client.ClientSecret = ClientSecret.Parse("e8ae294a-d5f3-4907-88fa-c83b3546b70c"); client.ClientCredentialStyle = ClientCredentialStyle.AuthorizationHeader; client.Scope = Scope.Parse("list"); client.Resource = Resource.Parse("urn:invoices"); }); ``` * V3 Program.cs ```csharp services.AddClientCredentialsTokenManagement() .AddClient("invoices", client => { client.TokenEndpoint = "https://sts.company.com/connect/token"; client.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57"; client.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c"; client.ClientCredentialStyle = ClientCredentialStyle.AuthorizationHeader; client.Scope = "list"; client.Resource = "urn:invoices"; }); ``` You can set the following options: * `TokenEndpoint` - URL of the OAuth token endpoint where this token client requests tokens from * `ClientId` - client ID * `ClientSecret` - client secret (if a shared secret is used) * `ClientCredentialStyle` - Specifies how the client ID / secret is sent to the token endpoint. Options are using the authorization header, or POST body values (defaults to header) * `Scope` - the requested scope of access (if any) * `Resource` - the resource indicator (if any) Internally the standard .NET options system is used to register the configuration. This means you can also register clients like this: * V4 Program.cs ```csharp services.Configure("invoices", client => { client.TokenEndpoint = new Uri("https://sts.company.com/connect/token"); client.ClientId = ClientId.Parse("4a632e2e-0466-4e5a-a094-0455c6105f57"); client.ClientSecret = ClientSecret.Parse("e8ae294a-d5f3-4907-88fa-c83b3546b70c"); client.Scope = Scope.Parse("list"); client.Resource = Resource.Parse("urn:invoices"); }); ``` * V3 Program.cs ```csharp services.Configure("invoices", client => { client.TokenEndpoint = "https://sts.company.com/connect/token"; client.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57"; client.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c"; client.Scope = "list"; client.Resource = "urn:invoices"; }); ``` Or use the `IConfigureNamedOptions` if you need access to the ASP.NET Core service provider during registration, e.g.: * V4 ClientCredentialsClientConfigureOptions.cs ```csharp using Duende.AccessTokenManagement; using Duende.IdentityModel.Client; using Microsoft.Extensions.Options; public class ClientCredentialsClientConfigureOptions(DiscoveryCache cache) : IConfigureNamedOptions { public void Configure(string? name, ClientCredentialsClient options) { if (name == "invoices") { var disco = cache.GetAsync().GetAwaiter().GetResult(); options.TokenEndpoint = new Uri(disco.TokenEndpoint); options.ClientId = ClientId.Parse("4a632e2e-0466-4e5a-a094-0455c6105f57"); options.ClientSecret = ClientSecret.Parse("e8ae294a-d5f3-4907-88fa-c83b3546b70c"); options.Scope = Scope.Parse("list"); options.Resource = Resource.Parse("urn:invoices"); } } public void Configure(ClientCredentialsClient options) { // implement default configure Configure("", options); } } ``` * V3 ClientCredentialsClientConfigureOptions.cs ```csharp using Duende.AccessTokenManagement; using Duende.IdentityModel.Client; using Microsoft.Extensions.Options; public class ClientCredentialsClientConfigureOptions(DiscoveryCache cache) : IConfigureNamedOptions { public void Configure(string? name, ClientCredentialsClient options) { if (name == "invoices") { var disco = cache.GetAsync().GetAwaiter().GetResult(); options.TokenEndpoint = disco.TokenEndpoint; options.ClientId = "4a632e2e-0466-4e5a-a094-0455c6105f57"; options.ClientSecret = "e8ae294a-d5f3-4907-88fa-c83b3546b70c"; options.Scope = "list"; options.Resource = "urn:invoices"; } } public void Configure(ClientCredentialsClient options) { // implement default configure Configure("", options); } } ``` You will also need to register the config options, for example: Program.cs ```csharp services.AddClientCredentialsTokenManagement(); services.AddSingleton(new DiscoveryCache("https://sts.company.com")); services.AddSingleton, ClientCredentialsClientConfigureOptions>(); ``` ## Backchannel Communication [Section titled “Backchannel Communication”](#backchannel-communication) By default, all backchannel communication will be done using a named client from the HTTP client factory. The name is `Duende.AccessTokenManagement.BackChannelHttpClient` which is also a constant called `ClientCredentialsTokenManagementDefaults.BackChannelHttpClientName`. You can register your own HTTP client with the factory using the above name and thus provide your own custom HTTP client. The client registration object has two additional properties to customize the HTTP client: * `HttpClientName` - if set, this HTTP client name from the factory will be used instead of the default one * `HttpClient` - allows setting an instance of `HttpClient` to use. Will take precedence over a client name ## Token caching [Section titled “Token caching”](#token-caching) * V4 In V4, access tokens are cached using [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/overview?view=aspnetcore-9.0#hybridcache). ### Using remote caches [Section titled “Using remote caches”](#using-remote-caches) Hybrid cache is a 2 tier cache, with in-memory and remote capabilities. Hybrid cache automatically picks up any IDistributedCache implementation as it’s remote cache. See [Distributed Caching in Asp.Net](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-9.0) on more information on topic. ### Injecting a custom cache [Section titled “Injecting a custom cache”](#injecting-a-custom-cache) By default, we use the default HybridCache implementation. You may want to inject a custom hybrid cache implementation, such as [FusionCache](https://github.com/ZiggyCreatures/FusionCache). You can do this either for the entire system: Program.cs ```csharp services.AddSingleton(new MyCustomCacheImplementation()); ``` Or only for `Duende.AccessTokenManagement`, by using Service Keys: Program.cs ```csharp services.AddKeyedSingleton(ServiceProviderKeys.ClientCredentialsTokenCache, new MyCustomCacheImplementation()); ``` ### Customizing cache keys [Section titled “Customizing cache keys”](#customizing-cache-keys) By default, cache keys are built up as follows: `{options.CacheKeyPrefix}::{client_name}::hashed({scope})::hashed({resource})` * `options.CacheKeyPrefix` can be configured using the `ClientCredentialsTokenManagementOptions` * `client_name` is the name of the client * `scope` is the scope parameter (if any) that’s used to request the access token. * `resource` is the resource parameter (if any) that’s used to request the access token. The values of both the `scope` and `resource` hashed (MD5) to ensure that the cache key length is not unbounded. You can implement your own cache key generator by implementing a custom `IClientCredentialsCacheKeyGenerator` and registering this to your service container. This is needed if you’re adding custom parameters to your `TokenRequestParameters` ### Encrypting cache entries [Section titled “Encrypting cache entries”](#encrypting-cache-entries) You may want to share a remote cache with other parts of the application or even with other applications. In that case, it may be wise to encrypt the access tokens in the remote cache. You can achieve this with a custom serializer. Program.cs ```csharp // Explicitly register a serializer for the client credentials tokens with the hybrid cache services.AddHybridCache() .AddSerializer(); // This example uses data protection api. You'll want to configure this to suit your needs services.AddDataProtection(); /// /// Example on how to implement a serializer that encrypts data using ASP.NET Core Data Protection. /// public class EncryptedHybridCacheSerializer : IHybridCacheSerializer { private readonly IDataProtector _protector; public EncryptedHybridCacheSerializer(IDataProtectionProvider provider) { _protector = provider.CreateProtector("ClientCredentialsToken"); } public ClientCredentialsToken Deserialize(ReadOnlySequence source) { // Convert the sequence to a byte array var buffer = source.ToArray(); // Unprotect (decrypt) the data var unprotected = _protector.Unprotect(buffer); // Deserialize the JSON payload return JsonSerializer.Deserialize(unprotected)!; } public void Serialize(ClientCredentialsToken value, IBufferWriter target) { // Serialize the value to JSON var json = JsonSerializer.SerializeToUtf8Bytes(value); // Protect (encrypt) the data var protectedBytes = _protector.Protect(json); // Write to the buffer target.Write(protectedBytes); } } ``` * V3 By default, tokens will be cached using the `IDistributedCache` abstraction in ASP.NET Core. You can either use the in-memory cache version, or a real distributed cache like Redis. For development purposes, you can use the `MemoryDistributedCache`: Program.cs ```csharp services.AddDistributedMemoryCache(); ``` Note that `MemoryDistributedCache` will be cleared whenever the process is restarted. It won’t be shared between multiple instances of your application in a load-balanced environment. As a result, a new token will have to be obtained when you restart your application, and each instance will obtain a different token. For production deployments, we recommend using a [distributed cache](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed#establish-distributed-caching-services). The built-in cache in `Duende.AccessTokenManagment` uses two settings from the options, which apply with any `IDistributedCache`: Program.cs ```csharp services.AddClientCredentialsTokenManagement(options => { options.CacheLifetimeBuffer = 60; options.CacheKeyPrefix = "Duende.AccessTokenManagement.Cache::"; }); ``` `CacheLifetimeBuffer` is a value in seconds that will be subtracted from the token lifetime, e.g. if a token is valid for one hour, it will be cached for 59 minutes only. The cache key prefix is used to construct the unique key for the cache item based on client name, requested scopes and resource. ### Additional V4 Caching Options [Section titled “Additional V4 Caching Options”](#additional-v4-caching-options) In V4, the `ClientCredentialsTokenManagementOptions` class has additional properties for fine-tuning cache behavior: * `DefaultCacheLifetime` - The default cache lifetime for the first token request when the actual expiration is not yet known. Defaults to 5 minutes. * `UseCacheAutoTuning` - When enabled, the cache uses the actual token expiration time to set the cache entry lifetime. The first request uses `DefaultCacheLifetime` since the expiration isn’t known yet. Defaults to `true`. * `LocalCacheExpiration` - How long local (in-memory) cache entries are valid before being refreshed from the remote cache. Defaults to 1 minute. Set to `null` to use the same expiration as the remote cache. * `NonceStoreKeyPrefix` - Prefix used for storing DPoP nonces in the cache. Program.cs ```csharp services.AddClientCredentialsTokenManagement(options => { options.CacheLifetimeBuffer = 60; options.CacheKeyPrefix = "Duende.AccessTokenManagement.Cache::"; // V4-specific options options.DefaultCacheLifetime = TimeSpan.FromMinutes(5); options.UseCacheAutoTuning = true; options.LocalCacheExpiration = TimeSpan.FromMinutes(1); }); ``` Finally, you can also replace the caching implementation altogether by registering your own `IClientCredentialsTokenCache`. ----- # Demonstrating Proof-of-Possession (DPoP) > Demonstrating Proof-of-Possession is a security mechanism that binds access tokens to specific cryptographic keys to prevent token theft and misuse. [DPoP](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-dpop) specifies how to bind an asymmetric key stored within a JSON Web Key (JWK) to an access token. This will make the access token bound to the key such that if the access token were to leak, it cannot be used without also having access to the private key of the corresponding JWK. The Duende.AccessTokenManagement library supports DPoP. ## DPoP Key [Section titled “DPoP Key”](#dpop-key) The main piece that your hosting application needs to concern itself with is how to get (and manage) the DPoP key. This key (and signing algorithm) will be either an “RS”, “PS”, or “ES” style key, and needs to be in the form of a JSON Web Key (or JWK). Consult the specification for more details. The creation and management of this DPoP key is up to the policy of the client. For example is can be dynamically created when the client starts up, and can be periodically rotated. The main constraint is that it must be stored for as long as the client uses any access tokens (and possibly refresh tokens) that they are bound to, which this library will manage for you. Creating a JWK in .NET is simple: Program.cs ```csharp using System.Security.Cryptography; using System.Text.Json; using Microsoft.IdentityModel.Tokens; var rsaKey = new RsaSecurityKey(RSA.Create(2048)); var jwkKey = JsonWebKeyConverter.ConvertFromSecurityKey(rsaKey); jwkKey.Alg = "PS256"; var jwk = JsonSerializer.Serialize(jwkKey); Console.WriteLine(jwk); ``` ## Key Configuration [Section titled “Key Configuration”](#key-configuration) Once you have a JWK you wish to use, then it must be configured or made available to this library. That can be done in one of two ways: * Configure the key at startup by setting the `DPoPJsonWebKey` property on either the `ClientCredentialsTokenManagementOptions` or `UserTokenManagementOptions` (depending on which of the two styles you are using from this library). * Implement the `IDPoPKeyStore` interface to produce the key at runtime. Here’s a sample configuring the key in an application using `AddOpenIdConnectAccessTokenManagement` in the startup code: Program.cs ```csharp services.AddOpenIdConnectAccessTokenManagement(options => { options.DPoPJsonWebKey = jwk; }); ``` Similarly, for an application using `AddClientCredentialsTokenManagement`, it would look like this: Program.cs ```csharp services.AddClientCredentialsTokenManagement() .AddClient("client_name", options => { options.DPoPJsonWebKey = jwk; }); ``` ## Proof Tokens At The Token Server’s Token Endpoint [Section titled “Proof Tokens At The Token Server’s Token Endpoint”](#proof-tokens-at-the-token-servers-token-endpoint) Once the key has been configured for the client, then the library will use it to produce a DPoP proof token when calling the token server (including token renewals if relevant). There is nothing explicit needed on behalf of the developer using this library. ### `dpop_jkt` At The Token Server’s Authorize Endpoint [Section titled “dpop\_jkt At The Token Server’s Authorize Endpoint”](#dpop_jkt-at-the-token-servers-authorize-endpoint) When using DPoP and `AddOpenIdConnectAccessTokenManagement`, this library will also automatically include the `dpop_jkt` parameter to the authorize endpoint. ## Proof Tokens At The API [Section titled “Proof Tokens At The API”](#proof-tokens-at-the-api) Once the library has gotten a DPoP bound access token for the client, then if your application is using any of the `HttpClient` client factory helpers (e.g. `AddClientCredentialsHttpClient` or `AddUserAccessTokenHttpClient`) then those outbound HTTP requests will automatically include a DPoP proof token for the associated DPoP access token. ## Considerations [Section titled “Considerations”](#considerations) A point to keep in mind when using DPoP and `AddOpenIdConnectAccessTokenManagement` is that the DPoP proof key is created per user session. This proof key must be store somewhere, and the `AuthenticationProperties` used by both the OIDC and cookie handlers is what is used to store this key. This implies that the OIDC `state` parameter will increase in size, as well the resultant cookie that represents the user’s session. The storage for each of these can be customized with the properties on the options `StateDataFormat` and `SessionStore` respectively. ----- # Extensibility > Learn how to extend and customize Duende.AccessTokenManagement, including custom token retrieval. There are several extension points where you can customize the behavior of Duende.AccessTokenManagement. The extension model is designed to favor composition over inheritance, making it easier to customize and extend while maintaining the library’s core functionality. ## Token Retrieval [Section titled “Token Retrieval”](#token-retrieval) Token retrieval can be customized by implementing the `AccessTokenRequestHandler.ITokenRetriever` interface. This interface defines a single method, `GetTokenAsync`, which is called by the `AccessTokenRequestHandler` to retrieve an access token. A common scenario for this would be if you wanted to implement a different token retrieval flow, that’s currently not implemented, such as [Impersonation or Delegation grants (RFC 8693)](https://datatracker.ietf.org/doc/html/rfc8693). Implementing this particular flow is outside the scope of this document. The following snippet demonstrates how to implement fictive scenario where a custom token retriever dynamically determines which credential flow to use. CustomTokenRetriever.cs ```csharp public class CustomTokenRetriever( UserTokenRequestParameters parameters, IClientCredentialsTokenManager clientCredentialsTokenManager, IUserTokenManager userTokenManagement, IUserAccessor userAccessor, ClientCredentialsClientName clientName) : AccessTokenRequestHandler.ITokenRetriever { public async Task> GetTokenAsync( HttpRequestMessage request, CancellationToken ct) { // You'll have to make a decision on what token parameters to use, // and you can override the default parameters. var param = parameters with { Scope = Scope.Parse("some scope"), ForceTokenRenewal = request.GetForceRenewal() // for retry policies. }; AccessTokenRequestHandler.IToken token; // Get the type from current context. // Using a random number as an example here. int tokenType = new Random().Next(1, 2); if (tokenType == 1) { var getTokenResult = await clientCredentialsTokenManager .GetAccessTokenAsync(clientName, param, ct); if (!getTokenResult.Succeeded) { return getTokenResult.FailedResult; } token = getTokenResult.Token; } else { var user = await userAccessor.GetCurrentUserAsync(ct); var getTokenResult = await userTokenManagement .GetAccessTokenAsync(user, param, ct); if (!getTokenResult.Succeeded) { return getTokenResult.FailedResult; } token = getTokenResult.Token; } return TokenResult.Success(token); } } ``` A custom token handler can be linked to your `HttpClient` by creating an `AccessTokenRequestHandler` and adding it to the request pipeline: Program.cs ```csharp services.AddHttpClient() .AddDefaultAccessTokenResiliency() .AddHttpMessageHandler(provider => { var yourCustomTokenRetriever = new CustomTokenRetriever(...); var logger = provider.GetRequiredService>(); var dPoPProofService = provider.GetRequiredService(); var dPoPNonceStore = provider.GetRequiredService(); return new AccessTokenRequestHandler( tokenRetriever: yourCustomTokenRetriever, dPoPNonceStore: dPoPNonceStore, dPoPProofService: dPoPProofService, logger: logger); }); ``` ## Token Request Customization [Section titled “Token Request Customization”](#token-request-customization) Token request parameters can be customized by implementing the `ITokenRequestCustomizer` interface. This interface allows you to dynamically modify token request parameters based on the incoming HTTP request context, making it ideal for multi-tenant applications where token parameters need to vary per tenant. The customizer is invoked before token retrieval and works with both user and client credentials flows. Unlike implementing a custom token retriever, which replaces the entire token acquisition logic, the customizer focuses on modifying parameters such as scopes, resources or other parts of the `TokenRequestParameters`. ### Multi-Tenant Scenario [Section titled “Multi-Tenant Scenario”](#multi-tenant-scenario) In multi-tenant applications, different tenants often require different parameters. For example, each tenant might have: * A unique API resource or audience identifier * Tenant-specific scopes The `ITokenRequestCustomizer` provides a clean way to handle these variations without needing separate `HttpClient` configurations for each tenant. The following example demonstrates a multi-tenant scenario where the customizer extracts the tenant identifier from the HTTP request context and applies tenant-specific token parameters: MultiTenantTokenRequestCustomizer.cs ```csharp public class MultiTenantTokenRequestCustomizer( ITenantResolver tenantResolver, ITenantConfigurationStore tenantConfigStore) : ITokenRequestCustomizer { public async Task Customize( HttpRequestContext httpRequestContext, TokenRequestParameters baseParameters, CancellationToken cancellationToken = default) { // Extract tenant identifier from the request context // HttpRequestContext provides access to the HttpRequestMessage // This could come from a header, subdomain, or route parameter var tenantId = await tenantResolver.GetTenantIdAsync( httpRequestContext.HttpRequestMessage, cancellationToken); // Get tenant-specific configuration var tenantConfig = await tenantConfigStore.GetConfigurationAsync(tenantId, cancellationToken); // Customize parameters with tenant-specific values return baseParameters with { Resource = Resource.Parse(tenantConfig.ApiResource), Scope = Scope.Parse(tenantConfig.RequiredScopes), // Add any additional customizations }; } } ``` An instance of the `ITokenRequestCustomizer` implementation can be registered as part of the call to the `Add*Handler` methods: Program.cs ```csharp var customizer = new MultiTenantTokenRequestCustomizer(tenantResolver, tenantConfigStore); // Client Credentials Token Handler services.AddHttpClient("client-credentials-token-http-client") .AddClientCredentialsTokenHandler(customizer, ClientCredentialsClientName.Parse("pure-client-credentials")); // User Access Token Handler services.AddHttpClient("user-access-token-http-client") .AddUserAccessTokenHandler(customizer); // Client Access Token Handler services.AddHttpClient("client-access-token-http-client") .AddClientAccessTokenHandler(customizer); ``` If you require access to services from the service provider, you can use the `Add*Handler` method overloads that accept a factory delegate: Program.cs ```csharp builder.Services.AddScoped(); // Client Credentials Token Handler services.AddHttpClient("client-credentials-token-http-client") .AddClientCredentialsTokenHandler( serviceProvider => serviceProvider.GetRequiredService(), ClientCredentialsClientName.Parse("pure-client-credentials")); // User Access Token Handler services.AddHttpClient("user-access-token-http-client") .AddUserAccessTokenHandler( serviceProvider => serviceProvider.GetRequiredService()); // Client Access Token Handler services.AddHttpClient("client-access-token-http-client") .AddClientAccessTokenHandler( serviceProvider => serviceProvider.GetRequiredService()); ``` When to use ITokenRequestCustomizer vs ITokenRetriever * Use `ITokenRequestCustomizer` when you need to modify token request parameters (scopes, resources, audiences) based on request context * Use `ITokenRetriever` when you need to replace the entire token acquisition logic with a custom flow ### Additional Use Cases [Section titled “Additional Use Cases”](#additional-use-cases) Beyond multi-tenancy, `ITokenRequestCustomizer` can be used for: * Dynamically setting scopes based on the target API endpoint * Adding audience or resource parameters based on request headers or route data * Implementing per-request token parameter logic without changing the core retrieval flow ## Principal Transformation After Token Refresh [Section titled “Principal Transformation After Token Refresh”](#principal-transformation-after-token-refresh) After a token refresh, you can update the user’s claims before the authentication session is re-issued. The `TransformPrincipalAfterRefreshAsync` delegate transforms the `ClaimsPrincipal` after a successful token refresh. ```csharp public delegate Task TransformPrincipalAfterRefreshAsync( ClaimsPrincipal principal, CancellationToken ct); ``` ### Use Cases [Section titled “Use Cases”](#use-cases) * **Refreshing claims from the identity provider**: Fetch updated claims from the userinfo endpoint after token refresh * **Updating role or permission claims**: Update roles or permissions that changed between token refreshes * **Adding computed claims**: Add claims based on external data sources that may have changed ### Example: Updating Claims After Refresh [Section titled “Example: Updating Claims After Refresh”](#example-updating-claims-after-refresh) Program.cs ```csharp builder.Services.AddOpenIdConnectAccessTokenManagement(options => { // Configure other options... }); // Register the principal transformation builder.Services.AddSingleton( serviceProvider => async (principal, ct) => { // Create a new identity with the existing claims var identity = (ClaimsIdentity)principal.Identity!; // Example: Fetch updated roles from a service var roleService = serviceProvider.GetRequiredService(); var currentRoles = await roleService.GetRolesForUserAsync( principal.FindFirstValue("sub"), ct); // Remove old role claims and add new ones var existingRoleClaims = identity.FindAll("role").ToList(); foreach (var claim in existingRoleClaims) { identity.RemoveClaim(claim); } foreach (var role in currentRoles) { identity.AddClaim(new Claim("role", role)); } return principal; }); ``` When to use TransformPrincipalAfterRefreshAsync Use this delegate when claims need to be synchronized with external systems during token refresh. For static claim transformations that happen at login time, use `IClaimsTransformation` instead. ## User Accessor [Section titled “User Accessor”](#user-accessor) The `IUserAccessor` interface provides access to the current user’s `ClaimsPrincipal`. Use this to access the current user outside of an HTTP request context, such as in background services or message handlers. ```csharp public interface IUserAccessor { /// /// Gets the current user's ClaimsPrincipal /// Task GetCurrentUserAsync(CancellationToken ct = default); } ``` ### Custom User Accessor Example [Section titled “Custom User Accessor Example”](#custom-user-accessor-example) The default implementation uses `IHttpContextAccessor`. For scenarios where you need to access the user from a different context (e.g., a background job with a captured user identity), implement a custom `IUserAccessor`: ```csharp public class BackgroundJobUserAccessor : IUserAccessor { private readonly AsyncLocal _currentUser = new(); public void SetUser(ClaimsPrincipal user) { _currentUser.Value = user; } public Task GetCurrentUserAsync(CancellationToken ct = default) { return Task.FromResult(_currentUser.Value ?? throw new InvalidOperationException("No user context available")); } } ``` Register your custom accessor: Program.cs ```csharp builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => sp.GetRequiredService()); ``` ## Token Refresh Concurrency Control [Section titled “Token Refresh Concurrency Control”](#token-refresh-concurrency-control) The `IUserTokenRequestConcurrencyControl` interface provides synchronization for token refresh operations. This prevents the “thundering herd” problem where multiple concurrent requests all attempt to refresh the same token simultaneously. ```csharp public interface IUserTokenRequestConcurrencyControl { /// /// Executes a token retrieval operation with concurrency control. /// If multiple requests attempt to refresh the same token concurrently, /// only one will execute the refresh and others will wait for the result. /// Task> ExecuteWithConcurrencyControlAsync( UserRefreshToken key, Func>> tokenRetriever, CancellationToken ct = default); } ``` ### Custom Concurrency Control [Section titled “Custom Concurrency Control”](#custom-concurrency-control) The default implementation uses in-memory locking, which works for single-server deployments. For distributed scenarios (multiple servers), implement a custom version using distributed locking: ```csharp public class DistributedTokenConcurrencyControl : IUserTokenRequestConcurrencyControl { private readonly IDistributedLockProvider _lockProvider; public DistributedTokenConcurrencyControl(IDistributedLockProvider lockProvider) { _lockProvider = lockProvider; } public async Task> ExecuteWithConcurrencyControlAsync( UserRefreshToken key, Func>> tokenRetriever, CancellationToken ct = default) { // Create a lock key based on the refresh token hash var lockKey = $"token-refresh:{key.RefreshToken.ToString().GetHashCode()}"; await using var handle = await _lockProvider.AcquireLockAsync( lockKey, timeout: TimeSpan.FromSeconds(30), ct); // Execute the token retrieval while holding the lock return await tokenRetriever(); } } ``` ## Token Endpoint Operations [Section titled “Token Endpoint Operations”](#token-endpoint-operations) The `IOpenIdConnectUserTokenEndpoint` interface provides low-level access to token endpoint operations. Use this for testing, mocking, or custom token refresh logic. ```csharp public interface IOpenIdConnectUserTokenEndpoint { /// /// Refreshes an access token using a refresh token /// Task> RefreshAccessTokenAsync( UserRefreshToken userToken, UserTokenRequestParameters parameters, CancellationToken ct = default); /// /// Revokes a refresh token at the identity provider /// Task RevokeRefreshTokenAsync( UserRefreshToken userToken, UserTokenRequestParameters parameters, CancellationToken ct = default); } ``` ### Testing with Mock Token Endpoint [Section titled “Testing with Mock Token Endpoint”](#testing-with-mock-token-endpoint) ```csharp public class MockUserTokenEndpoint : IOpenIdConnectUserTokenEndpoint { public Task> RefreshAccessTokenAsync( UserRefreshToken userToken, UserTokenRequestParameters parameters, CancellationToken ct = default) { // Return a test token for integration testing var token = new UserToken { AccessToken = AccessToken.Parse("test-access-token"), RefreshToken = RefreshToken.Parse("test-refresh-token"), Expiration = DateTimeOffset.UtcNow.AddHours(1) }; return Task.FromResult(TokenResult.Success(token)); } public Task RevokeRefreshTokenAsync( UserRefreshToken userToken, UserTokenRequestParameters parameters, CancellationToken ct = default) { // No-op for testing return Task.CompletedTask; } } ``` ## OpenID Connect Configuration Service [Section titled “OpenID Connect Configuration Service”](#openid-connect-configuration-service) The `IOpenIdConnectConfigurationService` interface extracts configuration from the registered OpenID Connect authentication handler. Use this to access OIDC configuration for custom token operations. ```csharp public interface IOpenIdConnectConfigurationService { /// /// Gets the OpenID Connect configuration for the specified scheme /// Task GetOpenIdConnectConfigurationAsync( Scheme? schemeName = default, CancellationToken ct = default); } ``` The returned `OpenIdConnectClientConfiguration` contains: | Property | Description | | -------------------- | ------------------------------------------------ | | `TokenEndpoint` | The token endpoint URI | | `RevocationEndpoint` | The revocation endpoint URI | | `ClientId` | The configured client ID | | `ClientSecret` | The configured client secret (if any) | | `HttpClient` | The `HttpClient` configured for the OIDC handler | | `Scheme` | The authentication scheme name | ### Example: Accessing OIDC Configuration [Section titled “Example: Accessing OIDC Configuration”](#example-accessing-oidc-configuration) ```csharp public class CustomTokenService { private readonly IOpenIdConnectConfigurationService _configService; public CustomTokenService(IOpenIdConnectConfigurationService configService) { _configService = configService; } public async Task GetTokenEndpointAsync(CancellationToken ct) { var config = await _configService.GetOpenIdConnectConfigurationAsync(ct: ct); return config.TokenEndpoint.ToString(); } } ``` Extensibility Summary | Interface | Purpose | | ------------------------------------- | ------------------------------------------- | | `ITokenRetriever` | Replace entire token acquisition logic | | `ITokenRequestCustomizer` | Modify token request parameters per-request | | `TransformPrincipalAfterRefreshAsync` | Transform claims after token refresh | | `IUserAccessor` | Access current user outside HTTP context | | `IUserTokenRequestConcurrencyControl` | Control concurrent token refresh | | `IOpenIdConnectUserTokenEndpoint` | Low-level token endpoint operations | | `IOpenIdConnectConfigurationService` | Access OIDC handler configuration | ----- # Logging > Documentation for logging configuration and usage in Duende Access Token Management, including log levels and Serilog setup Duende Access Token Management uses the standard logging facilities provided by ASP.NET Core. You generally do not need to perform any extra configuration, as it will use the logging provider you have already configured for your application. For log level definitions, environment guidance, and actionable next steps for each level, see the [Logging Fundamentals](/general/logging) guide. [Logging Fundamentals](/general/logging)Log level definitions, environment configuration table, and the log level anxiety spectrum. ## Configuration [Section titled “Configuration”](#configuration) Logs are written under the `Duende.AccessTokenManagement` category. To control log output for Access Token Management specifically, set that namespace in your `appsettings.json`: appsettings.json ```json { "Logging": { "LogLevel": { "Default": "Information", "Duende.AccessTokenManagement": "Debug" } } } ``` Tip The Microsoft [logging documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging) has a good introduction to the built-in logging providers and how to configure them. ## What Gets Logged [Section titled “What Gets Logged”](#what-gets-logged) Access Token Management emits structured log messages across several functional areas. Each message includes contextual parameters (client ID, URL, error details, etc.) for effective filtering and troubleshooting. ### User Token Acquisition [Section titled “User Token Acquisition”](#user-token-acquisition) | Level | Message | Description | | ------- | ------------------------------------------------------------------------------------- | ---------------------------------------------------------- | | Warning | Cannot authenticate scheme: `{Scheme}` to acquire user access token | Authentication failed for the specified scheme | | Warning | Authentication result properties are null for scheme: `{Scheme}` after authentication | Successful authentication but no token properties returned | | Warning | Failed to get a UserToken because no tokens found in cookie properties | `SaveTokens` must be enabled for automatic token refresh | | Debug | Starting user token acquisition | Beginning the user token retrieval process | | Warning | Cannot retrieve token: No active user | No authenticated user context available | | Warning | Cannot retrieve token: No token data found in user token store for user `{User}` | User exists but has no stored tokens | ### Token Refresh [Section titled “Token Refresh”](#token-refresh) | Level | Message | Description | | ----------- | -------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------- | | Debug | No refresh token found in user token store for user `{User}` / resource `{Resource}`. Returning current access token | No refresh token available; returning existing access token | | Debug | No access token found in user token store for user `{User}` / resource `{Resource}`. Trying to refresh | Missing access token; attempting refresh | | Debug | Token for user `{User}` will be refreshed. Expiration: `{Expiration}`, ForceRenewal: `{ForceRenewal}` | Token refresh triggered by expiration or explicit force | | Trace | Refreshing access token using refresh token: hash=`{TokenHash}` | Executing refresh token grant (token hashed for security) | | Debug | Sending Refresh token request to: `{Url}` | HTTP request to token endpoint | | Debug | Returning refreshed token for user: `{User}` | Refresh succeeded | | Debug | Returning current token for user: `{User}` | Using cached token (still valid) | | Warning | Error refreshing access token. Error = `{Error}`, Description: `{ErrorDescription}` | Refresh failed with OAuth error | | Information | Access Token of type `{TokenType}` refreshed with expiration: `{Expiration}` | Successful refresh with new expiration | ### Token Revocation [Section titled “Token Revocation”](#token-revocation) | Level | Message | Description | | ------- | ----------------------------------------------- | ----------------------------------- | | Trace | Revoking refresh token: hash=`{TokenHash}` | Starting revocation (token hashed) | | Debug | Sending Token revocation request to: `{Url}` | HTTP request to revocation endpoint | | Warning | Error revoking refresh token. Error = `{Error}` | Revocation failed | ### Client Credentials [Section titled “Client Credentials”](#client-credentials) | Level | Message | Description | | ----------- | ------------------------------------------------------------------------------------------------------------------ | ------------------------------------------ | | Debug | Requesting client credentials access token at endpoint: `{Url}` | Starting client credentials grant | | Information | Client Credentials token of type `{TokenType}` for Client: `{ClientName}` retrieved with expiration `{Expiration}` | Token successfully obtained | | Warning | Error requesting access token for client `{ClientName}`. Error = `{Error}`, Description: `{ErrorDescription}` | Token request failed | | Debug | Caching access token for client: `{ClientName}`. Expiration: `{Expiration}` | Storing token in cache | | Debug | Cache hit for obtaining access token for client: `{ClientName}` | Using cached token | | Debug | Cache miss while retrieving access token for client: `{ClientName}` | No cached token; fetching new one | | Warning | Will not cache token result with error for `{ClientName}`. Error = `{Error}`, Description: `{ErrorDescription}` | Skipping cache for failed token | | Warning | An exception has occurred while reading ClientCredentialsToken value from the cache for client `{ClientName}` | Cache read error; falling back to fetch | | Warning | Error trying to set token in cache for client `{ClientName}` | Cache write failed | | Warning | Error parsing cached access token for client `{ClientName}` | Cached value was corrupted | | Warning | Failed to obtain token from cache for client `{ClientName}` using cacheKey `{CacheKey}`. Will obtain new token | Cache retrieval failed; fetching new token | ### Request Handler [Section titled “Request Handler”](#request-handler) These messages are emitted by `AccessTokenRequestHandler` when sending HTTP requests with tokens: | Level | Message | Description | | ------- | ------------------------------------------------------------------------------------------------------------------- | -------------------------------------------------------- | | Debug | Sending Access token of type `{TokenType}` to endpoint: `{Url}` | Attaching token to outgoing request | | Warning | Failed to obtain an access token while sending the request. Error: `{Error}`, ErrorDescription `{ErrorDescription}` | Could not get token; request sent without authentication | | Warning | While sending a request, received UnAuthorized after acquiring a new access token | Fresh token was rejected by the resource server | | Debug | Token not accepted while sending request. Retrying with new access token | 401 response triggered token refresh and retry | ### DPoP (Demonstrating Proof-of-Possession) [Section titled “DPoP (Demonstrating Proof-of-Possession)”](#dpop-demonstrating-proof-of-possession) | Level | Message | Description | | ------- | ------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | Debug | Creating DPoP proof token for token request | Generating DPoP proof for token endpoint | | Debug | Sending DPoP proof token in request to endpoint: `{Url}` | Attaching DPoP proof to request | | Debug | Failed to create DPoP proof token for request to endpoint: `{Url}` | DPoP proof generation failed; falling back to Bearer | | Debug | The authorization server has supplied a new nonce on a successful response | Server-provided nonce stored for future requests | | Debug | DPoP nonce error: `{Error}`. Retrying using new nonce | Retrying with server-provided nonce | | Debug | DPoP error `{Error}` during token refresh. Retrying with server nonce | Nonce error during refresh; retrying | | Warning | Failed to get DPoP Nonce because server didn’t respond with ok. StatusCode was: `{StatusCode}` | Nonce request failed | | Trace | Cache hit for DPoP nonce for URL: `{Url}`, method: `{Method}` | Using cached nonce | | Trace | Writing DPoP nonce to Cache for URL: `{Url}`, method: `{Method}`. Expiration: `{Expiration}` | Storing nonce in cache | | Trace | Cache miss for DPoP nonce for URL: `{Url}`, method: `{Method}` | No cached nonce available | | Warning | Failed to parse the cached Nonce `{Value}` for URL: `{Url}`, method: `{Method}`. Error: `{Error}` | Cached nonce was invalid | ### Key Parsing [Section titled “Key Parsing”](#key-parsing) | Level | Message | Description | | ------- | --------------------------------------------- | -------------------------------- | | Warning | Failed to parse JsonWebKey | JWK parsing failed | | Warning | Failed to create thumbprint from JSON web key | Could not compute key thumbprint | ## OpenTelemetry Integration [Section titled “OpenTelemetry Integration”](#opentelemetry-integration) Access Token Management supports [OpenTelemetry](https://opentelemetry.io/) for distributed tracing and metrics collection. ### Tracing [Section titled “Tracing”](#tracing) The library emits traces under the activity source `Duende.AccessTokenManagement`. Add this to your OpenTelemetry configuration: ```csharp builder.Services.AddOpenTelemetry() .WithTracing(tracing => tracing .AddSource("Duende.AccessTokenManagement") // ... other sources ); ``` **Activity spans:** | Name | Description | | --------------------------------------------- | -------------------------------------------- | | `Duende.AccessTokenManagement.AcquiringToken` | Wraps the entire token acquisition operation | ### Metrics [Section titled “Metrics”](#metrics) Metrics are exposed under the meter `Duende.AccessTokenManagement`: ```csharp builder.Services.AddOpenTelemetry() .WithMetrics(metrics => metrics .AddMeter("Duende.AccessTokenManagement") // ... other meters ); ``` **Available counters:** | Counter | Description | Tags | | ------------------------ | ------------------------------------------------------------- | -------------------------------- | | `access_token_used` | Number of times an access token was used | `ClientId`, `TokenType` | | `token_retrieved` | Number of times a token was retrieved from the token endpoint | `ClientId`, `TokenType` | | `token_retrieval_failed` | Number of times token retrieval failed | `ClientId`, `TokenType`, `Error` | | `token_send_retry` | Number of times a token was rejected and retried | `ClientId` | | `dpop_nonce_error_retry` | Number of times a DPoP nonce error triggered a retry | `ClientId`, `Error` | The `TokenType` tag distinguishes between `ClientCredentials` and `User` token flows. ----- # Customizing User Token Management > Learn how to customize user token management options, per-request parameters, and token storage mechanisms in ASP.NET Core applications. The most common way to use [access token management is for interactive web applications](/accesstokenmanagement/web-apps/) - however, you may want to customize certain aspects of it. Here’s what you can do. ## General Options [Section titled “General Options”](#general-options) You can pass in some global options when registering token management in the ASP.NET Core service provider. * `ChallengeScheme` - by default the OIDC configuration is inferred from the default challenge scheme. This is recommended approach. If for some reason your OIDC handler is not the default challenge scheme, you can set the scheme name on the options * `UseChallengeSchemeScopedTokens` - the general assumption is that you only have one OIDC handler configured. If that is not the case, token management needs to maintain multiple sets of token artefacts simultaneously. You can opt in to that feature using this setting. * `RefreshBeforeExpiration` - specifies how long before expiration the token should be refreshed (defaults to 1 minute) * `ClientCredentialsScope` - when requesting client credentials tokens from the OIDC provider, the scope parameter will not be set since its value cannot be inferred from the OIDC configuration. With this setting you can set the value of the scope parameter. * `ClientCredentialsResource` - same as previous, but for the resource parameter * `ClientCredentialStyle` - specifies how client credentials are transmitted to the OIDC provider Program.cs ```csharp builder.Services.AddOpenIdConnectAccessTokenManagement(options => { options.ChallengeScheme = Scheme.Parse("schemeName"); options.UseChallengeSchemeScopedTokens = false; options.RefreshBeforeExpiration = TimeSpan.FromMinutes(2); options.ClientCredentialsScope = Scope.Parse("api1 api2"); options.ClientCredentialsResource = Resource.Parse("urn:resource"); options.ClientCredentialStyle = ClientCredentialStyle.PostBody; }); ``` ## Per Request Parameters [Section titled “Per Request Parameters”](#per-request-parameters) You can also modify token management parameters on a per-request basis. The `UserTokenRequestParameters` class can be used for that: * `SignInScheme` - allows specifying a sign-in scheme. This is used by the default token store * `ChallengeScheme` - allows specifying a challenge scheme. This is used to infer token service configuration * `ForceTokenRenewal` - forces token retrieval even if a cached token would be available * `Scope` - overrides the globally configured scope parameter * `Resource` - override the globally configured resource parameter * `Assertion` - allows setting a client assertion for the request The request parameters can be passed via the manual API: ```csharp var token = await _tokenManagementService .GetAccessTokenAsync(User, new UserTokenRequestParameters { // ... }); ``` …the extension methods ```csharp var token = await HttpContext.GetUserAccessTokenAsync( new UserTokenRequestParameters { // ... }); ``` …or the HTTP client factory Program.cs ```csharp // registers HTTP client that uses the managed user access token builder.Services.AddUserAccessTokenHttpClient("invoices", parameters: new UserTokenRequestParameters { // ... }, configureClient: client => { client.BaseAddress = new Uri("https://api.company.com/invoices/"); }); // registers a typed HTTP client with token management support builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri("https://api.company.com/invoices/"); }) .AddUserAccessTokenHandler(new UserTokenRequestParameters { // ... }); ``` ## Token Storage [Section titled “Token Storage”](#token-storage) By default, the user’s access and refresh token will be stored in the ASP.NET Core authentication session (implemented by the cookie handler). You can modify this in two ways: * the cookie handler itself has an extensible storage mechanism via the `TicketStore` mechanism * replace the store altogether by providing an `IUserTokenStore` implementation and registering it in the service provider at application startup ### IUserTokenStore Interface [Section titled “IUserTokenStore Interface”](#iusertokenstore-interface) The `IUserTokenStore` interface is the primary abstraction for token storage. Implement this interface to store tokens in a database, distributed cache, or other backing store. ```csharp public interface IUserTokenStore { /// /// Stores a token for a user /// Task StoreTokenAsync( ClaimsPrincipal user, UserToken token, UserTokenRequestParameters parameters = default, CancellationToken ct = default); /// /// Retrieves a token for a user. Returns a TokenForParameters which contains /// either the cached token or just the refresh token if no cached token exists. /// Task> GetTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters parameters = default, CancellationToken ct = default); /// /// Clears/removes the stored token for a user /// Task ClearTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters parameters = default, CancellationToken ct = default); } ``` #### Custom Token Store Example [Section titled “Custom Token Store Example”](#custom-token-store-example) The following example shows a custom token store that persists tokens to a database: ```csharp public class DatabaseUserTokenStore : IUserTokenStore { private readonly ITokenRepository _repository; public DatabaseUserTokenStore(ITokenRepository repository) { _repository = repository; } public async Task StoreTokenAsync( ClaimsPrincipal user, UserToken token, UserTokenRequestParameters parameters = default, CancellationToken ct = default) { var userId = user.FindFirstValue("sub") ?? throw new InvalidOperationException("No sub claim found"); await _repository.SaveTokenAsync(userId, new StoredToken { AccessToken = token.AccessToken.ToString(), RefreshToken = token.RefreshToken?.ToString(), Expiration = token.Expiration, Scope = token.Scope?.ToString() }, ct); } public async Task> GetTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters parameters = default, CancellationToken ct = default) { var userId = user.FindFirstValue("sub"); if (userId == null) { return TokenResult.Failure( new TokenResultFailure("No sub claim found")); } var stored = await _repository.GetTokenAsync(userId, ct); if (stored == null) { return TokenResult.Failure( new TokenResultFailure("No token found")); } var userToken = new UserToken { AccessToken = AccessToken.Parse(stored.AccessToken), RefreshToken = stored.RefreshToken != null ? RefreshToken.Parse(stored.RefreshToken) : null, Expiration = stored.Expiration }; var refreshToken = stored.RefreshToken != null ? new UserRefreshToken(RefreshToken.Parse(stored.RefreshToken), null) : null; return TokenResult.Success(new TokenForParameters(userToken, refreshToken)); } public async Task ClearTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters parameters = default, CancellationToken ct = default) { var userId = user.FindFirstValue("sub"); if (userId != null) { await _repository.DeleteTokenAsync(userId, ct); } } } ``` Register your custom store in the service provider: Program.cs ```csharp builder.Services.AddOpenIdConnectAccessTokenManagement(); builder.Services.AddSingleton(); ``` ### IStoreTokensInAuthenticationProperties Interface [Section titled “IStoreTokensInAuthenticationProperties Interface”](#istoretokensinauthenticationproperties-interface) For more granular control over how tokens are stored within the ASP.NET Core `AuthenticationProperties`, implement `IStoreTokensInAuthenticationProperties`. This is a lower-level interface used by the default `IUserTokenStore` implementation. ```csharp public interface IStoreTokensInAuthenticationProperties { /// /// Gets a user token from the authentication properties /// TokenResult GetUserToken( AuthenticationProperties authenticationProperties, UserTokenRequestParameters parameters = default); /// /// Sets a user token in the authentication properties /// Task SetUserTokenAsync( UserToken token, AuthenticationProperties authenticationProperties, UserTokenRequestParameters parameters = default, CancellationToken ct = default); /// /// Removes the user token from the authentication properties /// void RemoveUserToken( AuthenticationProperties authenticationProperties, UserTokenRequestParameters parameters = default); /// /// Gets the authentication scheme to use /// Task GetSchemeAsync( UserTokenRequestParameters parameters = default, CancellationToken ct = default); } ``` This interface is useful when you need to customize how tokens are serialized or structured within the authentication ticket, while still using the cookie-based storage mechanism ----- # Blazor Server Access Token Management > Learn how to manage access tokens in Blazor Server applications and handle token storage and HTTP client usage with Duende.AccessTokenManagement. Blazor Server applications have the same token management requirements as a regular ASP.NET Core web application. Because Blazor Server streams content to the application over a websocket, there often is no HTTP request or response to interact with during the execution of a Blazor Server application. You therefore cannot use `HttpContext` in a Blazor Server application as you would in a traditional ASP.NET Core web application. This means: * you cannot use `HttpContext` extension methods * you cannot use the ASP.NET authentication session to store tokens * the normal mechanism used to automatically attach tokens to Http Clients making API calls won’t work Fortunately, `Duende.AccessTokenManagement` provides a straightforward solution to these problems. Also see the [*BlazorServer* sample](https://github.com/DuendeSoftware/foss/tree/main/access-token-management/samples/BlazorServer) for source code of a full example. ## Token Storage [Section titled “Token Storage”](#token-storage) Since the tokens cannot be managed in the authentication session, you need to store them somewhere else. The options include an in-memory data structure, a distributed cache like redis, or a database. Duende.AccessTokenManagement describes this store for tokens with the *IUserTokenStore* interface. In non-blazor scenarios, the default implementation that stores the tokens in the session is used. In your Blazor server application, you’ll need to decide where you want to store the tokens and implement the store interface. The store interface is straightforward. `StoreTokenAsync` adds a token to the store for a particular user, `GetTokenAsync` retrieves the user’s token, and *ClearTokenAsync* clears the tokens stored for a particular user. A sample implementation that stores the tokens in memory can be found in the *ServerSideTokenStore* in the [*BlazorServer* sample](https://github.com/DuendeSoftware/foss/tree/main/access-token-management/samples/BlazorServer). Register your token store in the ASP.NET Core service provider and tell Duende.AccessTokenManagement to integrate with Blazor by calling `AddBlazorServerAccessTokenManagement`: Program.cs ```csharp builder.Services.AddOpenIdConnectAccessTokenManagement() .AddBlazorServerAccessTokenManagement(); ``` Once you’ve registered your token store, you need to use it. You initialize the token store with the `TokenValidated` event in the OpenID Connect handler: OidcEvents.cs ```csharp public class OidcEvents : OpenIdConnectEvents { private readonly IUserTokenStore _store; public OidcEvents(IUserTokenStore store) { _store = store; } public override async Task TokenValidated(TokenValidatedContext context) { var exp = DateTimeOffset.UtcNow.AddSeconds(double.Parse(context.TokenEndpointResponse!.ExpiresIn)); await _store.StoreTokenAsync(context.Principal!, new UserToken { AccessToken = AccessToken.Parse(context.TokenEndpointResponse.AccessToken), AccessTokenType = AccessTokenType.Parse(context.TokenEndpointResponse.TokenType), RefreshToken = RefreshToken.Parse(context.TokenEndpointResponse.RefreshToken), Scope = Scope.Parse(context.TokenEndpointResponse.Scope), IdentityToken = IdentityToken.Parse(context.TokenEndpointResponse.IdToken), Expiration = exp, }); await base.TokenValidated(context); } } ``` Once registered and initialized, `Duende.AccessTokenManagement` will keep the store up to date automatically as tokens are refreshed. ## Retrieving And Using Tokens [Section titled “Retrieving And Using Tokens”](#retrieving-and-using-tokens) If you’ve registered your token store with `AddBlazorServerAccessTokenManagement`, Duende.AccessTokenManagement will register the services necessary to attach tokens to outgoing HTTP requests automatically, using the same API as a non-blazor application. You inject an HTTP client factory and resolve named HTTP clients where ever you need to make HTTP requests, and you register the HTTP client’s that use access tokens in the ASP.NET Core service provider with our extension method: Program.cs ```csharp builder.Services.AddUserAccessTokenHttpClient("demoApiClient", configureClient: client => { client.BaseAddress = new Uri("https://demo.duendesoftware.com/api/"); }); ``` ----- # Duende AccessTokenManagement v3.x to v4.0 > Guide for upgrading Duende.AccessTokenManagement from version 3.x to version 4.0, including migration steps for custom implementations and breaking changes. ## Changes [Section titled “Changes”](#changes) ### Moving Towards HybridCache Implementation And Away from Distributed Cache [Section titled “Moving Towards HybridCache Implementation And Away from Distributed Cache”](#moving-towards-hybridcache-implementation-and-away-from-distributed-cache) Microsoft has recently released [HybridCache](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid). While this is only released as a .NET 9 assembly, these assemblies work fine in .NET 8. So, while we still support .NET 8 with ATM 4.0, we are moving towards using HybridCache. HybridCache brings significant improvements for us. Because of the two-layered cache, we’ve found it significantly improves performance.\ If you currently use a distributed cache, this should still work seamlessly. If you wish to encrypt access tokens, you can do so by implementing a custom serializer. Documentation on this will follow later. We have added support for using a custom HybridCache instance via keyed services. ### Complete Internal Refactoring [Section titled “Complete Internal Refactoring”](#complete-internal-refactoring) The library has undergone extensive internal changes—so much so that it can be considered a new implementation under the same conceptual umbrella. Despite this, the public API surface remains mostly compatible with earlier versions. * New extensibility model (see below). * All async methods now support cancellation tokens. * Renaming of certain classes and interfaces (see below). * Implementation logic is now internal. #### Reduced Public API Surface [Section titled “Reduced Public API Surface”](#reduced-public-api-surface) All internal implementation details are now marked as internal, reducing accidental coupling and clarifying the intended extension points. In V3, all classes were public and most public methods were marked as virtual. This meant you could override any class by inheriting from it and overriding a single method. While this was very convenient for our consumers, it made it challenging to introduce changes to the library without making breaking changes. We still want to ensure our users’ extensibility needs are met, but via more controlled mechanisms. If you find that you have an extensibility need not covered by the new model, please raise a discussion [in our discussion board](https://duende.link/community). If this is a scenario we want to support, we’ll do our best to accommodate it. ### Explicit Extension Model [Section titled “Explicit Extension Model”](#explicit-extension-model) Instead of relying on implicit behaviors or inheritance, V4 introduces clearly defined extension points, making it easier to customize behavior without relying on internal details. ### Composition Over Inheritance [Section titled “Composition Over Inheritance”](#composition-over-inheritance) The `AccessTokenHandler` has been restructured to use composition rather than inheritance, simplifying the customization of token handling and increasing testability. As part of these changes, the retry logic that was part of the `AccessTokenHandler`’s functionality in V3 has moved to a resiliency policy. If you’re not using `AddClientCredentialsHttpClient` to configure an HTTP client, and you depend on the retry policy to be there, you should use `AddDefaultAccessTokenResiliency` to add our implementation or provide your own retry policy. See [Service Workers](/accesstokenmanagement/workers/) for more details. If you wish to implement a custom access token handling process, for example to implement token exchange, you can now [implement your own `AccessTokenRequestHandler.ITokenRetriever`](/accesstokenmanagement/advanced/extensibility/#token-retrieval). ### Strongly Typed Configuration [Section titled “Strongly Typed Configuration”](#strongly-typed-configuration) Configuration is now represented by strongly typed objects, improving validation, discoverability, and IDE support. This means that where before you could assign strings to the configuration system, you’ll now have to explicitly parse the string values. For example: ```csharp var scheme = Scheme.Parse("oidc"); ``` ### Renamed classes [Section titled “Renamed classes”](#renamed-classes) Several classes have been renamed, either to clarify their usage or to drop the `service` suffix, which only adds noise: * `AccessTokenHandler` is now `AccessTokenRequestHandler` * `IClientCredentialsTokenManagementService` is now `IClientCredentialsTokenManager` * `IClientCredentialsTokenEndpointService` is now `IClientCredentialsTokenEndpoint` * `IUserTokenManagementService` is now `IUserTokenManager` * `ITokenRequestSynchronization` is now `IUserTokenRequestConcurrencyControl` * `IUserTokenEndpointService` is now `IOpenIdConnectUserTokenEndpoint` ----- # Web Applications > Learn how to manage access tokens in web applications, including setup, configuration, and usage with HTTP clients. The `Duende.AccessTokenManagement.OpenIdConnect` library automates all the tasks around access token lifetime management for user-centric web applications. While many of the details can be customized, by default the following is assumed: * ASP.NET Core web application * cookie authentication handler for session management * OpenID Connect authentication handler for authentication and access token requests against an OpenID Connect compliant token service * the token service returns a refresh token Using this library, you can either request `user access tokens` or `client credentials tokens`. User access tokens typically contain information about the currently logged in user, such as the `sub` claim. They are used to access services under the credentials of the currently logged in user. `Client credentials tokens` do not contain information about the currently logged in user and are typically used to do machine-to-machine calls. To get started, you’ll need to add `Duende.AccessTokenManagement.OpenIdConnect` to your solution. Then, there are two fundamental ways to interact with token management: 1. **Automatic** recommended: You request a http client from the IHTTPClientFactory. This http client automatically requests, optionally renews and attaches the access tokens on each request. 2. **Manually** advanced: You request an access token, which you can then use to (for example) authenticate with services. You are responsible for attaching the access token to requests. Let’s look at these steps in more detail. ## Adding AccessTokenManagement To Your Project [Section titled “Adding AccessTokenManagement To Your Project”](#adding-accesstokenmanagement-to-your-project) To use this library, start by adding the library to your .NET projects. ```bash dotnet add package Duende.AccessTokenManagement.OpenIdConnect ``` By default, the token management library will use the ASP.NET Core default authentication scheme for token storage. This is typically the cookie handler and its authentication session. It also used the default challenge scheme for deriving token client configuration for refreshing tokens or requesting client credential tokens (typically the OpenID Connect handler pointing to your trusted authority). Program.cs ```csharp // setting up default schemes and handlers builder.Services.AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("cookie", options => { options.Cookie.Name = "web"; // automatically revoke refresh token at signout time options.Events.OnSigningOut = async e => { await e.HttpContext.RevokeRefreshTokenAsync(); }; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://sts.company.com"; options.ClientId = "webapp"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.Scope.Clear(); // OIDC related scopes options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); // API scopes options.Scope.Add("invoice"); options.Scope.Add("customer"); // requests a refresh token options.Scope.Add("offline_access"); options.GetClaimsFromUserInfoEndpoint = true; options.MapInboundClaims = false; // important! this store the access and refresh token in the authentication session // this is needed to the standard token store to manage the artefacts options.SaveTokens = true; options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", RoleClaimType = "role" }; }); // adds services for token management builder.Services.AddOpenIdConnectAccessTokenManagement(); ``` ## Automatic Via HTTP Client Factory [Section titled “Automatic Via HTTP Client Factory”](#automatic-via-http-client-factory) Similar to the worker service support, you can register HTTP clients that automatically send the access token of the current user when making API calls. The message handler associated with those HTTP clients will try to make sure, the access token is always valid and not expired. Program.cs ```csharp // registers HTTP client that uses the managed user access token builder.Services.AddUserAccessTokenHttpClient("invoices", configureClient: client => { client.BaseAddress = new Uri("https://api.company.com/invoices/"); }); ``` This could be also a typed client: Program.cs ```csharp // registers a typed HTTP client with token management support builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri("https://api.company.com/invoices/"); }) .AddUserAccessTokenHandler(); ``` Of course, the ASP.NET Core web application host could also do machine to machine API calls that are independent of a user. In this case all the token client configuration can be inferred from the OpenID Connect handler configuration. The following registers an HTTP client that uses a client credentials token for outgoing calls: ```csharp // registers HTTP client that uses the managed client access token builder.Services.AddClientAccessTokenHttpClient("masterdata.client", configureClient: client => { client.BaseAddress = new Uri("https://api.company.com/masterdata/"); }); ``` As a typed client: Program.cs ```csharp builder.Services.AddHttpClient(client => { client.BaseAddress = new Uri("https://api.company.com/masterdata/"); }) .AddClientAccessTokenHandler(); ``` Last but not least, if you registered clients with the factory, you can use them. They will try to make sure that a current access token is always sent along. If that is not possible, ultimately a `401` HTTP status code will be returned to the calling code. ```csharp public async Task CallApi() { var client = _httpClientFactory.CreateClient("invoices"); var response = await client.GetAsync("list"); // rest omitted } ``` …or for a typed client: ```csharp public async Task CallApi([FromServices] InvoiceClient client) { var response = await client.GetList(); // rest omitted } ``` ### gRPC Support [Section titled “gRPC Support”](#grpc-support) If you are using gRPC, you can also use the `AddClientAccessTokenHandler` and `AddUserAccessTokenHandler` methods when registering typed gRPC clients: Program.cs ```csharp builder.Services.AddGrpcClient(o => { o.Address = new Uri("https://localhost:5001"); }) .AddUserAccessTokenHandler(); // or .AddClientAccessTokenHandler(); when using client credentials ``` ## Manually Request Access Tokens [Section titled “Manually Request Access Tokens”](#manually-request-access-tokens) If you want to use access tokens differently or have more advanced needs which the automatic option doesn’t cover, then you can also manually request user access tokens. * V4 You can get the current user access token manually by writing code against the `IUserTokenManager`. The `GetAccessTokenAsync` method returns a `TokenResult`. You can use the `.GetToken()` extension method which throws if the token request failed, or check the `.Succeeded` property for more control over error handling. Program.cs ```csharp public class HomeController : Controller { private readonly IHttpClientFactory _httpClientFactory; private readonly IUserTokenManager _userTokenManager; public HomeController(IHttpClientFactory httpClientFactory, IUserTokenManager userTokenManager) { _httpClientFactory = httpClientFactory; _userTokenManager = userTokenManager; } public async Task CallApi(CancellationToken ct) { // Option 1: Using .GetToken() - throws if token request failed var token = await _userTokenManager.GetAccessTokenAsync(User, ct: ct).GetToken(); var client = _httpClientFactory.CreateClient(); client.SetBearerToken(token.AccessToken.ToString()); var response = await client.GetAsync("https://api.company.com/invoices", ct); // rest omitted } public async Task CallApiWithErrorHandling(CancellationToken ct) { // Option 2: Checking result explicitly var result = await _userTokenManager.GetAccessTokenAsync(User, ct: ct); if (!result.Succeeded) { // Handle the failure - result.FailedResult contains error details return Unauthorized(); } var client = _httpClientFactory.CreateClient(); client.SetBearerToken(result.Token.AccessToken.ToString()); var response = await client.GetAsync("https://api.company.com/invoices", ct); // rest omitted } } ``` * V3 You can get the current user access token manually by writing code against the `IUserTokenManagementService`. Program.cs ```csharp public class HomeController : Controller { private readonly IHttpClientFactory _httpClientFactory; private readonly IUserTokenManagementService _tokenManagementService; public HomeController(IHttpClientFactory httpClientFactory, IUserTokenManagementService tokenManagementService) { _httpClientFactory = httpClientFactory; _tokenManagementService = tokenManagementService; } public async Task CallApi() { var token = await _tokenManagementService.GetAccessTokenAsync(User); var client = _httpClientFactory.CreateClient(); client.SetBearerToken(token.Value); var response = await client.GetAsync("https://api.company.com/invoices"); // rest omitted } } ``` ### HTTP Context Extension Methods [Section titled “HTTP Context Extension Methods”](#http-context-extension-methods) Alternatively, you can also manually request access tokens via these extension methods on the `HttpContext`: * `GetUserAccessTokenAsync` - returns a `TokenResult` representing the user. If the current access token is expired, it will be refreshed. * `GetClientAccessTokenAsync` - returns a `TokenResult` representing the client. If the current access token is expired, a new one will be requested * `RevokeRefreshTokenAsync` - revokes the refresh token ```csharp public async Task CallApi(CancellationToken ct) { var token = await HttpContext.GetUserAccessTokenAsync(ct: ct).GetToken(); var client = _httpClientFactory.CreateClient(); client.SetBearerToken(token.AccessToken.ToString()); var response = await client.GetAsync("https://api.company.com/invoices", ct); // rest omitted } ``` ----- # Service Workers and Background Tasks > Learn how to manage OAuth access tokens in worker applications and background tasks using Duende.AccessTokenManagement. A common scenario in worker applications or background tasks (or really any daemon-style applications) is to call APIs using an OAuth token obtained via the client credentials flow. The access tokens need to be requested and [cached](/accesstokenmanagement/advanced/client-credentials/#token-caching) (either locally or shared between multiple instances) and made available to the code calling the APIs. In case of expiration (or other token invalidation reasons), a new access token needs to be requested. The actual business code should not need to be aware of this. For more information, see the [advanced topic on client credentials](/accesstokenmanagement/advanced/client-credentials/). Sample code Take a look at the [`Worker` project in the samples folder](https://github.com/DuendeSoftware/foss/tree/main/access-token-management/samples/) for example code. To get started, you will need to add the `Duende.AccessTokenManagement` package to your solution. Next, there are two fundamental ways to interact with token management: 1. **Automatic** recommended: You request an `HttpClient` from the `IHttpClientFactory`. This HTTP client automatically requests, optionally renews and attaches the access tokens on each request. 2. **Manually** advanced: You request an access token, which you can then use to (for example) authenticate with services. You are responsible for attaching the access token to requests. Let’s cover these steps in more detail. ## Adding Duende.AccessTokenManagement [Section titled “Adding Duende.AccessTokenManagement”](#adding-duendeaccesstokenmanagement) Start by adding a reference to the `Duende.AccessTokenManagement` NuGet package to your application. ```bash dotnet add package Duende.AccessTokenManagement ``` You can add the necessary services to the ASP.NET Core service provider by calling `AddClientCredentialsTokenManagement()`. After that you can add one or more named client definitions by calling `AddClient`. * V4 Program.cs ```csharp services.AddClientCredentialsTokenManagement() .AddClient(ClientCredentialsClientName.Parse("catalog.client"), client => { client.TokenEndpoint = new Uri("https://demo.duendesoftware.com/connect/token"); client.ClientId = ClientId.Parse("6f59b670-990f-4ef7-856f-0dd584ed1fac"); client.ClientSecret = ClientSecret.Parse("d0c17c6a-ba47-4654-a874-f6d576cdf799"); client.Scope = Scope.Parse("catalog inventory"); }) .AddClient(ClientCredentialsClientName.Parse("invoice.client"), client => { client.TokenEndpoint = new Uri("https://demo.duendesoftware.com/connect/token"); client.ClientId = ClientId.Parse("ff8ac57f-5ade-47f1-b8cd-4c2424672351"); client.ClientSecret = ClientSecret.Parse("4dbbf8ec-d62a-4639-b0db-aa5357a0cf46"); client.Scope = Scope.Parse("invoice customers"); }); ``` * V3 Program.cs ```csharp services.AddClientCredentialsTokenManagement() .AddClient("catalog.client", client => { client.TokenEndpoint = "https://demo.duendesoftware.com/connect/token"; client.ClientId = "6f59b670-990f-4ef7-856f-0dd584ed1fac"; client.ClientSecret = "d0c17c6a-ba47-4654-a874-f6d576cdf799"; client.Scope = "catalog inventory"; }) .AddClient("invoice.client", client => { client.TokenEndpoint = "https://demo.duendesoftware.com/connect/token"; client.ClientId = "ff8ac57f-5ade-47f1-b8cd-4c2424672351"; client.ClientSecret = "4dbbf8ec-d62a-4639-b0db-aa5357a0cf46"; client.Scope = "invoice customers"; }); // in v3, you explicitly need to add a distributed cache implementation, such as in-memory services.AddDistributedMemoryCache(); ``` ## Automatic Token Management Using HTTP Factory [Section titled “Automatic Token Management Using HTTP Factory”](#automatic-token-management-using-http-factory) You can register HTTP clients with the factory that will automatically use the above client definitions to request and use access tokens. The following code registers an `HttpClient` called `invoices` which automatically uses the `invoice.client` definition: * V4 Program.cs ```csharp services.AddClientCredentialsHttpClient("invoices", ClientCredentialsClientName.Parse("invoice.client"), client => { client.BaseAddress = new Uri("https://apis.company.com/invoice/"); }); ``` You can also set up a typed HTTP client to use a token client definition, e.g.: Program.cs ```csharp services.AddHttpClient(client => { client.BaseAddress = new Uri("https://apis.company.com/catalog/"); }) .AddClientCredentialsTokenHandler(ClientCredentialsClientName.Parse("catalog.client")); ``` * V3 Program.cs ```csharp services.AddClientCredentialsHttpClient("invoices", "invoice.client", client => { client.BaseAddress = new Uri("https://apis.company.com/invoice/"); }); ``` You can also set up a typed HTTP client to use a token client definition, e.g.: Program.cs ```csharp services.AddHttpClient(client => { client.BaseAddress = new Uri("https://apis.company.com/catalog/"); }) .AddClientCredentialsTokenHandler("catalog.client"); ``` Once you have set up HTTP clients in the HTTP factory, no token-related code is needed at all, e.g.: WorkerHttpClient.cs ```csharp public class WorkerHttpClient : BackgroundService { private readonly ILogger _logger; private readonly IHttpClientFactory _clientFactory; public WorkerHttpClient(ILogger logger, IHttpClientFactory factory) { _logger = logger; _clientFactory = factory; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var client = _clientFactory.CreateClient("invoices"); var response = await client.GetAsync("test", stoppingToken); // rest omitted } } } ``` Default resiliency handler in v4 When you use `AddClientCredentialsHttpClient`, the configured HTTP client will include a resiliency message handler which automatically retries the HTTP request in case of a `401 Unauthorized` response. This retry helps in two scenarios: * The access token has expired. A new token is requested to retry the original HTTP request. * You’re using [DPoP](/accesstokenmanagement/advanced/dpop/). The DPoP request may need to be retried with a nonce value present in the HTTP response. The retry only happens once: if it still results in `401 Unauthorized`, the response is returned to the caller. This functionality is **not** included when you use `AddClientCredentialsTokenHandler` when registering your own HTTP clients. You can however add the resiliency message handler manually: ```csharp services.AddHttpClient(client => { client.BaseAddress = new Uri("https://apis.company.com/catalog/"); }) .AddDefaultAccessTokenResiliency() .AddClientCredentialsTokenHandler("catalog.client"); ``` ## Manually Request Access Tokens [Section titled “Manually Request Access Tokens”](#manually-request-access-tokens) If you want to use access tokens in a different way or have more advanced needs which the automatic option doesn’t cover, then you can also manually request access tokens. * V4 You can retrieve the current access token for a given token client via `IClientCredentialsTokenManager.GetAccessTokenAsync`. WorkerManual.cs ```csharp public class WorkerManual( IHttpClientFactory factory, IClientCredentialsTokenManager tokenManagementService ) : BackgroundService { protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var client = factory.CreateClient(); client.BaseAddress = new Uri("https://apis.company.com/catalog/"); // get access token for client and set on HttpClient var token = await tokenManagementService.GetAccessTokenAsync( ClientCredentialsClientName.Parse("catalog.client"), ct: stoppingToken) .GetToken(); client.SetBearerToken(token.AccessToken.ToString()); var response = await client.GetAsync("list", stoppingToken); // rest omitted } } } ``` The result of the GetAccessTokenAsync method is a `TokenResult`. You can interrogate this to see if the result was successful by checking the `Succeeded` property or by calling `WasSuccessful()`. Alternatively, as you see in the example, you can call the `.GetToken()` which will throw if the token couldn’t be retrieved. You can customize some of the per-request parameters by passing in an instance of `TokenRequestParameters`. This allows forcing a fresh token request (even if a cached token would exist) and also allows setting a per-request scope, resource and client assertion. ### Error Handling with TokenResult [Section titled “Error Handling with TokenResult”](#error-handling-with-tokenresult) The `TokenResult` type provides multiple ways to handle token acquisition failures: TokenResultErrorHandling.cs ```csharp // Option 1: Using .GetToken() - throws on failure (best for simple cases) var token = await tokenManager .GetAccessTokenAsync(clientName, ct: stoppingToken) .GetToken(); // Option 2: Using .Succeeded property (best for custom error handling) var result = await tokenManager.GetAccessTokenAsync(clientName, ct: stoppingToken); if (!result.Succeeded) { _logger.LogError("Token request failed: {Error} - {ErrorDescription}", result.FailedResult.Error, result.FailedResult.ErrorDescription); return; } var token = result.Token; // Option 3: Using WasSuccessful with out parameter if (result.WasSuccessful(out var successToken)) { client.SetBearerToken(successToken.AccessToken.ToString()); } ``` * V3 You can retrieve the current access token for a given token client via `IClientCredentialsTokenManagementService.GetAccessTokenAsync`. WorkerManual.cs ```csharp public class WorkerManual : BackgroundService { private readonly IHttpClientFactory _clientFactory; private readonly IClientCredentialsTokenManagementService _tokenManagementService; public WorkerManual(IHttpClientFactory factory, IClientCredentialsTokenManagementService tokenManagementService) { _clientFactory = factory; _tokenManagementService = tokenManagementService; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { while (!stoppingToken.IsCancellationRequested) { var client = _clientFactory.CreateClient(); client.BaseAddress = new Uri("https://apis.company.com/catalog/"); // get access token for client and set on HttpClient var token = await _tokenManagementService.GetAccessTokenAsync("catalog.client"); client.SetBearerToken(token.Value); var response = await client.GetAsync("list", stoppingToken); // rest omitted } } } ``` You can customize some of the per-request parameters by passing in an instance of `ClientCredentialsTokenRequestParameters`. This allows forcing a fresh token request (even if a cached token would exist) and also allows setting a per-request scope, resource and client assertion. ----- # Backend For Frontend (BFF) Security Framework > A comprehensive security framework for securing browser-based frontends with ASP.NET Core backends The Duende.BFF (Backend For Frontend) security framework packages the necessary components to secure browser-based frontends (e.g., SPAs or Blazor applications) with ASP.NET Core backends. Duende.BFF is a library for building services that comply with the BFF pattern and solve security and identity problems in browser-based applications such as SPAs and Blazor-based applications. It is used to create a backend host that is paired with a frontend application. This backend is called the Backend For Frontend (BFF) host, and is responsible for all the OAuth and OIDC protocol interactions. It completely implements the latest recommendations from the IETF regarding security for browser-based applications. It offers the following functionality: * Protection from Token Extraction attacks * Built-in CSRF Attack protection * Server Side OAuth 2.0 Support * Multi-frontend support (Introduced in V4) * User Management APIs * Back-channel logout * Securing access to both local and external APIs by serving as a reverse proxy. * Server side Session State Management * Blazor Authentication State Management * Open Telemetry support (Introduced in V4) Duende.BFF is free for development, testing and personal projects, but production use requires a license. Special offers may apply. The source code for the BFF framework can be found on GitHub. Builds are distributed through NuGet. Also check out the samples. [GitHub Repository](https://github.com/DuendeSoftware/products/tree/main/bff)View the source code for this library on GitHub. [NuGet Package](https://www.nuget.org/packages/Duende.BFF)View the package on NuGet.org. ## Getting Started [Section titled “Getting Started”](#getting-started) If you’re upgrading from a previous version, please check our [upgrade guides](/bff/upgrading). If you’re starting a new BFF project, consider the following startup guides: * [Single frontend BFF](/bff/getting-started/single-frontend/) * [Multi-frontend BFF](/bff/getting-started/multi-frontend/) * [Blazor](/bff/getting-started/blazor/) ## Do I Need BFF? [Section titled “Do I Need BFF?”](#do-i-need-bff) If you’re building a browser-based application (SPA or Blazor WASM) that needs to call authenticated APIs, BFF is the recommended security architecture. This section helps you decide. ### BFF Pattern vs. Token-in-Browser [Section titled “BFF Pattern vs. Token-in-Browser”](#bff-pattern-vs-token-in-browser) | Concern | BFF Pattern | Token-in-Browser | | ----------------------------- | ------------------------------------------------- | ---------------------------------------------------------------- | | **Token storage** | Server-side only — browser never sees tokens | Tokens in `localStorage` or `sessionStorage` | | **XSS token theft** | Not possible — no tokens in browser memory | High risk — any injected script can steal tokens | | **Third-party cookie issues** | Not affected — uses first-party session cookies | Silent renewal via `prompt=none` breaks in Safari/Firefox/Chrome | | **CSRF exposure** | Mitigated with `X-CSRF` header + SameSite cookies | Not applicable (tokens sent as `Authorization` header) | | **Session revocation** | Server can forcibly end sessions | No server-side control; wait for token expiry | | **Complexity** | Slightly more infrastructure (BFF host required) | Simpler initial setup, but security is harder to get right | | **IETF recommendation** | ✅ Recommended (OAuth 2.0 for Browser-Based Apps) | ❌ Implicit grant deprecated; token-in-browser discouraged | Security implications of NOT using BFF Storing access tokens in the browser (e.g., `localStorage`, `sessionStorage`, or JavaScript memory) exposes them to theft via XSS attacks and supply-chain attacks through compromised npm packages. Once stolen, an attacker can use the token independently of the user’s browser — there is no way to detect or stop this. Additionally, OIDC silent login (`prompt=none`) relies on third-party cookies, which are blocked by Safari, Firefox, and increasingly Chrome. This means session management in token-in-browser apps will silently break for a growing proportion of users. See [Threats Against Browser-based Applications](#threats-against-browser-based-applications) below for the full threat model. When BFF is the right choice Use BFF if any of the following are true: * Your frontend calls APIs that require user authentication * You need reliable session management across browsers * You want protection against XSS token theft * You’re building a new application and want to follow current IETF best practices See the [architecture](/bff/architecture/) pages for a deeper discussion of the threat model. ## Background [Section titled “Background”](#background) Single-Page Applications (SPAs) are increasingly common, offering rich functionality within the browser. Front-end development has rapidly evolved with new frameworks and changing browser security requirements. Consequently, best practices for securing these applications have also shifted dramatically. While implementing OAuth logic directly in the browser was once considered acceptable, this is no longer recommended. Storing any authentication state in the browser (such as access tokens) has proven to be inherently risky (see Threats against browser based applications). Because of this, the IETF is currently recommending delegating all authentication logic to a server-based host via a Backend-For-Frontend pattern as the preferred approach to securing modern web applications. ### The Backend For Frontend Pattern [Section titled “The Backend For Frontend Pattern”](#the-backend-for-frontend-pattern) The BFF pattern (Backend-For-Frontend) pattern states that every browser based application should also have a server side application that handles all authentication requirements, including performing authentication flows and securing access to APIs. The server will now expose http endpoints that the browser can use to login, logout or interrogate the active session. With this, the browser based application can trigger an authentication flow by redirecting to a URL, such as /bff/login. Once the authentication process is completed, the server places a secure authentication cookie in the browser. This cookie is then used to authenticate all subsequent requests, until the user is logged out again. The BFF should expose all APIs that the front-end wants to access securely. So it can either host APIs locally, or act as a reverse proxy towards external APIs. With this approach, the browser based application will not have direct access to the access token. So if the browser based application is compromised, for example with XSS attacks, there is no risk of the attacker stealing the access tokens. As the name of this pattern already implies, the BFF backend is the (only) Backend for the Frontend. They should be considered part of the same application. It should only expose the APIs that the front-end needs to function. ### 3rd party cookies [Section titled “3rd party cookies”](#3rd-party-cookies) In recent years, several browsers (notably Safari and Firefox) have started to block 3rd party cookies. Chrome is planning to do the same in the future. While this is done for valid privacy reasons, it also limits some of the functionality a browser based application can provide. A couple of particularly notable OIDC flows that don’t work for SPAs when third party cookies are blocked are OIDC Session Management and OIDC Silent Login via the prompt=none parameter. ### CSRF protection [Section titled “CSRF protection”](#csrf-protection) There is one thing to keep an eye out for with this pattern, and that’s Cross Site Request Forgery (CSRF). The browser automatically sends the authentication cookie for safe-listed cross-origin requests, which exposes the application to CORS Attacks. Fortunately, this threat can easily be mitigated by a BFF solution by requiring a custom header to be passed along. See more on CORS protection. ### The BFF Framework in an application architecture [Section titled “The BFF Framework in an application architecture”](#the-bff-framework-in-an-application-architecture) The following diagram illustrates how the Duende BFF Security Framework fits into a typical application architecture. ``` flowchart TD subgraph Browser SPA["Browser-Based Application"] CookieJar["🍪 Cookie Jar"] end subgraph BFF["BFF Host"] AuthEndpoints["Authentication
Endpoints"] SessionMgmt["Session
Management"] CookieAuth["Cookie Authorization"] CSRF["CSRF Protection"] Proxy["Proxy to
External APIs"] LocalAPIs["Local APIs"] SessionStore[("Server-Side
Session Storage")] end IdP["Identity Provider"] ExternalAPIs["External APIs"] SPA -->|"login / logout"| AuthEndpoints AuthEndpoints -->|"Set-Cookie"| CookieJar CookieJar -->|"Auth cookie"| CookieAuth AuthEndpoints <-->|"redirect"| IdP AuthEndpoints --> SessionMgmt SessionMgmt --> SessionStore CookieAuth --> CSRF CSRF --> Proxy CSRF --> LocalAPIs Proxy -->|"Bearer token"| ExternalAPIs SessionMgmt -->|"Acquire tokens"| IdP ExternalAPIs -->|"Validate tokens"| IdP ``` The browser based application runs inside the browser’s secure sandbox. It can be built using any type of front-end technology, such as via Vanilla-JS, React, Vue, WebComponents, Blazor, etc. When the user wants to log in, the app can redirect the browser to the authentication endpoints. This will trigger an OpenID Connect authentication flow, at the end of which, it will place an authentication cookie in the browser. This cookie has to be an HTTP Only Same Site and Secure cookie. This makes sure that the browser application cannot get the contents of the cookie, which makes stealing the session much more difficult. The browser will now automatically add the authentication cookie to all calls to the BFF, so all calls to the APIs are secured. This means that embedded (local) APIs are already automatically secured. The app cannot access external Api’s directly, because the authentication cookie won’t be sent to 3rd party applications. To overcome this, the BFF can proxy requests through the BFF host, while exchanging the authentication cookie for a bearer token that’s issued from the identity provider. This can be configured to include or exclude the user’s credentials. As mentioned earlier, the BFF needs protection against CSRF attacks, because of the nature of using authentication cookies. While .net has various built-in methods for protecting against CSRF attacks, they often require a bit of work to implement. The easiest way to protect (just as effective as the .Net provided security mechanisms) is just to require the use of a custom header. The BFF Security framework by default requires the app to add a custom header called x-csrf=1 to the application. Just the fact that this header must be present is enough to protect the BFF from CSRF attacks. ### Logical and Physical Sessions [Section titled “Logical and Physical Sessions”](#logical-and-physical-sessions) When implemented correctly, a user will think of their time interacting with a solution as *“one session”* also known as the **“logical session”**. The user should not be concerned with the steps developers take to provide a seamless experience. Users want to use the app, get their tasks completed, and log out happy. ``` sequenceDiagram actor Alice box logical session participant App end Alice->>App: /login App->>Alice: /account ``` So while the user will only see (and care about) a single session, it’s entirely possible that there will be multiple physical sessions active. For most distributed applications, including those implemented with BFF, **sessions are managed independently by each component of an application architecture.** This means that there are **N+1** physical sessions possible, where **N** is the number of sessions for each service in your solution, and the **+1** being the session managed on the BFF host. Since we are focusing on ASP.NET Core, those sessions typically are stored using the Cookie Authentication handler features of .NET. ``` sequenceDiagram actor Alice box App session participant App end box Service 1 session participant Service 1 end box Service N... session participant Service N... end Alice->>App: /login App->>Alice: /account App->>Service 1: request App->>Service N...: N... request ``` The separation allows each service to manage its session to its specific needs. While it can depend on your requirements, we find most developers want to coordinate the physical session lifetimes, creating a more predictable logical session. If that is your case, we recommend you first start by turning each physical session into a more powerful [server-side session](/bff/fundamentals/session/server-side-sessions/). Server-side sessions are instances that are persisted to data storage and allow for visibility into currently active sessions and better management techniques. Let’s take a look at the advantages of server-side sessions. Server-side sessions at each component allows for: * Receiving back channel logout notifications * Forcibly end a user’s session of that node * Store and view information about a session lifetime * Coordinate sessions across an application’s components * Different claims data Server-side sessions at IdentityServer allow for more powerful features: * Receive back channel logout notifications from upstream identity providers in a federation * Forcibly end a user’s session at IdentityServer * Global inactivity timeout across SSO apps and session coordination * Coordinate sessions to registered clients Keep in mind the distinctions between logical and physical sessions, and you will better understand the interplay between elements in your solution. ### Threats Against Browser-based Applications [Section titled “Threats Against Browser-based Applications”](#threats-against-browser-based-applications) Let’s look at some of the common ways browser-based apps are typically attacked and what their consequences would be. #### Token theft [Section titled “Token theft”](#token-theft) Often, malicious actors are trying to steal access tokens. In this paragraph, we’ll look into several techniques how this is often done and what the consequences are. But it’s important to note that all these techniques rely on the browser-based application having access to the access token. Therefore, these attacks can be prevented by implementing the BFF pattern. ##### Script injection attacks [Section titled “Script injection attacks”](#script-injection-attacks) The most common way malicious actors steal access tokens is by injecting malicious JavaScript code into the browser. This can happen in many different ways. Script injection attacks or supply chain attacks (via compromised NPM packages or cloud-hosted scripts) are just some examples. Since the malicious code runs in the same security sandbox as the application’s code, it has exactly the same privileges as the application code. This means there is no way to securely store and handle access tokens in the browser. There have been attempts to place the code that accesses and uses web tokens in more highly isolated storage areas, such as Web Workers, but these attempts have also been proven to be vulnerable to token exfiltration attacks, so they are not suitable as an alternative. If the browser-based application has access to your access token, so can malicious actors. ##### Other ways of compromising browser security [Section titled “Other ways of compromising browser security”](#other-ways-of-compromising-browser-security) Injecting code is not the only way that browser security can be broken. Sometimes the browser sandbox itself is under attack. Browsers attempt to provide a secure environment in which web pages and their scripts can safely be loaded and executed in isolation. On many occasions, this browser sandbox has been breached by exploits. A recent example is the POC from Google on Browser-Based Spectre Attacks. By bypassing the security sandbox, the attackers are able to read the memory from your application and steal the access tokens. The best way to protect yourself from this is not having any access tokens stored in the application’s memory at all by following the BFF pattern. ##### Consequences of token theft [Section titled “Consequences of token theft”](#consequences-of-token-theft) Once an attacker is able to inject malicious code, there are a number of things the attacker can do. At a minimum, the attacker can take over the current user’s session and in the background perform malicious actions under the credentials of the user. This would only be possible as long as the user has the application open, which limits how long the attacker can misuse the session. It’s worse if the attacker is able to extract the authentication token. The attacker can now access the application directly from his own computer, as long as the access token is valid. For this reason, it’s recommended to keep access token lifetimes short. If the attacker is also able to acquire the refresh token or worse, is able to request new tokens, then the attacker can use the credentials indefinitely. ##### Attacks at OAuth Implicit Grant [Section titled “Attacks at OAuth Implicit Grant”](#attacks-at-oauth-implicit-grant) Sometimes there are vulnerabilities discovered even in the protocols that are underlying most of the web’s security. As a result, these protocols are constantly evolving and updated to reflect the latest knowledge and known vulnerabilities. One example of this is OAuth Implicit grant. This was once a recommended pattern and many applications have implemented this since. However, in recent years it’s become clear that this protocol is no longer deemed secure and in the words of the IETF: > Browser-based clients MUST use the Authorization Code grant type and MUST NOT use the Implicit grant type to obtain access tokens #### CSRF Attacks [Section titled “CSRF Attacks”](#csrf-attacks) Cookie-based authentication (when using Secure and HTTP Only cookies) effectively prevents browser-based token stealing attacks. But this approach is vulnerable to a different type of attack, namely CSRF attacks. This is similar but different from CORS attacks which lies in the definition of what the browser considers a Site vs an Origin and what kind of request a browser considers ‘safe’ for Cross Origin requests. ##### Origins and Sites [Section titled “Origins and Sites”](#origins-and-sites) To a browser, a [site](https://developer.mozilla.org/en-US/docs/Glossary/Site) is defined as TLD (top-level domain - 1). So, a single segment under a top-level domain, such as example in `example.co.uk`, where `co.uk` is the top-level domain. Any subdomain under that (so `site1.example.co.uk` and `www.example.co.uk`) are considered to be from the same site. Contrast this to an origin, which is the scheme + hostname + port. In the previous example, the origins would be `https://example.co.uk` and `https://www.example.co.uk`. The site is the same, but the origin is different. Browsers have built-in control when cookies should be sent. For example, by setting [SameSite=strict](https://owasp.org/www-community/SameSite), the browser will only send along cookies if you are navigating within the same **site** (not origins). Browsers also have built-in **Cross Origin** protection. Most requests that go across different origins (not sites) will by default be subjected to CORS protection. This means that the server needs to say if the requests are safe to use cross-origin. The exclusion to this are requests that the browser considers safe. The following diagram (created based on this article [Wikipedia](https://en.wikipedia.org/wiki/Cross-origin_resource_sharing)) shows this quite clearly: ``` flowchart TD; A[JavaScript makes a cross-domain XHR call] --> B{Is it a GET or HEAD?}; subgraph cors-safe B -->|Yes| X[Make actual XHR]; B -->|No| C{Is it a POST?}; C -->|Yes| E{Is the content-type standard?}; C -->|No| D[Make OPTIONS call to server with all custom details]; E -->|No| D; E -->|Yes| F{Are there custom HTTP headers?}; F -->|No| X; F -->|Yes| D; end subgraph cors-verify D --> G{Did server respond with appropriate Access-Control-* headers?}; G -->|No| H[ERROR]; end G -->|Yes| X; style cors-safe fill:#d9ead3,stroke:#6aa84f; style cors-verify fill:#f4cccc,stroke:#cc0000; ``` So some requests, like regular GET or POSTs with a standard content type are NOT subject to CORS validation, but others (IE: deletes or requests with a custom HTTP header) are. ##### CSRF Attack inner workings [Section titled “CSRF Attack inner workings”](#csrf-attack-inner-workings) CSRF attacks exploit the fact that browsers automatically send authentication cookies with requests to the same [site](https://developer.mozilla.org/en-US/docs/Glossary/Site). Should an attacker trick a user that’s logged in to an application into visiting a malicious website, that browser can make malicious requests to the application under the credentials of the user. Same Site cookies already drastically reduce the attack surface because they ensure the browser only sends the cookies when the user is on the same site. So a user logged in to an application at app.company.com will not be vulnerable when visiting malicious-site.com. However, the application can still be at risk. Should other applications running under different subdomains of the same site be compromised, then you are still vulnerable to CSRF attacks. Luring a user to a compromised site under a subdomain will bypass this Same Site protection and leave the application still vulnerable to CSRF attacks. Unfortunately, compromised applications running under different subdomains is a common attack vector, not to be underestimated. ##### Protection against CSRF Attacks [Section titled “Protection against CSRF Attacks”](#protection-against-csrf-attacks) Many frameworks, including [dotnet](https://learn.microsoft.com/en-us/aspnet/core/security/anti-request-forgery?view=aspnetcore-9.0), have built-in protection against CSRF attacks. These mitigations require you to make certain changes to your application, such as embedding specific form fields in your application which needs to be re-submitted or reading a specific cookie value. While these protections are effective, there is a simpler and more straight forward solution to preventing any CSRF attack. The trick is to require a custom header on the APIs that you wish to protect. It doesn’t matter what that custom header is or what the value is, for example, some-header=1. The browser-based application now MUST send this header along with every request. However, if a page on the malicious subdomain wants to call this API, it also has to add this custom header. This custom header now triggers a CORS Preflight check. This pre-flight check will fail because it detects that the request is cross-origin. Now the API developer has to develop a CORS policy that will protect against CORS attacks. So, effective CSRF attack protection relies on these pillars: 1. Using **Same-Site=strict** Cookies 2. Requiring a specific header to be sent on every API request (IE: x-csrf=1) 3. having a cors policy that restricts the cookies only to a list of white-listed **origins**. ##### Session Hijacking [Section titled “Session Hijacking”](#session-hijacking) In session hijacking, a malicious actor somehow gets access to the user’s session cookie and is then able to exploit it by effectively cloning the session. Before HTTPS was widespread, session hijacking was a common occurrence, especially when using public Wi-Fi networks. However, since SSL connections are pretty much widespread, this has become more difficult. Not impossible, because there have been cases where trusted certificate authorities have been compromised. Even if SSL is not compromised, there are other ways for malicious actors to hijack the session. For example, if the user’s computer is compromised then browser security can still be bypassed. There have also been occurrences of session hijacking where (malicious) helpdesk employees asked for ‘har’ files (which are effectively complete request traces, including the authentication cookies), which were then used to hijack sessions. Right now, it’s very difficult to completely protect against this type of attack. However, there are interesting new standards being discussed, such as Device Bound Session Credentials. This standard aims to make sure that a session is cryptographically bound to a single device. Even if stolen, it can’t be used by a different device. ## See Also [Section titled “See Also”](#see-also) * [Duende IdentityServer](/identityserver/) — The authorization server BFF authenticates against for OpenID Connect flows * [Access Token Management](/accesstokenmanagement/) — Token lifecycle library used by BFF for automatic token refresh and caching ----- # Architecture > Overview of BFF host architecture, including authentication, session management, and integration with ASP.NET Core components A BFF host is an ASP.NET Core application that acts as a security proxy between the browser and your backend APIs. Understanding the key architectural decisions up front will save you significant rework later. New to BFF? If you haven’t yet decided whether to use BFF, start with the [overview](/bff/) which covers the threat model and the BFF-vs-token-in-browser comparison. ## How the BFF Fits Into Your System [Section titled “How the BFF Fits Into Your System”](#how-the-bff-fits-into-your-system) The following diagram shows how the BFF protects browser-based applications: ``` flowchart TD subgraph Browser SPA["Browser-Based Application"] CookieJar["🍪 Cookie Jar"] end subgraph BFF["BFF Host"] AuthEndpoints["Authentication
Endpoints"] SessionMgmt["Session
Management"] CookieAuth["Cookie Authorization"] CSRF["CSRF Protection"] Proxy["Proxy to
External APIs"] LocalAPIs["Local APIs"] SessionStore[("Server-Side
Session Storage")] end IdP["Identity Provider"] ExternalAPIs["External APIs"] SPA -->|"login / logout"| AuthEndpoints AuthEndpoints -->|"Set-Cookie"| CookieJar CookieJar -->|"Auth cookie"| CookieAuth AuthEndpoints <-->|"redirect"| IdP AuthEndpoints --> SessionMgmt SessionMgmt --> SessionStore CookieAuth --> CSRF CSRF --> Proxy CSRF --> LocalAPIs Proxy -->|"Bearer token"| ExternalAPIs SessionMgmt -->|"Acquire tokens"| IdP ExternalAPIs -->|"Validate tokens"| IdP ``` The BFF sits between the browser and everything else. The browser only ever holds a **session cookie** — it never sees tokens. The BFF exchanges that cookie for bearer tokens when forwarding requests to downstream APIs. ## Architectural Decisions [Section titled “Architectural Decisions”](#architectural-decisions) ### Decision 1: Where Does Your UI Live? [Section titled “Decision 1: Where Does Your UI Live?”](#decision-1-where-does-your-ui-live) The simplest setup hosts both the UI assets and the BFF from the **same origin**. This makes cookies same-site, eliminates CORS, and avoids [third-party cookie blocking](/bff/architecture/third-party-cookies/). You can also run the frontend on a **separate origin** (e.g. a Vite dev server, a CDN) and point it at the BFF via CORS. This is more complex but enables independent deployment. [UI Hosting](/bff/architecture/ui-hosting/)Full comparison of same-origin vs. separate-origin hosting ### Decision 2: Cookie-Only vs. Server-Side Sessions [Section titled “Decision 2: Cookie-Only vs. Server-Side Sessions”](#decision-2-cookie-only-vs-server-side-sessions) By default, the BFF stores the entire session in the cookie. This is simple and stateless but has limits: cookie size, no server-side revocation. With **server-side sessions**, the cookie holds only a session ID. The server stores the session state (typically in a database or distributed cache). This enables: * Forced logout across all sessions * Back-channel logout from the identity provider * Querying active sessions [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/)Configure server-side session storage for scalability and revocation ### Decision 3: How Do You Expose APIs? [Section titled “Decision 3: How Do You Expose APIs?”](#decision-3-how-do-you-expose-apis) | API Pattern | When to Use | | ----------------------- | ------------------------------------------------------------------------------------------------ | | **Local API** | Business logic hosted inside the BFF process itself. Lowest latency, no token forwarding needed. | | **Remote API (direct)** | External microservice. BFF forwards the request with a bearer token attached. | | **Remote API (YARP)** | External microservice with complex routing rules. BFF uses YARP as the reverse proxy. | [API Types](/bff/fundamentals/apis/)Decision flowchart for choosing the right API pattern ### Decision 4: Single Frontend vs. Multi-Frontend [Section titled “Decision 4: Single Frontend vs. Multi-Frontend”](#decision-4-single-frontend-vs-multi-frontend) Each BFF instance is tied to **one** browser-based application and **one** OIDC client registration. If you have multiple frontends (e.g. a customer portal and an admin app), run separate BFF instances with separate client IDs. They can share infrastructure (same process, different routes) but should not share session state or token storage. [Common Configurations](/bff/fundamentals/options/#common-configurations)Multi-frontend configuration example ### Decision 5: Blazor or JavaScript? [Section titled “Decision 5: Blazor or JavaScript?”](#decision-5-blazor-or-javascript) Both are supported, but have different integration patterns: * **JavaScript SPAs** interact with the BFF via `/bff/user`, `/bff/login`, `/bff/logout`, and API endpoints * **Blazor** uses built-in `AuthenticationStateProvider` integration and can call APIs server-side (no token forwarding from browser) [Blazor Fundamentals](/bff/fundamentals/blazor/)Blazor-specific guidance for rendering modes, data access, and auth state ## Trust Boundaries [Section titled “Trust Boundaries”](#trust-boundaries) ``` flowchart TD subgraph Browser["Browser (untrusted)"] B1["Holds session cookie only
(HttpOnly, Secure, SameSite)"] B2["Never sees access or refresh tokens"] end subgraph BFF["BFF Host (trusted server)"] BFF1["Validates session cookie on every request"] BFF2["Manages access/refresh tokens in server memory or DB"] BFF3["Enforces anti-forgery (X-CSRF header) on API routes"] end subgraph IdP["Identity Provider
(e.g. IdentityServer)"] end subgraph APIs["Downstream APIs
(microservices, external)"] end Browser -->|"HTTPS + Cookie"| BFF BFF -->|"OIDC/OAuth (HTTPS)"| IdP BFF -->|"Bearer token (HTTPS)"| APIs ``` The critical security property: **tokens never cross the trust boundary into the browser**. All token operations happen server-to-server. ## Internals [Section titled “Internals”](#internals) Duende.BFF is built on top of: | Component | Role | Details | | ---------------------------- | ------------------------------------------------------ | ---------------------------------------------------- | | ASP.NET OIDC handler | Protocol processing (auth code + PKCE, token exchange) | Standard ASP.NET middleware | | ASP.NET Cookie handler | Session management and cookie issuance | Extended by BFF for server-side sessions | | Duende.AccessTokenManagement | Token storage, refresh, revocation | [Docs](/accesstokenmanagement/) | | YARP | Reverse proxy for remote APIs | [BFF YARP integration](/bff/fundamentals/apis/yarp/) | ## See Also [Section titled “See Also”](#see-also) [IdentityServer Client Configuration](/identityserver/fundamentals/clients/)Register your BFF as a confidential OIDC client [Third-Party Cookies](/bff/architecture/third-party-cookies/)How browser cookie restrictions affect BFF architecture [UI Hosting](/bff/architecture/ui-hosting/)Options for hosting the frontend alongside the BFF [Middleware Pipeline](/bff/fundamentals/middleware-pipeline/)Canonical middleware order reference ----- # Multi-frontend support > Overview on what BFF multi-frontend support is, how it works and why you would use it. BFF V4.0 introduces the capability to support multiple BFF Frontends in a single host. This helps to simplify your application landscape by consolidating multiple physical BFF Hosts into a single deployable unit. A single BFF setup consists of: 1. A browser based application, typically built using technology like React, Angular or VueJS. This is typically deployed to a Content Delivery Network (CDN). 2. A BFF host, that will take care of the OpenID Connect login flows. 3. An API surface, exposed and protected by the BFF. With the BFF Multi-frontend support, you can logically host multiple of these BFF Setups in a single host. The concept of a single frontend (with OpenID Connect configuration, an API surface and a browser based app) is now codified inside the BFF. By using a flexible frontend selection mechanism (using Hosts or Paths to distinguish), it’s possible to create very flexible setups. The BFF dynamically configures the aspnet core authentication pipeline according to recommended practices. For example, when doing Host based routing, it will configure the cookies using the most secure settings and with the prefix [`__Host`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Reference/Headers/Set-Cookie). Frontends can be added or removed dynamically from the system, without having to restart the system. You can do this via configuration (for example by modifying a configuration file) or programmatically. Note The Duende BFF V4 library doesn’t ship with an abstraction to store or read frontends from a database. It’s possible to implement this by creating your own store (based on your requirements), then modify the `FrontendCollection` at run-time. ## A Typical Example [Section titled “A Typical Example”](#a-typical-example) Consider an enterprise that hosts multiple browser based applications. Each of these applications is developed by a separate team and as such, has its own deployment schedule. There are some internal-facing applications that are exclusively used by internal employees. These internal employees are all present in Microsoft Entra ID, so these internal-facing applications should directly authenticate against Microsoft Entra ID. These applications also use several internal APIs, that due to the sensitivity, should not be accessible by external users. However, they also use some of the more common APIs. These apps are only accessible via an internal DNS name, such as `https://app1.internal.example.com`. There are also several public facing applications, that are used directly by customers. These users should be able to log in using their own identity, via providers like Google, Twitter, or others. This authentication process is handled by Duende IdentityServer. There is constant development ongoing on these applications and it’s not uncommon for new applications to be introduced. There should be single sign-on across all these public facing applications. They are all available on the same domain name, but use path based routing to distinguish themselves, such as `https://app.example.com/app1` There is also a partner portal. This partner portal can only be accessed by employees of the partners. Each partner should be able to bring their own identity provider. This is implemented using the [Dynamic Providers](/identityserver/ui/login/dynamicproviders/) feature of Duende IdentityServer. This setup, with multiple frontends, each having different authentication requirements and different API surfaces, is now supported by the BFF. Each frontend can either rely on the global configuration or override (parts of) this configuration, such as the identity provider or the Client ID and Client Secret to use. It’s also possible to dynamically add or remove frontends, without restarting the BFF host. ## Internals [Section titled “Internals”](#internals) BFF V4 still allows you to manually configure the ASP.NET Core authentication options, by calling `.AddAuthentication().AddOpenIdConnect().AddCookies()`. However, if you wish to use the multi-frontend features, then this setup needs to become dynamic. To achieve this, the BFF automatically configures the ASP.NET Core pipeline: ``` --- title: BFF Middleware Pipeline --- flowchart TD A["FrontendSelectionMiddleware"] --> B["PathMappingMiddleware"] B --> C["OpenIdCallbackMiddleware"] C --> D["Your ASP.NET Core Pipeline"]:::app D --> E["MapRemoteRoutesMiddleware"] E --> F["ProxyIndexMiddleware"] ``` 1. `FrontendSelectionMiddleware` - This middleware performs the frontend selection by seeing which frontend’s selection criteria best matches the incoming request route. It’s possible to mix both path based routing and host based routing, so the most specific will be selected. 2. `PathMappingMiddleware` - If you use path mapping, in the selected frontend, then it will automatically map the frontend’s path so none of the subsequent middlewares know (or need to care) about this fact. 3. `OpenIdCallbackMiddleware` - To dynamically perform the OpenID Connect authentication without explicitly adding each frontend as a scheme, we inject a middleware that will handle the OpenID Connect callbacks. This only kicks in for dynamic frontends. 4. Your own applications logic is executed in this part of the pipeline. For example, calling `.UseAuthentication(), .UseRequestLogging()`, etc. After your application’s logic is executed, there are two middlewares registered as fallback routes: 5. `MapRemoteRoutesMiddleware` - This will handle any configured remote routes. Note, it will not handle plain YARP calls, only routes that are specifically added to a frontend. 6. `ProxyIndexMiddleware` - If configured, this proxies the `index.html` to start the browser based app. If you don’t want this automatic mapping of BFF middleware, you can turn it off using `BffOptions.AutomaticallyRegisterBffMiddleware`. When doing so, you’ll need to manually register and add the middlewares: ```csharp var app = builder.Build(); app.UseBffPreProcessing(); // TODO: your custom middleware goes here app.UseRouting(); app.UseBff(); app.UseBffPostProcessing(); app.Run(); ``` ## Authentication Architecture [Section titled “Authentication Architecture”](#authentication-architecture) When you use multiple frontends, you can’t rely on [manual authentication configuration](/bff/fundamentals/session/handlers/#manually-configuring-authentication). This is because each frontend requires its own scheme, and potentially its own OpenID Connect and Cookie configuration. The BFF registers a dynamic authentication scheme, which automatically configures the OpenID Connect and Cookie Scheme’s on behalf of the frontends. It does this using a custom `AuthenticationSchemeProvider` called `BffAuthenticationSchemeProvider` to return appropriate authentication schemes for each frontend. The BFF will register two schemes: * `duende-bff-oidc` * `duende-bff-cookie` Then, if there are no default authentication schemes registered, it will register ‘duende\_bff\_cookie’ schemes as the `AuthenticationOptions.DefaultScheme`, and ‘duende\_bff\_oidc’ as the `AuthenticationOptions.DefaultAuthenticateScheme` and `AuthenticationOptions.DefaultSignOutScheme`. This will ensure that calls to `Authenticate()` or `Signout()` will use the appropriate schemes. If you’re using multiple frontends, then the BFF will create dynamic schemes with the following signature: `duende_bff_oidc_[frontendname]` and `duende_bff_cookie_[frontendname]`. This ensures that every frontend can use its own OpenID Connect and Cookie settings. ----- # Third Party Cookies > Learn about the impact of third-party cookie blocking on OIDC flows and how the BFF pattern addresses these challenges If the BFF and OpenID Connect Provider (OP) are hosted on different [sites](https://developer.mozilla.org/en-US/docs/Glossary/Site), then some browsers will block cookies from being sent during navigation between those sites. Almost all browsers have the option of blocking third party cookies. Safari and Firefox are the most widely used browsers that do so by default, while Chrome is planning to do so in the future. This change is being made to protect user privacy, but it also impacts OIDC flows traditionally used by SPAs. A couple of particularly notable OIDC flows that don’t work for SPAs when third party cookies are blocked are [OIDC Session Management](https://openid.net/specs/openid-connect-session-1_0.html) and [OIDC Silent Login via the prompt=none parameter](https://openid.net/specs/openid-connect-core-1_0.html#authrequest). ## Session Management [Section titled “Session Management”](#session-management) OIDC Session Management allows a client SPA to monitor the session at the OP by reading a cookie from the OP in a hidden iframe. If third party cookie blocking prevents the iframe from seeing that cookie, the SPA will not be able to monitor the session. The BFF solves this problem using [OIDC back-channel logout](/bff/fundamentals/session/management/back-channel-logout/). The BFF is able to operate server side, and is therefore able to have a back channel to the OP. When the session ends at the OP, it can send a back-channel message to the BFF, ending the session at the BFF. ## Silent Login [Section titled “Silent Login”](#silent-login) OIDC Silent Login allows a client application to start its session without needing any user interaction if the OP has an ongoing session. The main benefit is that a SPA can load in the browser and then start a session without navigating away from the SPA for an OIDC flow, preventing the need to reload the SPA. Similarly to OIDC Session Management, OIDC Silent Login relies on a hidden iframe, though in this case, the hidden iframe makes requests to the OP, passing the *prompt=none* parameter to indicate that user interaction isn’t sensible. If that request includes the OP’s session cookie, the OP can respond successfully and the application can obtain tokens. But if the request does not include a session - either because no session has been started or because the cookie has been blocked - then the silent login will fail, and the user will have to be redirected to the OP for an interactive login. ### BFF With A Federation Gateway [Section titled “BFF With A Federation Gateway”](#bff-with-a-federation-gateway) The BFF supports silent login from the SPA with the /bff/silent-login [endpoint](/bff/fundamentals/session/management/silent-login/). This endpoint is intended to be invoked in an iframe and issues a challenge to login non-interactively with *prompt=none*. Just as in a traditional SPA, this technique will be disrupted by third party cookie blocking when the BFF and OP are third parties. If you need silent login with a third party OP, we recommend that you use the [Federation Gateway](/identityserver/ui/federation/) pattern. In the federation gateway pattern, one identity provider (the gateway) federates with other remote identity providers. Because the client applications only interact with the gateway, the implementation details of the remote identity providers are abstracted. In this case, we shield the client application from the fact that the remote identity provider is a third party by hosting the gateway as a first party to the client. This makes the client application’s requests for silent login always first party. ### Alternatives [Section titled “Alternatives”](#alternatives) Alternatively, you can accomplish a similar goal (logging in without needing to initially load the SPA, only to redirect away from it) by detecting that the user is not authenticated in the BFF and issuing a challenge before the site is ever loaded. This approach is not typically our first recommendation, because it makes allowing anonymous access to parts of the UI difficult and because it requires *samesite=lax* cookies (see below). ----- # UI Hosting > A guide exploring different UI hosting strategies and their benefits when using Backend For Frontend (BFF) systems When building modern web applications, selecting the right hosting strategy for your UI assets is crucial for optimizing performance, simplifying deployment, and ensuring seamless integration with Backend For Frontend (BFF) systems. This guide explores various hosting approaches and their benefits. ## Hosting Options for the UI [Section titled “Hosting Options for the UI”](#hosting-options-for-the-ui) There are several options for hosting the UI assets when using a BFF. * Host the assets within the BFF host using the static file middleware * Host the UI and BFF separately on subdomains of the same site and use CORS to allow cross-origin requests * Serves the index page of the UI from the BFF host, and all other assets are loaded from another domain, such as a CDN ### Serving SPA assets from BFF host [Section titled “Serving SPA assets from BFF host”](#serving-spa-assets-from-bff-host) Hosting the UI together with the BFF is the simplest choice, as requests from the front end to the backend will automatically include the authentication cookie and not require CORS headers. This makes the BFF and the front-end application a single deployable unit. Below shows a graphical overview of what that would look like: ``` flowchart LR subgraph Browser["Browser: https://application.url"] app["app"] end subgraph BFF["BFF Application"] endpoints["BFF endpoints
local / remote API endpoints"] static["Static files middleware"] end subgraph FS["Local Filesystem"] index["index.html"] scripts["script_assets.js"] images["images"] end app -->|"cookie"| endpoints app --> static static --> FS ``` If you create a BFF host using our templates, the UI will be hosted in this way: Terminal ```bash dotnet new duende-bff-remoteapi # or dotnet new duende-bff-localapi ``` Many frontend applications require a build process, which complicates the use of the static file middleware at development time. Visual Studio includes SPA templates that start up a SPA and proxy requests to it during development. Samples of Duende.BFF that take this approach using [React](/bff/samples#reactjs-frontend) and [Angular](/bff/samples#angular-frontend) are available. Microsoft’s templates are easy-to-use at dev time from Visual Studio. They allow you to run the solution, and the template proxies requests to the front end for you. At deploy time, that proxy is removed and the static assets of the site are served by the static file middleware. ### Host The UI Separately [Section titled “Host The UI Separately”](#host-the-ui-separately) You may want to host the UI outside the BFF. At development time, UI developers might prefer to run the frontend outside of Visual Studio (e.g., using the node cli). You might also want to have separate deployments of the frontend and the BFF, and you might want your static UI assets hosted on a CDN. Below is a schematic overview of what that would look like: ``` flowchart LR subgraph Browser["Browser: https://application.url"] app["app"] end subgraph BFF["BFF Application (https://bff.url)"] endpoints["BFF endpoints
local / remote API endpoints"] end subgraph CDN["CDN"] index["index.html"] scripts["script_assets.js"] images["images"] end app -->|"cookie + CORS"| endpoints app -->|"load assets"| CDN ``` The browser accesses the application via the BFF. The BFF proxies the calls to index.html to the CDN. The browser can then download all static assets from the CDN, but then use the BFF (and it’s API’s and user management API’s) secured by the authentication cookie as normal. Effectively, this turns your front-end and BFF Host into two separately deployable units. You’ll need to ensure that the two components are hosted on subdomains of the same domain so that [third party cookie blocking](/bff/architecture/third-party-cookies/) doesn’t prevent the frontend from including cookies in its requests to the BFF host. In order for this architecture to work, the following things are needed: * To make sure that client side routing works, there should be a catch-all route configured that proxies calls to the index.html. Once the index.html is served, the front-end will take over the application specific routing. * The API’s hosted by the BFF and the applications API’s should be excluded from this catch-all routing. However, they should not be visited by the browser directly. * The CDN needs to be configured to allow CORS requests from the application’s origin. * In order to include the auth cookie in those requests, the frontend code will have to [declare that it should send credentials](https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API/Using_Fetch#sending_a_request_with_credentials_included) using the *credentials: “include”* option. A sample of this approach is [available](/bff/samples#separate-host-for-ui). ### Serve The Index Page From The BFF Host [Section titled “Serve The Index Page From The BFF Host”](#serve-the-index-page-from-the-bff-host) Lastly, you could serve the index page of the SPA from the BFF, but have all the other static assets hosted on another host (presumably a CDN). This technique makes the UI and BFF have exactly the same origin, so the authentication cookie will be sent from the frontend to the BFF automatically, and third party cookie blocking and the SameSite cookie attribute won’t present any problems. The following diagram shows how that would work: ``` flowchart LR subgraph Browser["Browser: https://application.url"] app["app"] end subgraph BFF["BFF Application"] endpoints["BFF endpoints
local / remote API endpoints"] proxy["proxy"] end subgraph CDN["CDN (https://the.cdn)"] index["index.html"] scripts["script_assets.js"] images["images"] end app -->|"cookie"| endpoints app -->|"initial request"| proxy proxy -->|"proxy index.html"| CDN app -->|"load assets"| CDN ``` Setting this up for local development takes a bit of effort, however. As you make changes to the frontend, the UI’s build process might generate a change to the index page. If it does, you’ll need to arrange for the index page being served by the BFF host to reflect that change. Additionally, the front end will need to be configurable so that it is able to load its assets from other hosts. The mechanism for doing so will vary depending on the technology used to build the frontend. For instance, Angular includes a number of [deployment options](https://angular.io/guide/deployment) that allow you to control where it expects to find assets. The added complexity of this technique is justified when there is a requirement to host the front end on a different site (typically a CDN) from the BFF. Note BFF V4 has built-in support for proxying the index.html from a CDN. ----- # Diagnostics > Overview of Duende Backend for Frontend (BFF) diagnostic capabilities including logging and OpenTelemetry integration to assist with monitoring and troubleshooting ## Logging [Section titled “Logging”](#logging) Duende Backend for Frontend (BFF) offers several diagnostics possibilities. It uses the standard logging facilities provided by ASP.NET Core, so you don’t need to do any extra configuration to benefit from rich logging functionality, including support for multiple logging providers. See the Microsoft [documentation](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/logging) for a good introduction on logging. BFF follows the standard logging levels defined by the .NET logging framework, and uses the Microsoft guidelines for when certain log levels are used. For general information on how to configure logging in Duende products, see our [Logging Fundamentals](/general/logging/) guide. ### Configuration [Section titled “Configuration”](#configuration) Logs are typically written under the `Duende.Bff` category, with more concrete categories for specific components. To get detailed logs from the BFF middleware with the `Microsoft.Extensions.Logging` framework, you can configure your `appsettings.json` to enable `Debug` level logs for the `Duende.Bff` namespace: appsettings.json ```json { "Logging": { "LogLevel": { "Default": "Information", "Duende.Bff": "Debug" } } } ``` Multiple frontends When using [multiple frontends and the `FrontendSelectionMiddleware`](/bff/architecture/multi-frontend/), log messages are written in a log scope that contains a `frontend` property with the name of the frontend for which the log message was emitted. ## OpenTelemetry v4.0 [Section titled “OpenTelemetry ”v4.0](#opentelemetry) OpenTelemetry provides a single standard for collecting and exporting telemetry data, such as metrics, logs, and traces. To start emitting OpenTelemetry data in Duende Backend for Frontend (BFF), you need to: * add the OpenTelemetry libraries to your BFF host and client applications * start collecting traces and metrics from the various BFF sources (and other sources such as ASP.NET Core, the `HttpClient`, etc.) The following configuration adds the OpenTelemetry configuration to your service setup, and exports data to an [OTLP exporter](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel): Program.cs ```csharp var openTelemetry = builder.Services.AddOpenTelemetry(); openTelemetry.ConfigureResource(r => r .AddService(builder.Environment.ApplicationName)); openTelemetry.WithMetrics(metrics => { metrics.AddAspNetCoreInstrumentation() .AddHttpClientInstrumentation() .AddRuntimeInstrumentation() .AddMeter(BffMetrics.MeterName); }); openTelemetry.WithTracing(tracing => { tracing.AddSource(builder.Environment.ApplicationName) .AddAspNetCoreInstrumentation() // Uncomment the following line to enable gRPC instrumentation // (requires the OpenTelemetry.Instrumentation.GrpcNetClient package) //.AddGrpcClientInstrumentation() .AddHttpClientInstrumentation(); }); openTelemetry.UseOtlpExporter(); ``` ## Metrics [Section titled “Metrics”](#metrics) OpenTelemetry metrics are run-time measurements typically used to show graphs on a dashboard, to inspect overall application health, or to set up monitoring rules. The BFF host emits metrics collected through the `Duende.Bff` meter (meter name: `BffMetrics.MeterName`). Add it to your OpenTelemetry configuration with `.AddMeter(BffMetrics.MeterName)`. ### Session Metrics [Section titled “Session Metrics”](#session-metrics) | Metric Name | Type | Description | | ----------------- | ------- | --------------------------------------------------------------- | | `session.started` | Counter | Number of new sessions started (user logins) | | `session.ended` | Counter | Number of sessions ended (logouts, expiry, back-channel logout) | These counters can be used to track login/logout rates and detect unusual session activity (e.g., a spike in `session.ended` could indicate a back-channel logout sweep). ### Example: Prometheus Query [Section titled “Example: Prometheus Query”](#example-prometheus-query) If you are exporting metrics to Prometheus, the following PromQL queries can be useful: ```promql # Login rate over 5 minutes rate(session_started_total[5m]) # Logout rate over 5 minutes rate(session_ended_total[5m]) # Ratio of logouts to logins (high ratio may indicate session problems) rate(session_ended_total[5m]) / rate(session_started_total[5m]) ``` Note Metric names in Prometheus are automatically converted from dot notation (e.g., `session.started`) to underscores (e.g., `session_started_total`). ## Distributed Tracing [Section titled “Distributed Tracing”](#distributed-tracing) BFF participates in distributed tracing via ASP.NET Core’s standard `ActivitySource` integration. When you configure `AddAspNetCoreInstrumentation()` and `AddHttpClientInstrumentation()` in your OpenTelemetry setup, the following BFF operations will appear as spans in your traces: * Incoming requests to BFF management endpoints (`/bff/login`, `/bff/logout`, `/bff/user`, etc.) * Outgoing HTTP requests made by the BFF when proxying to remote APIs * Token refresh calls to the identity provider (via `Duende.AccessTokenManagement`) ### Complete OpenTelemetry Setup [Section titled “Complete OpenTelemetry Setup”](#complete-opentelemetry-setup) Program.cs ```csharp var openTelemetry = builder.Services.AddOpenTelemetry(); openTelemetry.ConfigureResource(r => r .AddService(builder.Environment.ApplicationName)); openTelemetry.WithMetrics(metrics => { metrics .AddAspNetCoreInstrumentation() // HTTP request metrics .AddHttpClientInstrumentation() // Outgoing HTTP call metrics .AddRuntimeInstrumentation() // .NET runtime metrics (GC, threadpool, etc.) .AddMeter(BffMetrics.MeterName); // BFF-specific session metrics }); openTelemetry.WithTracing(tracing => { tracing .AddSource(builder.Environment.ApplicationName) .AddAspNetCoreInstrumentation() // Trace incoming requests .AddHttpClientInstrumentation(); // Trace outgoing HTTP calls (token refresh, proxied API calls) }); // Export to an OTLP-compatible backend (Jaeger, Zipkin, Grafana Tempo, etc.) openTelemetry.UseOtlpExporter(); ``` ### Multi-Frontend Tracing [Section titled “Multi-Frontend Tracing”](#multi-frontend-tracing) When using multiple frontends, log messages and traces include a `frontend` scope property identifying which frontend the activity belongs to. This allows you to filter traces by frontend in your observability backend. ## Diagnostics Endpoint [Section titled “Diagnostics Endpoint”](#diagnostics-endpoint) The BFF also includes a `/bff/diagnostics` endpoint for development-time troubleshooting. It returns the current user and client access tokens. See [Diagnostics Endpoint](/bff/fundamentals/session/management/diagnostics/) for details. ## See Also [Section titled “See Also”](#see-also) [Logging Fundamentals](/general/logging/)General Duende logging configuration [OpenTelemetry .NET Documentation](https://learn.microsoft.com/en-us/dotnet/core/diagnostics/observability-with-otel)Setting up OTLP export [Troubleshooting](/bff/troubleshooting/)Common BFF issues and their solutions ----- # BFF Extensibility > Overview of all extensibility points in Duende.BFF for customizing session management, management endpoints, HTTP forwarding, and token storage. Duende.BFF is designed to be extended at multiple layers. Most production applications will use the defaults, but each area has well-defined extension points for when you need to go beyond the defaults. ## Extensibility Points [Section titled “Extensibility Points”](#extensibility-points) | Area | What You Can Customize | Detail Page | | ------------------------ | ----------------------------------------------------------------------------------- | ---------------------------------------------------- | | **Management Endpoints** | Login, logout, user info, back-channel logout, diagnostics, silent login processing | [Management Endpoints](#management-endpoints) | | **Session Store** | Where server-side session data is persisted (custom database, cache, etc.) | [Session Management](/bff/extensibility/sessions/) | | **HTTP Forwarder** | Custom HTTP clients, request/response transformations for proxied calls | [HTTP Forwarder](/bff/extensibility/http-forwarder/) | | **Token Management** | Token storage backend, per-route token retrieval (delegation, impersonation) | [Token Management](/bff/extensibility/tokens/) | ## Management Endpoints [Section titled “Management Endpoints”](#management-endpoints) Each BFF management endpoint has a corresponding interface that you can implement to customize its behavior. In v4, the pattern is to map a custom route at the same path and call the default endpoint implementation, allowing you to add logic before and after default processing. | Endpoint | Default Path | Interface (v4) | Interface (v3) | Detail | | ------------------- | ------------------- | ---------------------------- | --------------------------- | --------------------------------------------------------------------------------------- | | Login | `/bff/login` | `ILoginEndpoint` | `ILoginService` | [Login Extensibility](/bff/extensibility/management/login/) | | Logout | `/bff/logout` | `ILogoutEndpoint` | `ILogoutService` | [Logout Extensibility](/bff/extensibility/management/logout/) | | User | `/bff/user` | `IUserEndpoint` | `IUserService` | [User Extensibility](/bff/extensibility/management/user/) | | Silent Login | `/bff/silent-login` | `ISilentLoginEndpoint` | `ISilentLoginService` | [Silent Login Extensibility](/bff/extensibility/management/silent-login/) | | Back-Channel Logout | `/bff/backchannel` | `IBackchannelLogoutEndpoint` | `IBackchannelLogoutService` | [Back-Channel Logout Extensibility](/bff/extensibility/management/back-channel-logout/) | | Diagnostics | `/bff/diagnostics` | `IDiagnosticsEndpoint` | `IDiagnosticsService` | [Diagnostics Extensibility](/bff/extensibility/management/diagnostics/) | ### General Pattern (v4) [Section titled “General Pattern (v4)”](#general-pattern-v4) All management endpoint customizations in v4 follow the same pattern: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.LoginPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before the default processing var endpoint = context.RequestServices.GetRequiredService(); await endpoint.ProcessRequestAsync(context, ct); // Custom logic after the default processing }); ``` ## Session Store [Section titled “Session Store”](#session-store) By default, BFF uses either an in-memory store or Entity Framework Core for server-side sessions. To use a different storage backend (Redis, custom database, etc.), implement `IUserSessionStore`: ```csharp builder.Services.AddBff() .AddServerSideSessions(); ``` See [Session Management Extensibility](/bff/extensibility/sessions/) for the full interface and implementation guidance. ## HTTP Forwarder [Section titled “HTTP Forwarder”](#http-forwarder) When using `MapRemoteBffApiEndpoint`, BFF uses a default HTTP client and a default set of request/response transformations. You can customize: * **The HTTP client** — implement `IForwarderHttpClientFactory` to use a proxy, custom certificates, etc. * **Request/response transformations** — add custom headers, modify paths, or replace the default transformer entirely. See [HTTP Forwarder Extensibility](/bff/extensibility/http-forwarder/) for details. ## Token Management [Section titled “Token Management”](#token-management) BFF’s token management (powered by `Duende.AccessTokenManagement`) can be extended in two ways: * **Custom token store** — implement `IUserTokenStore` to store tokens outside of the session cookie or server-side session. * **Per-route token retrieval** — implement `IAccessTokenRetriever` for scenarios like token exchange or impersonation, where different API routes need different tokens. ```csharp app.MapRemoteBffApiEndpoint("/api/delegated", new Uri("https://api.example.com")) .WithAccessToken(RequiredTokenType.User) .WithAccessTokenRetriever(); ``` See [Token Management Extensibility](/bff/extensibility/tokens/) for details. ## See Also [Section titled “See Also”](#see-also) [Configuration Options](/bff/fundamentals/options/)Settings that control BFF behavior without custom code [Troubleshooting](/bff/troubleshooting/)Common issues and their solutions ----- # HTTP Forwarder > Learn how to customize the HTTP forwarding behavior in BFF by providing custom HTTP clients and request/response transformations You can customize the HTTP forwarder behavior in two ways * provide a customized HTTP client for outgoing calls * provide custom request/response transformation ## Custom HTTP Clients [Section titled “Custom HTTP Clients”](#custom-http-clients) By default, Duende.BFF will create and cache an HTTP client per configured route or local path. This invoker is set up like this: ```csharp var client = new HttpMessageInvoker(new SocketsHttpHandler { UseProxy = false, AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false }); ``` If you want to customize the HTTP client you can implement the `IForwarderHttpClientFactory` interface (from YARP’s `Yarp.ReverseProxy.Forwarder` namespace), e.g.: ```csharp public class MyInvokerFactory : IForwarderHttpClientFactory { public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) { return new HttpMessageInvoker(new SocketsHttpHandler { // this API needs a proxy UseProxy = true, Proxy = new WebProxy("https://myproxy"), AllowAutoRedirect = false, AutomaticDecompression = DecompressionMethods.None, UseCookies = false }); } } ``` …and override our registration: ```csharp services.AddSingleton(); ``` ## Custom Transformations When Using Direct Forwarding [Section titled “Custom Transformations When Using Direct Forwarding”](#custom-transformations-when-using-direct-forwarding) The method `MapRemoteBffApiEndpoint` uses default transformations that: * removes the cookie header from the forwarded request * removes local path from the forwarded request * adds the access token to the original request If you wish to change or extend this behavior, you can do this for a single mapped endpoint or for all mapped API endpoints. ### Changing The Transformer For A Single Mapped Endpoint [Section titled “Changing The Transformer For A Single Mapped Endpoint”](#changing-the-transformer-for-a-single-mapped-endpoint) This code block shows an example of how you can extend the default transformers with an additional custom transform. * Duende BFF v4 ```csharp app.MapRemoteBffApiEndpoint("/local", new Uri("https://target/"), context => { // If you want to extend the existing behavior, then you must call the default builder: DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken("/local", context); // You can also add custom transformers, such as this one that adds an additional header context.AddRequestHeader("custom", "with value"); }); ``` The default transform builder performs these transforms: ```csharp context.AddRequestHeaderRemove("Cookie"); context.AddPathRemovePrefix(pathMatch); context.AddBffAccessToken(pathMatch); ``` * Duende BFF v3 ```csharp app.MapRemoteBffApiEndpoint("/local", "https://target/", context => { // If you want to extend the existing behavior, then you must call the default builder: DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken("/local", context); // You can also add custom transformers, such as this one that adds an additional header context.AddRequestHeader("custom", "with value"); }); ``` The default transform builder performs these transforms: ```csharp context.AddRequestHeaderRemove("Cookie"); context.AddPathRemovePrefix(localPath); context.AddBffAccessToken(localPath); ``` For more information, also see the [YARP documentation on transforms](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/transforms?view=aspnetcore-9.0) ### Changing The Default Transformer [Section titled “Changing The Default Transformer”](#changing-the-default-transformer) You can change the default transformer builder delegate by registering one in the services collection: * Duende BFF v4 ```csharp BffYarpTransformBuilder builder = (pathMatch, context) => { // If you want to extend the existing behavior, then you must call the default builder: DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(pathMatch, context); // You can also add custom transformers, such as this one that adds an additional header context.AddResponseHeader("added-by-custom-default-transform", "some-value"); }; services.AddSingleton(builder); ``` * Duende BFF v3 ```csharp BffYarpTransformBuilder builder = (localPath, context) => { // If you want to extend the existing behavior, then you must call the default builder: DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(localPath, context); // You can also add custom transformers, such as this one that adds an additional header context.AddResponseHeader("added-by-custom-default-transform", "some-value"); }; services.AddSingleton(builder); ``` ## Changing The Forwarder Request Configuration [Section titled “Changing The Forwarder Request Configuration”](#changing-the-forwarder-request-configuration) Note Forwarder request configuration is available in Duende BFF v4 and later. You can also modify the forwarder request configuration, either globally or per mapped path. This can be useful if you want to tweak things like activity timeouts. ```csharp // Register a forwarder config globally: services.AddSingleton(new ForwarderRequestConfig() { ActivityTimeout = TimeSpan.FromMilliseconds(100) }); // Or modify one on a per mapped route basis: app.MapRemoteBffApiEndpoint("/local", new Uri("https://target/"), requestConfig: new ForwarderRequestConfig() { ActivityTimeout = TimeSpan.FromMilliseconds(100) }); ``` ----- # BFF Management Endpoints Extensibility The behavior of each [management endpoint](/bff/fundamentals/session/management) is defined in a service. When you add Duende.BFF to the service container, a default implementation for every management endpoint gets registered. You can add your own implementation by overriding the default after calling `AddBff()`. * V4 The following endpoints are registered in the service container: ```csharp // management endpoints builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); ``` The management endpoint services all inherit from the `IBffEndpoint`, which provides a general-purpose mechanism to add custom logic to the endpoints. IBffEndpoint.cs ```csharp public interface IBffEndpoint { Task ProcessRequestAsync(HttpContext context, CancellationToken ct = default); } ``` You can customize the behavior of the endpoints by implementing the appropriate interface. The [default implementations](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal) can serve as a starting point for your own implementation. If you want to extend the default behavior of a management endpoint, you can add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.LoginPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 ```csharp // management endpoints builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); builder.Services.AddTransient(); ``` The management endpoint services all inherit from the `IBffEndpointService`, which provides a general-purpose mechanism to add custom logic to the endpoints. IBffEndpointService.cs ```csharp public interface IBffEndpointService { Task ProcessRequestAsync(HttpContext context); } ``` You can customize the behavior of the endpoints either by implementing the appropriate interface or by extending the default implementation of that interface. In many cases, extending the default implementation is preferred, as this allows you to keep most of the default behavior by calling the base `ProcessRequestAsync` from your derived class. Several of the default endpoint service implementations also define virtual methods that can be overridden to customize their behavior with more granularity. ----- # BFF Back-Channel Logout Endpoint Extensibility The back-channel logout endpoint has several extensibility points organized into two interfaces. The `IBackchannelLogoutEndpoint` is the top-level abstraction that processes requests to the endpoint. This service can be used to add custom request processing logic or to change how it validates incoming requests. When the back-channel logout endpoint receives a valid request, it revokes sessions using the `ISessionRevocationService`. How this endpoint works See [Back-Channel Logout Endpoint](/bff/fundamentals/session/management/back-channel-logout/) for an explanation of how server-to-server logout works and how to configure it. Caution In BFF V3, the `IBackchannelLogoutEndpoint` interface is called `IBackchannelLogoutService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the back-channel logout endpoint by implementing the `ProcessRequestAsync` method of the `IBackchannelLogoutEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultBackchannelLogoutEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the back-channel logout endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.BackChannelLogoutPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultBackchannelLogoutService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ## Session Revocation [Section titled “Session Revocation”](#session-revocation) The back-channel logout service will call the registered session revocation service to revoke the user session when it receives a valid logout token. To customize the revocation process, implement the `ISessionRevocationService`. ----- # BFF Diagnostics Endpoint Extensibility The BFF diagnostics endpoint can be customized by implementing the `IDiagnosticsEndpoint`. How this endpoint works See [Diagnostics Endpoint](/bff/fundamentals/session/management/diagnostics/) for an explanation of what this endpoint provides and when it is enabled. Caution In BFF V3, the `IDiagnosticsEndpoint` interface is called `IDiagnosticsService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the diagnostics endpoint by implementing the `ProcessRequestAsync` method of the `IDiagnosticsEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultDiagnosticsEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the diagnostics endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.DiagnosticsPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultDiagnosticsService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ----- # BFF Login Endpoint Extensibility The BFF login endpoint has extensibility points in two interfaces. The `ILoginEndpoint` is the top-level abstraction that processes requests to the endpoint. This service can be used to add custom request processing logic. The `IReturnUrlValidator` ensures that the `returnUrl` parameter passed to the login endpoint is safe to use. How this endpoint works See [Login Endpoint](/bff/fundamentals/session/management/login/) for an explanation of what this endpoint does and how to use it from your frontend. Caution In BFF V3, the `ILoginEndpoint` interface is called `ILoginService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the login endpoint by implementing the `ProcessRequestAsync` method of the `ILoginEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultLoginEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the login endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.LoginPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultLoginService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ## Return URL Validation [Section titled “Return URL Validation”](#return-url-validation) To prevent open redirector attacks, the `returnUrl` parameter to the login endpoint must be validated. You can customize this validation by implementing the `IReturnUrlValidator` interface. The default implementation enforces that return URLs are local. ----- # BFF Logout Endpoint Extensibility The BFF logout endpoint has extensibility points in two interfaces. The `ILogoutEndpoint` is the top-level abstraction that processes requests to the endpoint. This service can be used to add custom request processing logic. The `IReturnUrlValidator` ensures that the `returnUrl` parameter passed to the logout endpoint is safe to use. How this endpoint works See [Logout Endpoint](/bff/fundamentals/session/management/logout/) for an explanation of what this endpoint does and how to use it from your frontend. Caution In BFF V3, the `ILogoutEndpoint` interface is called `ILogoutService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the logout endpoint by implementing the `ProcessRequestAsync` method of the `ILogoutEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultLogoutEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the logout endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.LogoutPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultLogoutService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ## Return URL Validation [Section titled “Return URL Validation”](#return-url-validation) To prevent open redirector attacks, the `returnUrl` parameter to the logout endpoint must be validated. You can customize this validation by implementing the `IReturnUrlValidator` interface. The default implementation enforces that return URLs are local. ----- # BFF Silent Login Endpoint Extensibility The BFF silent login endpoint can be customized by implementing the `ISilentLoginEndpoint`. How this endpoint works See [Silent Login Endpoint](/bff/fundamentals/session/management/silent-login/) for an explanation of the silent login flow and usage. Caution In BFF V3, the `ISilentLoginEndpoint` interface is called `ISilentLoginService` instead. Danger The silent login endpoint has been marked as obsolete in BFF V4 and will be removed in a future version. To handle silent login in the future, pass the `prompt=none` parameter on to the login endpoint instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the silent login endpoint by implementing the `ProcessRequestAsync` method of the `ISilentLoginEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultSilentLoginEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the silent login endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.SilentLoginPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultSilentLoginService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ----- # BFF Silent Login Callback Extensibility The BFF silent login callback endpoint can be customized by implementing the `ISilentLoginCallbackEndpoint`. Caution In BFF V3, the `ISilentLoginCallbackEndpoint` interface is called `ISilentLoginCallbackService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the silent login callback endpoint by implementing the `ProcessRequestAsync` method of the `ISilentLoginCallbackEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultSilentLoginCallbackEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the silent login callback endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.SilentLoginCallbackPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultSilentLoginCallbackService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ----- # BFF User Endpoint Extensibility The BFF user endpoint can be customized by implementing the `IUserEndpoint`. How this endpoint works See [User Endpoint](/bff/fundamentals/session/management/user/) for an explanation of what this endpoint returns and how to call it from your frontend. Caution In BFF V3, the `IUserEndpoint` interface is called `IUserService` instead. ## Request Processing [Section titled “Request Processing”](#request-processing) * V4 You can customize the behavior of the user endpoint by implementing the `ProcessRequestAsync` method of the `IUserEndpoint` interface. The [default implementation](https://github.com/DuendeSoftware/products/tree/releases/bff/4.0.x/bff/src/Bff/Endpoints/Internal/DefaultUserEndpoint.cs) can serve as a starting point for your own implementation. If you want to extend the default behavior of the user endpoint, you can instead add a custom endpoint and call the original endpoint implementation: Program.cs ```csharp var bffOptions = app.Services.GetRequiredService>().Value; app.MapGet(bffOptions.UserPath, async (HttpContext context, CancellationToken ct) => { // Custom logic before calling the original endpoint implementation var endpointProcessor = context.RequestServices.GetRequiredService(); await endpointProcessor.ProcessRequestAsync(context, ct); // Custom logic after calling the original endpoint implementation }); ``` * V3 `ProcessRequestAsync` is the top-level function called in the endpoint service `DefaultUserService`, and can be used to add arbitrary logic to the endpoint. For example, you could take whatever actions you need before normal processing of the request like this: ```csharp public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) { // Custom logic here return base.ProcessRequestAsync(context); } ``` ### Enriching User Claims [Section titled “Enriching User Claims”](#enriching-user-claims) There are several ways how you can enrich the claims for a specific user, depending on where the required data comes from. #### Claims Transformations [Section titled “Claims Transformations”](#claims-transformations) To enrich claims for a user, you can implement a custom `IClaimsTransformation`. Claims transformation executes as part of the authentication process. ```csharp services.AddScoped(); public class CustomClaimsTransformer : IClaimsTransformation { public Task TransformAsync(ClaimsPrincipal principal) { var identity = (ClaimsIdentity)principal.Identity; if (!identity.HasClaim(c => c.Type == "custom_claim")) { identity.AddClaim(new Claim("custom_claim", "your_value")); } return Task.FromResult(principal); } } ``` See the [Claims Transformation](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/claims?view=aspnetcore-9.0) topic in the ASP.NET Core documentation for more information. #### User Endpoint Claims Enricher v4.0 [Section titled “User Endpoint Claims Enricher ”v4.0](#user-endpoint-claims-enricher) User claims can be enriched by implementing the `IUserEndpointClaimsEnricher` interface. This interface is specific to the user endpoint and runs after authentication. Because this runs within the user endpoint request, you can access the current HTTP context to retrieve the user’s access token. We recommend using the [`GetUserAccessTokenAsync`](/accesstokenmanagement/web-apps/#http-context-extension-methods) extension method from `Duende.AccessTokenManagement.OpenIdConnect`, as it will automatically handle refreshing the token if it has expired. Program.cs ```csharp builder.Services.AddTransient(); ``` CustomUserEndpointClaimsEnricher.cs ```csharp using Duende.Bff; using Duende.Bff.Endpoints; using Duende.AccessTokenManagement.OpenIdConnect; using Microsoft.AspNetCore.Authentication; public class CustomUserEndpointClaimsEnricher : IUserEndpointClaimsEnricher { private readonly IHttpContextAccessor _httpContextAccessor; public CustomUserEndpointClaimsEnricher(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public async Task> EnrichClaimsAsync( AuthenticateResult authenticateResult, IReadOnlyList claims, CancellationToken ct = default) { var newClaims = claims.ToList(); // Get the access token using the extension method // This will automatically handle token refreshing if needed var token = await _httpContextAccessor.HttpContext.GetUserAccessTokenAsync(cancellationToken: ct); if (!string.IsNullOrEmpty(token.AccessToken)) { // Call external API using the access token // ... } // Add custom claims newClaims.Add(new ClaimRecord("custom_data", "some value")); return newClaims; } } ``` ----- # Session Management > Configure and implement custom server-side session storage and lifecycle management through IUserSessionStore interface Server-side sessions enable secure and efficient storage of session data, allowing flexibility through custom implementations of the `IUserSessionStore` interface. This ensures adaptability to various storage solutions tailored to your application’s needs. ## User Session Store [Section titled “User Session Store”](#user-session-store) If using the server-side sessions feature, you need a store for the session data. An Entity Framework Core based implementation of this store is provided. If you wish to use some other type of store, can implement the `IUserSessionStore` interface: * Duende BFF v4 ```csharp /// /// User session store /// public interface IUserSessionStore { /// /// Retrieves a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task GetUserSessionAsync(UserSessionKey key, CancellationToken ct = default); /// /// Creates a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task CreateUserSessionAsync(UserSession session, CancellationToken ct = default); /// /// Updates a user session /// /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task UpdateUserSessionAsync(UserSessionKey key, UserSessionUpdate session, CancellationToken ct = default); /// /// Deletes a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task DeleteUserSessionAsync(UserSessionKey key, CancellationToken ct = default); /// /// Queries user sessions based on the filter. /// /// The partition key to use /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task> GetUserSessionsAsync(PartitionKey partitionKey, UserSessionsFilter filter, CancellationToken ct = default); /// /// Deletes user sessions based on the filter. /// /// The partition key /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task DeleteUserSessionsAsync(PartitionKey partitionKey, UserSessionsFilter filter, CancellationToken ct = default); } ``` Do not store `UserSession` directly Your `IUserSessionStore` implementation is expected to implement custom code to roundtrip the data from the user session to the underlying storage mechanism. You should not rely on existing serializers, such as `System.Text.Json` or `Newtonsoft.Json`, to serialize the `UserSession` object. * Duende BFF v3 ```csharp /// /// User session store /// public interface IUserSessionStore { /// /// Retrieves a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task GetUserSessionAsync(string key, CancellationToken cancellationToken = default); /// /// Creates a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task CreateUserSessionAsync(UserSession session, CancellationToken cancellationToken = default); /// /// Updates a user session /// /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task UpdateUserSessionAsync(string key, UserSessionUpdate session, CancellationToken cancellationToken = default); /// /// Deletes a user session /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task DeleteUserSessionAsync(string key, CancellationToken cancellationToken = default); /// /// Queries user sessions based on the filter. /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task> GetUserSessionsAsync(UserSessionsFilter filter, CancellationToken cancellationToken = default); /// /// Deletes user sessions based on the filter. /// /// /// A token that can be used to request cancellation of the asynchronous operation. /// Task DeleteUserSessionsAsync(UserSessionsFilter filter, CancellationToken cancellationToken = default); } ``` Once you have an implementation, you can register it when you enable server-side sessions: Program.cs ```csharp builder.Services.AddBff() .AddServerSideSessions(); ``` ## User Session Store Cleanup [Section titled “User Session Store Cleanup”](#user-session-store-cleanup) The `IUserSessionStoreCleanup` interface is used to model cleaning up expired sessions. ```csharp /// /// User session store cleanup /// public interface IUserSessionStoreCleanup { /// /// Deletes expired sessions /// Task DeleteExpiredSessionsAsync(CancellationToken cancellationToken = default); } ``` ----- # Token Management > Learn how to customize token storage and management in the BFF framework, including HTTP client configuration and per-route token retrieval The token management library does essentially two things: * stores access and refresh tokens in the current session * refreshes access tokens automatically at the token service when needed Both aspects can be customized. ### Token service communication [Section titled “Token service communication”](#token-service-communication) The token management library uses a named HTTP client from the HTTP client factory for all token service communication. You can provide a customized HTTP client yourself using the well-known name after calling `AddBff`: ```csharp builder.Services.AddHttpClient( ClientCredentialsTokenManagementDefaults.BackChannelHttpClientName, configureClient => { // ... }); ``` Note You can also supply client assertions to the token management library. See this [sample](/bff/samples) for JWT-based client authentication. ### Custom Token Storage [Section titled “Custom Token Storage”](#custom-token-storage) We recommend that you use the default storage mechanism, as this will automatically be compatible with the Duende.BFF server-side sessions. If you do not use server-side sessions, then the access and refresh token will be stored in the protected session cookie. If you want to change this, you can take over token storage completely. This would involve two steps * turn off the `SaveTokens` flag on the OpenID Connect handler and handle the relevant events manually to store the tokens in your custom store * implement and register the `Duende.AccessTokenManagement.IUserTokenStore` interface The interface is responsible to storing, retrieving and clearing tokens for the automatic token management: ```csharp public interface IUserTokenStore { /// /// Stores tokens /// /// User the tokens belong to /// /// Extra optional parameters /// Task StoreTokenAsync( ClaimsPrincipal user, UserToken token, UserTokenRequestParameters? parameters = null, CancellationToken token = default); /// /// Retrieves tokens from store /// /// User the tokens belong to /// Extra optional parameters /// access and refresh token and access token expiration Task GetTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters? parameters = null, CancellationToken token = default); /// /// Clears the stored tokens for a given user /// /// User the tokens belong to /// Extra optional parameters /// Task ClearTokenAsync( ClaimsPrincipal user, UserTokenRequestParameters? parameters = null, CancellationToken token = default); } ``` ### Per-route Customized Token Retrieval [Section titled “Per-route Customized Token Retrieval”](#per-route-customized-token-retrieval) The token store defines how tokens are retrieved globally. However, you can add custom logic that changes the way that access tokens are retrieved on a per-route basis. For example, you might need to exchange a token to perform delegation or impersonation for some API calls, depending on the remote API. The interface that describes this extension point is the `IAccessTokenRetriever`. ```csharp /// /// Retrieves access tokens /// public interface IAccessTokenRetriever { /// /// Asynchronously gets the access token. /// /// Context used to retrieve the token. /// A task that contains the access token result, which is an /// object model that can represent various types of tokens (bearer, dpop), /// the absence of an optional token, or an error. Task GetAccessTokenAsync(AccessTokenRetrievalContext context, CancellationToken ct = default); } ``` You can implement this interface yourself or extend the `DefaultAccessTokenRetriever`. Note In Duende BFF v4, `DefaultAccessTokenRetriever` was made `internal`. If you need to customize token retrieval in v4, implement the `IAccessTokenRetriever` interface directly. The `AccessTokenResult` class represents the result of this operation. It is an abstract class with concrete implementations that represent successfully retrieving a bearer token (`BearerTokenResult`), successfully retrieving a DPoP token (`DPoPTokenResult`), failing to find an optional token (`NoAccessTokenResult`), which is not an error, and failure to retrieve a token (`AccessTokenRetrievalError`). Your implementation of GetAccessTokenAsync should return one of those types. Implementations of the `IAccessTokenRetriever` can be added to endpoints when they are mapped using the `WithAccessTokenRetriever` extension method: ```csharp app.MapRemoteBffApiEndpoint( "/api/impersonation", new Uri("https://api.example.com/endpoint/requiring/impersonation") ).WithAccessToken(RequiredTokenType.User) .WithAccessTokenRetriever(); ``` Custom access token retrievers can also be used with [YARP routes and clusters](/bff/fundamentals/apis/yarp/#custom-access-token-retriever). Use the `WithAccessTokenRetriever()` extension method on a `RouteConfig` or `ClusterConfig`, or set the `Duende.Bff.Yarp.AccessTokenRetriever` metadata key in JSON configuration: ```csharp // YARP route with custom retriever new RouteConfig() { RouteId = "impersonation", ClusterId = "cluster1", Match = new RouteMatch { Path = "/api/impersonation/{**catch-all}" } }.WithAccessToken(RequiredTokenType.User) .WithAccessTokenRetriever() ``` The `GetAccessTokenAsync` method will be invoked on every call to APIs that use the access token retriever. If retrieving the token is an expensive operation, you may need to cache it. It is up to your retriever code to perform caching. ----- # Securing and Accessing API Endpoints > Learn about the different types of APIs in a BFF architecture and how to secure and access them properly A frontend application using the BFF pattern can call two types of APIs: embedded (local) APIs, and proxied remote APIs. ## Choosing an API Approach [Section titled “Choosing an API Approach”](#choosing-an-api-approach) ``` flowchart TD Q1{"Is the API only used
by this frontend?"} Q2{"Do you need load balancing,
service discovery, or
complex routing/transforms?"} Local["✅ Embedded (Local) API
Host the API inside the BFF itself"] Remote["✅ Remote API — Direct Forwarding
MapRemoteBffApiEndpoint()"] Yarp["✅ YARP Integration
Full YARP configuration with BFF extensions"] Q1 -->|Yes| Local Q1 -->|No| Q2 Q2 -->|Yes| Yarp Q2 -->|No| Remote ``` Use the table below for additional guidance on token requirements: | Scenario | Recommended approach | | ----------------------------------------------------------- | -------------------------------------------------- | | API is only used by this frontend | [Embedded (Local) API](local/) | | API is shared by multiple clients or deployed separately | [Remote API — Direct Forwarding](remote/) | | Complex routing, load balancing, or transforms are needed | [YARP](yarp/) | | API requires the logged-in user’s token | Remote or YARP with `RequiredTokenType.User` | | API uses machine-to-machine (client credentials) auth | Remote or YARP with `RequiredTokenType.Client` | | API is publicly accessible (no auth required) | Remote with `RequiredTokenType.None` | | API should use user token if logged in, anonymous otherwise | Remote or YARP with `RequiredTokenType.UserOrNone` | Start with local APIs when in doubt If the API only serves this one frontend and doesn’t need to be independently deployed or versioned, embed it directly in the BFF host as a local API. It’s the simplest approach and benefits from full CSRF protection with minimal configuration. ## Embedded (Local) APIs [Section titled “Embedded (Local) APIs”](#embedded-local-apis) These APIs are embedded inside the BFF and typically exist to support the BFF’s frontend; they are not shared with other frontends or services. See [Embedded APIs](local/) for more information. ## Proxying Remote APIs [Section titled “Proxying Remote APIs”](#proxying-remote-apis) These APIs are deployed on a different host than the BFF, which allows them to be shared between multiple frontends or (more generally speaking) multiple clients. These APIs can only be called via the BFF host acting as a proxy. You can use [Direct Forwarding](remote/) for most scenarios. If you have more complex requirements, you can also directly interact with [YARP](yarp/). ## See Also [Section titled “See Also”](#see-also) * [Token Management](/bff/fundamentals/tokens/) — How BFF attaches access tokens to outgoing API calls * [Access Token Management](/accesstokenmanagement/) — The underlying token lifecycle library * [IdentityServer API Resources](/identityserver/fundamentals/resources/api-resources/) — Configuring scopes for your APIs ----- # Embedded (Local) APIs > Documentation about Embedded (Local) APIs in BFF, including self-contained APIs and those using managed access tokens, along with securing endpoints and configuration details. An *Embedded API* (or local API) is an API located within the BFF host. Embedded APIs are implemented with the familiar ASP.NET abstractions of API controllers or Minimal API endpoints. There are two styles of Embedded APIs: * Self-contained Embedded APIs * Embedded APIs that Make Requests using Managed Access Tokens #### Self-Contained Embedded APIs [Section titled “Self-Contained Embedded APIs”](#self-contained-embedded-apis) These APIs reside within the BFF and don’t make HTTP requests to other APIs. They access data controlled by the BFF itself, which can simplify the architecture of the system by reducing the number of APIs that must be deployed and managed. They are suitable for scenarios where the BFF is the sole consumer of the data. If you require data accessibility from other applications or services, this approach is probably not suitable. #### Embedded APIs That Make Requests Using Managed Access Tokens [Section titled “Embedded APIs That Make Requests Using Managed Access Tokens”](#embedded-apis-that-make-requests-using-managed-access-tokens) Alternatively, you can make the data available as a service and make HTTP requests to that service from your BFF’s Embedded endpoints. The benefits of this style of Embedded Endpoint include: * Your frontend’s network access can be simplified into an aggregated call for the specific data that it needs, which reduces the amount of data that must be sent to the client. * Your BFF endpoint can expose a subset of your remote APIs so that they are called in a more controlled manner than if the BFF proxied all requests to the endpoint. * Your BFF endpoint can include business logic to call the appropriate endpoints, which simplifies your front end code. Your Embedded endpoints can leverage services like the HTTP client factory and Duende.BFF [token management](/bff/fundamentals/tokens/) to make the outgoing calls. The following is a simplified example showing how Embedded endpoints can get managed access tokens and use them to make requests to remote APIs. Program.cs ```csharp app.MapGet("/myApi", async (IHttpClientFactory httpClientFactory, HttpContext context) => { var id = context.Request.Query["id"]; // create HTTP client var client = httpClientFactory.CreateClient(); // get current user access token and set it on HttpClient var token = await context.GetUserAccessTokenAsync(); client.SetBearerToken(token); // call remote API var response = await client.GetAsync($"https://remoteServer/remoteApi?id={id}"); // maybe process response and return to frontend return Results.Text(await response.Content.ReadAsStringAsync()); }); ``` The example above is simplified to demonstrate the way that you might obtain a token. Embedded endpoints will typically enforce constraints on the way the API is called, aggregate multiple calls, or perform other business logic. Embedded endpoints that merely forward requests from the frontend to the remote API may not be needed at all. Instead, you could proxy the requests through the BFF using either the [simple http forwarder](/bff/fundamentals/apis/remote/) or [YARP](/bff/fundamentals/apis/yarp/). ## Securing Embedded API Endpoints [Section titled “Securing Embedded API Endpoints”](#securing-embedded-api-endpoints) Regardless of the style of data access used by an Embedded API, it must be protected against threats such as [CSRF (Cross-Site Request Forgery)](https://developer.mozilla.org/en-US/docs/Glossary/CSRF) attacks. To defend against such attacks and ensure that only the frontend can access these endpoints, we recommend implementing two layers of protection. #### SameSite Cookies [Section titled “SameSite Cookies”](#samesite-cookies) [The SameSite cookie attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value) is a feature of modern browsers that restricts cookies so that they are only sent to pages originating from the [site](https://developer.mozilla.org/en-US/docs/Glossary/Site) where the cookie was originally issued. This is a good first layer of defense, but makes the assumption that you can trust all subdomains of your site. All subdomains within a registrable domain are considered the same site for purposes of SameSite cookies. Thus, if another application hosted on a subdomain within your site is infected with malware, it can make CSRF attacks against your application. #### Anti-forgery Header [Section titled “Anti-forgery Header”](#anti-forgery-header) We recommend requiring an additional custom header on API endpoints, for example: ```plaintext GET /endpoint x-csrf: 1 ``` The value of the header is not important, but its presence, combined with the cookie requirement, triggers a CORS preflight request for cross-origin calls. This effectively isolates the caller to the same origin as the backend, providing a robust security guarantee. Additionally, API endpoints should handle scenarios where the session has expired or authorization fails without triggering an authentication redirect to the upstream identity provider. Instead, they should return Ajax-friendly status codes. ## Setup [Section titled “Setup”](#setup) ### Adding Anti-forgery Protection [Section titled “Adding Anti-forgery Protection”](#adding-anti-forgery-protection) Duende.BFF can automate the pre-processing step of requiring the custom anti-forgery header. To do so, first add the BFF middleware to the pipeline, and then decorate your endpoints to indicate that they should receive BFF pre-processing. 1. **Add Middleware to the pipeline** Add the BFF middleware to the pipeline by calling `UseBff`. Note that the middleware must be placed before the authorization middleware, but after routing. Program.cs ```csharp app.UseAuthentication(); app.UseRouting(); app.UseBff(); app.UseAuthorization(); // map endpoints ``` 2. **Decorate Endpoints** Endpoints that require the pre- and post-processing described above must be decorated with a call to `AsBffApiEndpoint()`. For Minimal API endpoints, you can apply BFF pre- and post-processing when they are mapped. ```csharp app.MapPost("/foo", context => { // ... }) .RequireAuthorization() // no anonymous access .AsBffApiEndpoint(); // BFF pre/post processing ``` For MVC controllers, you can similarly apply BFF pre- and post-processing to controller actions when they are mapped. ```csharp app.MapControllers() .RequireAuthorization() // no anonymous access .AsBffApiEndpoint(); // BFF pre/post processing ``` Alternatively, you can apply the `[BffApi]` attribute directly to the controller or action. ```csharp [Route("myApi")] [BffApi] public class MyApiController : ControllerBase { // ... } ``` ### Disabling Anti-forgery Protection [Section titled “Disabling Anti-forgery Protection”](#disabling-anti-forgery-protection) Disabling anti-forgery protection is possible but not recommended. Antiforgery protection defends against CSRF attacks, so opting out may cause security vulnerabilities. However, if you are defending against CSRF attacks with some other mechanism, you can opt out of Duende.BFF’s CSRF protection. Depending on the version of Duende.BFF, use one of the following approaches. For *version 1.x*, set the `requireAntiForgeryCheck` parameter to `false` when adding the endpoint. For example: Program.cs ```csharp // MVC controllers app.MapControllers() .RequireAuthorization() // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks .AsBffApiEndpoint(requireAntiforgeryCheck: false); // simple endpoint app.MapPost("/foo", context => { // ... }) .RequireAuthorization() // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks .AsBffApiEndpoint(requireAntiforgeryCheck: false); ``` On MVC controllers and actions you can set the `RequireAntiForgeryCheck` as a flag in the `BffApiAttribute`, like this: ```csharp [Route("sample")] // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks [BffApi(requireAntiForgeryCheck: false)] public class SampleApiController : ControllerBase { /* ... */ } ``` In *version 2.x and 3.x*, use the `SkipAntiforgery` fluent API when adding the endpoint. For example: Program.cs ```csharp // MVC controllers app.MapControllers() .RequireAuthorization() .AsBffApiEndpoint() // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks .SkipAntiforgery(); // simple endpoint app.MapPost("/foo", context => { /* ... */ }) .RequireAuthorization() .AsBffApiEndpoint() // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks .SkipAntiforgery(); ``` MVC controllers and actions can use the `BffApiSkipAntiforgeryAttribute` (which is independent of the `BffApiAttribute`), like this: ```csharp [Route("sample")] // WARNING: Disabling antiforgery protection may make // your APIs vulnerable to CSRF attacks [BffApiSkipAntiforgeryAttribute] public class SampleApiController : ControllerBase { /* ... */ } ``` Note It’s also possible to disable anti-forgery protection using *BffOptions.DisableAntiForgeryCheck()* ### Skipping Response Handling [Section titled “Skipping Response Handling”](#skipping-response-handling) By default, when BFF pre/post-processing is enabled on an endpoint (via `.AsBffApiEndpoint()`), the BFF framework intercepts authentication challenge and forbid responses. Instead of returning a 302 redirect to the identity provider (which is not useful for API calls), it converts them to API-friendly status codes: * **Challenge** (unauthenticated) → returns **401** (instead of 302 redirect) * **Forbid** (unauthorized) → returns **403** (instead of 302 redirect) If you want to opt out of this behavior and let the default authentication response handling occur (e.g., if your endpoint needs to trigger a redirect), you can use the `SkipResponseHandling()` extension method: ```csharp app.MapGet("/my-endpoint", context => { /* ... */ }) .AsBffApiEndpoint() .SkipResponseHandling(); ``` For MVC controllers, you can use the `[BffApiSkipResponseHandling]` attribute: ```csharp [Route("my-endpoint")] [BffApi] [BffApiSkipResponseHandling] public class MyController : ControllerBase { /* ... */ } ``` ----- # Proxying Remote APIs > Learn how to configure and secure remote API access through BFF using HTTP forwarding and token management. Note You will need to have the [`Duende.Bff.Yarp`](https://www.nuget.org/packages/Duende.BFF.Yarp) NuGet package installed to use these features. A *Remote API* is an API that is deployed separately from the BFF host. Remote APIs use access tokens to authenticate and authorize requests, but the frontend does not possess an access token to make requests to remote APIs directly. Instead, all access to remote APIs is proxied through the BFF, which authenticates the frontend using its authentication cookie, gets the appropriate access token, and forwards the request to the Remote API with the token attached. There are two different ways to set up Remote API proxying in Duende.BFF. This page describes the built-in simple HTTP forwarder. Alternatively, you can integrate Duende.BFF with Microsoft’s [YARP](/bff/fundamentals/apis/yarp/) reverse proxy, which allows for more complex reverse proxy features provided by YARP combined with the security and identity features of Duende.BFF. ## Direct HTTP Forwarding [Section titled “Direct HTTP Forwarding”](#direct-http-forwarding) Duende.BFF’s direct HTTP forwarder maps routes in the BFF to a remote API surface. It uses [Microsoft YARP](https://github.com/microsoft/reverse-proxy) internally, but is much simpler to configure than YARP. The intent is to provide a developer-centric and simplified way to proxy requests from the BFF to remote APIs when more complex reverse proxy features are not needed. These routes receive automatic anti-forgery protection and integrate with automatic token management. To enable this feature, add a reference to the [`Duende.BFF.Yarp` NuGet package](https://www.nuget.org/packages/Duende.BFF.Yarp), add the remote APIs service to the service provider, and then add the remote endpoint mappings. Note The BFF multi-frontend feature has built-in support for direct forwarding. #### Add Remote API Service to Service Provider [Section titled “Add Remote API Service to Service Provider”](#add-remote-api-service-to-service-provider) To use the HTTP forwarder, register it in the service provider: Program.cs ```csharp builder.Services.AddBff() .AddRemoteApis(); ``` #### Map Remote APIs [Section titled “Map Remote APIs”](#map-remote-apis) Use the `MapRemoteBffApiEndpoint` extension method to describe how to map requests coming into the BFF to remote APIs. `MapRemoteBffApiEndpoint` takes two parameters: the base path of requests that will be mapped externally, and the address to the external API where the requests will be mapped. The `MapRemoteBffApiEndpoint` extension method maps a path and all sub-paths below it. The intent is to allow easy mapping of groups of URLs. For example, you can set up mappings for the `/users`, `/users/{userId}`, `/users/{userId}/books`, and `/users/{userId}/books/{bookId}` endpoints without having to explicitly include all of them: * V4 Program.cs ```csharp app.MapRemoteBffApiEndpoint("/api/users", new Uri("https://remoteHost/users")) .WithAccessToken(RequiredTokenType.User); ``` * V3 Program.cs ```csharp app.MapRemoteBffApiEndpoint("/api/users", new Uri("https://remoteHost/users")) .WithAccessToken(TokenType.User); ``` Note This example opens up the complete */users* API namespace to the frontend, and thus, to the outside world. While it is convenient to register API paths this way, consider if you need to be more specific when designing the forwarding paths to prevent accidentally exposing unintended endpoints. The `WithAccessToken` method can be added to [specify token requirements](#access-token-requirements) for the remote API. The BFF will automatically forward the correct access token to the remote API, which will be scoped to the client application, the user, or either. ## Securing Remote APIs [Section titled “Securing Remote APIs”](#securing-remote-apis) Remote APIs typically require access control and must be protected against threats such as [CSRF (Cross-Site Request Forgery)](https://developer.mozilla.org/en-US/docs/Glossary/CSRF) attacks. To provide access control, you can specify authorization policies on the mapped routes and configure them with access token requirements. To defend against CSRF attacks, you should use SameSite cookies to authenticate calls from the frontend to the BFF. As an additional layer of defense, APIs mapped with `MapRemoteBffApiEndpoint` are automatically protected with an anti-forgery header. #### SameSite cookies [Section titled “SameSite cookies”](#samesite-cookies) [The SameSite cookie attribute](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value) is a feature of modern browsers that restricts cookies so that they are only sent to pages originating from the [site](https://developer.mozilla.org/en-US/docs/Glossary/Site) where the cookie was originally issued. This prevents CSRF attacks, because cross site requests will no longer implicitly include the user’s credentials. This is a good first layer of defense but makes the assumption that you can trust all subdomains of your site. All subdomains within a registrable domain are considered the same site for purposes of SameSite cookies. Thus, if another application hosted on a subdomain within your site is infected with malware, it can make CSRF attacks against your application. #### Anti-forgery header [Section titled “Anti-forgery header”](#anti-forgery-header) Remote APIs mapped in the BFF always require an additional custom header on API endpoints. For example: ```text GET /endpoint x-csrf: 1 ``` The value of the header is not important, but its presence, combined with the cookie requirement, triggers a CORS preflight request for cross-origin calls. This effectively isolates the caller to the same origin as the backend, providing a robust security guarantee. #### Require authorization [Section titled “Require authorization”](#require-authorization) The `MapRemoteBffApiEndpoint` method returns the appropriate type to integrate with the ASP.NET Core authorization system. You can attach authorization policies to remote endpoints using the `WithAccessToken` extension method, just as you would for a standard ASP.NET core endpoint created with `MapGet`. The authorization middleware will then enforce that policy before forwarding requests on that route to the remote endpoint. Note In Duende.BFF version 3, use the `MapRemoteBffApiEndpoint` method with the `RequireAuthorization` extension method to attach authorization policies. #### Access token requirements [Section titled “Access token requirements”](#access-token-requirements) Remote APIs sometimes allow anonymous access but usually require an access token, and the type of access token (user or client) will vary as well. You can specify access token requirements via the `WithAccessToken` extension method. Its `RequiredTokenType` parameter has five options: * **`None`** No token is required. * **`User`** A valid user access token is required and will be forwarded to the remote API. A user access token is an access token obtained during an OIDC flow (or subsequent refresh), and is associated with a particular user. User tokens are obtained when the user initially logs in, and will be automatically refreshed using a refresh token when they expire. * **`Client`** A valid client access token is required and will be forwarded to the remote API. A client access token is an access token obtained through the client credentials flow, and is associated with the client application, not any particular user. Client tokens can be obtained even if the user is not logged in. * **`UserOrClient`** Either a valid user access token or a valid client access token (as fallback) is required and will be forwarded to the remote API. * **`UserOrNone`** A valid user access token will be forwarded to the remote API when logged in. No access token will be sent when not logged in, and no OIDC flow is challenged to get an access token. Note These settings only specify the logic that is applied before the API call gets proxied. The remote APIs you are calling should always specify their own authorization and token requirements. ----- # YARP extensions > Integration of Duende.BFF with Microsoft's YARP reverse proxy, including token management and anti-forgery protection features. Duende.BFF integrates with Microsoft’s full-featured reverse proxy [YARP](https://microsoft.github.io/reverse-proxy/). YARP includes many advanced features such as load balancing, service discovery, and session affinity. It also has its own extensibility mechanism. Duende.BFF includes YARP extensions for token management and anti-forgery protection so that you can combine the security and identity features of `Duende.BFF` with the flexible reverse proxy features of YARP. ## Adding YARP [Section titled “Adding YARP”](#adding-yarp) To enable Duende.BFF’s YARP integration, add a reference to the *Duende.BFF.Yarp* NuGet package to your project and add YARP and the BFF’s YARP extensions to DI: ```csharp builder.Services.AddBff(); // adds YARP with BFF extensions var yarpBuilder = services.AddReverseProxy() .AddBffExtensions(); ``` ## Configuring YARP [Section titled “Configuring YARP”](#configuring-yarp) YARP is most commonly configured by a config file. The following simple example forwards a local URL to a remote API: ```json { "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" } } }, "Clusters": { "cluster1": { "Destinations": { "destination1": { "Address": "https://API.mycompany.com/todos" } } } } } } ``` See the Microsoft [documentation](https://microsoft.github.io/reverse-proxy/articles/config-files.html) for the complete configuration schema. Another option is to configure YARP in code using the in-memory config provider included in the BFF extensions for YARP. The above configuration as code would look like this: ```csharp yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1", Match = new() { Path = "/todos/{**catch-all}" } } }, new[] { new ClusterConfig { ClusterId = "cluster1", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination1", new() { Address = "https://API.mycompany.com/todos" } }, } } }); ``` ## Token Management [Section titled “Token Management”](#token-management) Duende.BFF’s YARP extensions provide access token management and attach user or client access tokens automatically to proxied API calls. To enable this, add metadata with the name *Duende.Bff.Yarp.TokenType* to the route or cluster configuration: ```json { "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" }, "Metadata": { "Duende.Bff.Yarp.TokenType": "User" } } } } } ``` Similarly to the [simple HTTP forwarder](/bff/fundamentals/apis/remote/#access-token-requirements), the allowed values for the token type are `None`, `User`, `Client`, `UserOrClient`, and `UserOrNone`. Routes that set the `Duende.Bff.Yarp.TokenType` metadata **require** the given type of access token. If it is unavailable (for example, if the `User` token type is specified but the request to the BFF is anonymous), then the proxied request will not be sent, and the BFF will return an HTTP 401: Unauthorized response. If you are using the code config method, call the `WithAccessToken` extension method to achieve the same thing: ```csharp yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1", Match = new RouteMatch { Path = "/todos/{**catch-all}" } }.WithAccessToken(RequiredTokenType.User) }, // rest omitted ); ``` Again, the `WithAccessToken` method causes the route to require the given type of access token. If it is unavailable, the proxied request will not be made and the BFF will return an HTTP 401: Unauthorized response. ## Optional User Access Tokens [Section titled “Optional User Access Tokens”](#optional-user-access-tokens) You can attach user access tokens optionally using the `UserOrNone` token type. This causes the user’s access token to be sent with the proxied request when the user is logged in, but makes the request anonymously when the user is not logged in. In configuration, set the `Duende.Bff.Yarp.TokenType` metadata to `UserOrNone`: ```json { "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" }, "Metadata": { "Duende.Bff.Yarp.TokenType": "UserOrNone" } } } } } ``` If you are using the code config method, call the `WithAccessToken` extension method with `RequiredTokenType.UserOrNone`: ```csharp yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1", Match = new RouteMatch { Path = "/todos/{**catch-all}" } }.WithAccessToken(RequiredTokenType.UserOrNone) }, // rest omitted ); ``` ### Anti-forgery Protection [Section titled “Anti-forgery Protection”](#anti-forgery-protection) Duende.BFF’s YARP extensions can also add anti-forgery protection to proxied API calls. Anti-forgery protection defends against CSRF attacks by requiring a custom header on API endpoints, for example: ```plaintext GET /endpoint x-csrf: 1 ``` The value of the header is not important, but its presence, combined with the cookie requirement, triggers a CORS preflight request for cross-origin calls. This effectively isolates the caller to the same origin as the backend, providing a robust security guarantee. You can add the anti-forgery protection to all YARP routes by calling the `AsBffApiEndpoint` extension method: ```csharp app.MapReverseProxy() .AsBffApiEndpoint(); // or shorter app.MapBffReverseProxy(); ``` If you need more fine-grained control over which routes should enforce the anti-forgery header, you can also annotate the route configuration by adding the `Duende.Bff.Yarp.AntiforgeryCheck` metadata to the route config: ```json { "ReverseProxy": { "Routes": { "todos": { "ClusterId": "cluster1", "Match": { "Path": "/todos/{**catch-all}" }, "Metadata": { "Duende.Bff.Yarp.AntiforgeryCheck": "true" } } } } } ``` This is also possible in code: ```csharp yarpBuilder.LoadFromMemory( new[] { new RouteConfig() { RouteId = "todos", ClusterId = "cluster1", Match = new RouteMatch { Path = "/todos/{**catch-all}" } }.WithAntiforgeryCheck() }, // rest omitted ); ``` Note You can combine the token management feature with the anti-forgery check. To enforce the presence of the anti-forgery headers, you need to add a middleware to the YARP pipeline: Program.cs ```csharp app.MapReverseProxy(proxyApp => { proxyApp.UseAntiforgeryCheck(); }); ``` ## Custom Access Token Retriever [Section titled “Custom Access Token Retriever”](#custom-access-token-retriever) You can specify a custom [`IAccessTokenRetriever`](/bff/extensibility/tokens/#per-route-customized-token-retrieval) on YARP routes and clusters. This allows you to customize how access tokens are obtained for proxied requests — for example, to perform token exchange for delegation or impersonation scenarios. ### Code Configuration [Section titled “Code Configuration”](#code-configuration) A custom retriever can be set at the **route level** or the **cluster level**. Route-level retrievers take precedence over cluster-level retrievers. Use the `WithAccessTokenRetriever()` extension method on a `RouteConfig`: ```csharp // Route-level retriever new RouteConfig() { RouteId = "impersonation", ClusterId = "cluster1", Match = new RouteMatch { Path = "/api/impersonation/{**catch-all}" } }.WithAccessToken(RequiredTokenType.User) .WithAccessTokenRetriever() .WithAntiforgeryCheck() ``` Or `ClusterConfig`: ```csharp // Cluster-level retriever (applies to all routes using this cluster) new ClusterConfig() { ClusterId = "cluster-with-impersonation", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination1", new() { Address = "https://api.example.com" } }, } }.WithAccessTokenRetriever() ``` ### JSON Configuration [Section titled “JSON Configuration”](#json-configuration) Use the `Duende.Bff.Yarp.AccessTokenRetriever` metadata key with an assembly-qualified type name: ```json { "ReverseProxy": { "Routes": { "impersonation": { "ClusterId": "cluster1", "Match": { "Path": "/api/impersonation/{**catch-all}" }, "Metadata": { "Duende.Bff.Yarp.TokenType": "User", "Duende.Bff.Yarp.AntiforgeryCheck": "true", "Duende.Bff.Yarp.AccessTokenRetriever": "MyApp.ImpersonationAccessTokenRetriever, MyApp" } } }, "Clusters": { "cluster-with-impersonation": { "Destinations": { "destination1": { "Address": "https://api.example.com" } }, "Metadata": { "Duende.Bff.Yarp.AccessTokenRetriever": "MyApp.ImpersonationAccessTokenRetriever, MyApp" } } } } } ``` ### Precedence [Section titled “Precedence”](#precedence) When a retriever is specified on both the route and the cluster, the **route-level retriever takes precedence**. This allows you to set a default retriever on a cluster and override it for specific routes. Note The custom retriever type must implement `IAccessTokenRetriever` and be registered in the service collection. ----- # BFF Security Framework Blazor Support > Overview of integrating the Duende BFF Security Framework with Blazor applications for secure authentication and authorization. Microsoft’s Blazor framework helps developers build rich, interactive web applications using C# and .NET. While Blazor is well-suited for rich web UIs, it introduces unique challenges around secure authentication and authorization — especially when rendering happens both on the server and in the browser. The Duende BFF Security Framework addresses these challenges by keeping access tokens on the server and providing a unified authentication state across Blazor’s rendering modes. ## Architecture [Section titled “Architecture”](#architecture) A BFF-backed Blazor app has three elements: the **backend** (server-side logic and APIs), the **frontend** (the Blazor application), and the **client** (the browser). The BFF host acts as the combined backend and frontend host: ``` flowchart LR Client[Client / Browser] subgraph BFF Host Backend[Backend APIs] Frontend[Blazor Frontend] end Client <--> Frontend Frontend <--> Backend ``` For a detailed architecture diagram and explanation, see the [Architecture overview](/bff/architecture/). ## Where to Go Next [Section titled “Where to Go Next”](#where-to-go-next) | Topic | Description | | ----------------------------------------------------------------------- | -------------------------------------------------- | | [Rendering Modes & BFF](/bff/fundamentals/blazor/rendering-modes/) | Which Blazor rendering modes work with BFF and why | | [Data Access Patterns](/bff/fundamentals/blazor/data-access/) | How to securely call APIs from Blazor components | | [Getting Started: Blazor](/bff/getting-started/blazor/) | Step-by-step setup guide for a Blazor BFF app | | [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) | Persistent session storage for Blazor apps | | [Token Management](/bff/fundamentals/tokens/) | How BFF manages access tokens for Blazor | ## See Also [Section titled “See Also”](#see-also) * [Access Token Management](/accesstokenmanagement/) * [Blazor Server token management](/accesstokenmanagement/blazor-server/) * [IdentityServer Quickstarts](/identityserver/quickstarts/0-overview/) ----- # Blazor Data Access Patterns > How to securely access local and remote APIs from Blazor components using the BFF security framework. Depending on your Blazor rendering mode, you need different strategies for accessing data from components. The BFF security framework provides a consistent model: tokens never leave the server, and browser components access data through BFF-hosted endpoints secured with the authentication cookie. ## Overview [Section titled “Overview”](#overview) ``` flowchart LR WASM["Blazor WASM Component
(browser)"] Server["BFF Host
(server)"] Remote["Remote API
(external)"] DB["Database
(server-side)"] WASM -- "cookie + X-CSRF header" --> Server Server -- "access token" --> Remote Server -- "direct access" --> DB ``` For server-side rendering, components access data directly (database, services). For WASM rendering, components make HTTP calls to BFF-hosted endpoints which handle token attachment. ## Embedded (Local) APIs [Section titled “Embedded (Local) APIs”](#embedded-local-apis) An Embedded API is hosted within the BFF itself. It lives within the server’s security boundary, so no token needs to be passed to the browser. ### Defining the Abstraction [Section titled “Defining the Abstraction”](#defining-the-abstraction) Use an interface to abstract between server and client implementations: Shared/IDataAccessor.cs ```csharp public interface IDataAccessor { Task GetData(); } public record Data(string Value); ``` ### Server Implementation [Section titled “Server Implementation”](#server-implementation) Server/ServerDataAccessor.cs ```csharp internal class ServerDataAccessor : IDataAccessor { public Task GetData() { // Access data directly (database, cache, etc.) return Task.FromResult(new[] { new Data("example") }); } } ``` Register the server implementation and expose it as a BFF endpoint: Server/Program.cs ```csharp builder.Services.AddSingleton(); // ... app.MapGet("/some_data", async (IDataAccessor dataAccessor) => await dataAccessor.GetData()) .RequireAuthorization() .AsBffApiEndpoint(); ``` ### Client (WASM) Implementation [Section titled “Client (WASM) Implementation”](#client-wasm-implementation) On the client, use an `HttpClient` that routes through the BFF host: Client/Program.cs ```csharp builder.Services.AddBffBlazorClient() .AddLocalApiHttpClient(); // Register the concrete implementation with the abstraction builder.Services.AddSingleton(sp => sp.GetRequiredService()); ``` Client/HttpClientDataAccessor.cs ```csharp internal class HttpClientDataAccessor(HttpClient client) : IDataAccessor { public async Task GetData() => await client.GetFromJsonAsync("/some_data") ?? throw new JsonException("Failed to deserialize"); } ``` Note When using `AddLocalApiHttpClient()`, the `HttpClient` is pre-configured to include the authentication cookie and `X-CSRF` header automatically. You do not need to set these manually. ## Secured Remote APIs [Section titled “Secured Remote APIs”](#secured-remote-apis) If your BFF needs to proxy requests to a remote API (one that requires a bearer token), configure a remote endpoint on the server and access it from the client via the BFF proxy. ### Server-side Proxy Setup [Section titled “Server-side Proxy Setup”](#server-side-proxy-setup) Server/Program.cs ```csharp app.MapRemoteBffApiEndpoint("/remote-apis/data", new Uri("https://api.example.com/data")) .WithAccessToken(RequiredTokenType.User); ``` Also register an `HttpClient` that attaches the user access token for use in Embedded API endpoints: ```csharp builder.Services.AddUserAccessTokenHttpClient("backend", configureClient: client => client.BaseAddress = new Uri("https://api.example.com/")); ``` ### Client-Side Access [Section titled “Client-Side Access”](#client-side-access) Client/Program.cs ```csharp builder.Services.AddBffBlazorClient(); builder.Services.AddRemoteApiHttpClient("backend"); builder.Services.AddTransient(sp => sp.GetRequiredService().CreateClient("backend")); ``` The diagram below shows the full flow: ``` sequenceDiagram participant WASM as Blazor WASM participant BFF as BFF Host participant API as Remote API WASM->>BFF: GET /remote-apis/data (cookie + X-CSRF) BFF->>BFF: Validate session & get access token BFF->>API: GET /data (Bearer token) API-->>BFF: 200 OK + data BFF-->>WASM: 200 OK + data ``` ## Auto-Rendering Mode [Section titled “Auto-Rendering Mode”](#auto-rendering-mode) In Interactive Auto mode, a component may render on the server first, then transition to WASM. Use the interface-based abstraction pattern from above: inject `IDataAccessor` in your component, and register both `ServerDataAccessor` (for server rendering) and `HttpClientDataAccessor` (for WASM rendering). ```razor @* Component works identically in server and WASM rendering modes *@ @inject IDataAccessor DataAccessor @if (items == null) {

Loading...

} else { @foreach (var item in items) {

@item.Value

} } @code { private Data[]? items; protected override async Task OnInitializedAsync() { items = await DataAccessor.GetData(); } } ``` ## See Also [Section titled “See Also”](#see-also) [Embedded (Local) APIs](/bff/fundamentals/apis/local/)Full reference for BFF-hosted endpoints [Proxying Remote APIs](/bff/fundamentals/apis/remote/)Direct forwarding to upstream services [YARP Integration](/bff/fundamentals/apis/yarp/)Advanced reverse proxy configuration [Rendering Modes & BFF](/bff/fundamentals/blazor/rendering-modes/)Which Blazor modes need BFF [Getting Started: Blazor](/bff/getting-started/blazor/)Full setup walkthrough ----- # Blazor Rendering Modes & BFF > Learn which Blazor rendering modes are compatible with the BFF security pattern and why. Blazor supports [several rendering modes](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-9.0#render-modes). The BFF pattern is only applicable to modes where code runs in the browser (client context), because that is where the risk of token exposure exists. ## Rendering Mode Compatibility [Section titled “Rendering Mode Compatibility”](#rendering-mode-compatibility) | Mode | Description | Renders In | Interactive | Use BFF? | | --------------------------- | ------------------------------------------------------------- | ---------------- | ----------- | -------- | | **Static Server** | Static server-side rendering (SSR) | Server | ❌ | ❌ | | **Interactive Server** | Interactive SSR using Blazor Server and WebSockets | Server | ✅ | ❌ | | **Interactive WebAssembly** | Client-side rendering (CSR) using Blazor WASM | Browser | ✅ | ✅ | | **Interactive Auto** | Starts as Interactive Server, switches to WASM after download | Server → Browser | ✅ | ✅ | ## Static Server [Section titled “Static Server”](#static-server) Caution BFF is not necessary for Static Server rendering. Standard ASP.NET Core authentication patterns apply. Static Server renders Blazor components as plain HTML with no client-side interactivity. Because all rendering happens on the server, tokens never reach the browser. Use standard ASP.NET Core cookie authentication instead of BFF. You may still want to use the `AuthenticationStateProvider` for accessing user claims in components. ## Interactive Server [Section titled “Interactive Server”](#interactive-server) Caution BFF is not typically necessary for Interactive Server rendering. All component interactivity is managed server-side via WebSockets (SignalR). In Interactive Server mode, Blazor components run on the server and push UI updates to the browser over a WebSocket connection. Because no application code runs in the browser, tokens remain server-side naturally. You can still use [Session Management](/bff/fundamentals/session/) features of BFF if you want server-side session control, but the BFF security pattern itself is not required. ## Interactive WebAssembly [Section titled “Interactive WebAssembly”](#interactive-webassembly) Note **BFF is recommended for Interactive WebAssembly.** Your Blazor components execute inside the browser and can be subject to XSS and token theft attacks. In Interactive WebAssembly mode, the Blazor runtime and your application code are downloaded to and executed in the browser. This means: * Your components run in the same JavaScript sandbox as the rest of the page * Any access token stored in the WASM memory is potentially accessible to injected scripts * You must never expose access tokens to WASM components The BFF pattern solves this by keeping tokens on the server. WASM components call BFF-hosted API endpoints (using the authentication cookie), and the BFF attaches the access token server-side before forwarding to remote APIs. See the [Getting Started: Blazor guide](/bff/getting-started/blazor/) for setup instructions. ## Interactive Auto [Section titled “Interactive Auto”](#interactive-auto) Note **BFF is recommended for Interactive Auto.** This mode starts as Interactive Server but transitions to WebAssembly, creating an unpredictable execution context. Interactive Auto combines Interactive Server and Interactive WebAssembly: rendering starts on the server but switches to client-side WASM on subsequent visits after the Blazor bundle is downloaded. Because your application may be running in the browser at any time, you cannot rely on server-side-only token handling. The BFF pattern ensures tokens remain server-side regardless of which rendering mode is active. ## Authentication State [Section titled “Authentication State”](#authentication-state) The `AuthenticationState` contains information about the currently logged-in user, including management claims like the logout URL. Blazor uses `AuthenticationStateProvider` implementations to make authentication state available to components: * **On the server**: The BFF’s `AddServerManagementClaimsTransform` enriches the claims with the logout URL. * **On the client (WASM)**: The `BffClientAuthenticationStateProvider` polls `/bff/user` to keep the client in sync with the server session. This also notifies the frontend if the session is terminated server-side (e.g., back-channel logout). ## Server-Side Token Store [Section titled “Server-Side Token Store”](#server-side-token-store) Blazor Server applications stream content over a WebSocket, so there is often no `HttpContext` available during component execution. This means: * You cannot use `HttpContext` extension methods in Blazor Server components * The normal mechanism to attach tokens to `HttpClient` calls does not work without special setup When you register `AddBlazorServer()`, BFF automatically registers the `ServerSideTokenStore` and Duende.AccessTokenManagement integration so that token management works correctly in Blazor Server. For more details, see [Blazor Server token management](/accesstokenmanagement/blazor-server/). ## See Also [Section titled “See Also”](#see-also) [Data Access Patterns](/bff/fundamentals/blazor/data-access/)How to call APIs from Blazor components [Getting Started: Blazor](/bff/getting-started/blazor/)Full walkthrough setup guide [Troubleshooting](/bff/troubleshooting/)Common Blazor BFF issues ----- # Production Deployment > Guide for deploying Duende BFF to production, covering load balancing, data protection, health checks, cookie domain configuration, and monitoring. This page covers the production-specific concerns you need to address before deploying a BFF host. For middleware pipeline order, see [Middleware Pipeline](/bff/fundamentals/middleware-pipeline/). ## Load Balancing and Sticky Sessions [Section titled “Load Balancing and Sticky Sessions”](#load-balancing-and-sticky-sessions) The BFF uses ASP.NET Core’s Data Protection to encrypt and sign session cookies. In a multi-instance (load-balanced) deployment, **all instances must share the same Data Protection key ring** — otherwise cookies issued by one instance cannot be decrypted by another, causing random logout on failover. ### Shared Key Storage [Section titled “Shared Key Storage”](#shared-key-storage) Configure Data Protection to store keys in a shared location accessible to all instances: ```csharp // Using Azure Blob Storage + Azure Key Vault builder.Services.AddDataProtection() .PersistKeysToAzureBlobStorage(connectionString, "data-protection", "keys.xml") .ProtectKeysWithAzureKeyVault(keyIdentifier, credential); // Using a network file share or a database builder.Services.AddDataProtection() .PersistKeysToFileSystem(new DirectoryInfo(@"\\server\share\dp-keys")) .ProtectKeysWithCertificate(certificate); ``` Do not use in-memory keys in production The default in-memory key ring is regenerated on every restart. Any existing sessions are invalidated when the process restarts or when traffic is routed to a new instance. See also: [Data Protection](/general/data-protection/) ### Server-Side Sessions (Recommended for Multi-Instance) [Section titled “Server-Side Sessions (Recommended for Multi-Instance)”](#server-side-sessions-recommended-for-multi-instance) With cookie-only sessions, every instance must share Data Protection keys. With **server-side sessions**, the cookie only holds an opaque session ID — the session payload is stored in a shared database. This is simpler to operate because: * Key ring only needs to be consistent (not necessarily shared) — the cookie just holds an ID * Sessions can be inspected and revoked server-side * Cookie size is minimized [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/)Setup guide for database-backed session storage ### Sticky Sessions [Section titled “Sticky Sessions”](#sticky-sessions) If you cannot use server-side sessions and cannot share a Data Protection key ring, configure your load balancer to route each user consistently to the same instance (“sticky sessions” / session affinity). This is a last resort — prefer shared key storage. ## Health Check Endpoints [Section titled “Health Check Endpoints”](#health-check-endpoints) Expose a health check endpoint so your load balancer and orchestrator (Kubernetes, etc.) can detect unhealthy instances: ```csharp builder.Services.AddHealthChecks(); // In your pipeline (after UseRouting): app.MapHealthChecks("/health"); ``` For a more complete health check that validates downstream dependencies (database, token endpoint reachability): ```csharp builder.Services.AddHealthChecks() .AddDbContextCheck() // EF Core session store .AddUrlGroup(new Uri("https://idp.example.com/.well-known/openid-configuration"), name: "identity-provider"); app.MapHealthChecks("/health/live", new HealthCheckOptions { Predicate = _ => false }); app.MapHealthChecks("/health/ready", new HealthCheckOptions()); ``` * `/health/live` — liveness: is the process running? (no dependency checks) * `/health/ready` — readiness: are all dependencies reachable? (fail this to take the instance out of rotation) ## Cookie Domain Configuration [Section titled “Cookie Domain Configuration”](#cookie-domain-configuration) By default, the BFF session cookie is scoped to the exact host. If you need the cookie to work across subdomains (e.g. `app.example.com` and `api.example.com`): ```csharp builder.Services.AddAuthentication() .AddCookie(options => { options.Cookie.Domain = ".example.com"; // Note leading dot options.Cookie.SameSite = SameSiteMode.Strict; options.Cookie.SecurePolicy = CookieSecurePolicy.Always; }); ``` Scope cookies as narrowly as possible Setting a broad cookie domain (`.example.com`) means the cookie is sent to all subdomains, including any you don’t control. Only use this when architecturally required, and ensure all subdomains are trusted. For split-host deployments (frontend on `app.example.com`, BFF on `bff.example.com`), you will need to set the cookie domain AND configure CORS. See [Separate Host for UI](/bff/architecture/ui-hosting/) and the [SplitHosts sample](/bff/samples/). ## Reverse Proxy Configuration [Section titled “Reverse Proxy Configuration”](#reverse-proxy-configuration) The BFF is typically deployed behind a reverse proxy (NGINX, Azure Application Gateway, AWS ALB, etc.). Configure ASP.NET Core to trust forwarded headers: ```csharp builder.Services.Configure(options => { options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; // Restrict to your proxy's IP to prevent header spoofing: options.KnownProxies.Add(IPAddress.Parse("10.0.0.100")); }); // Must be the FIRST middleware: app.UseForwardedHeaders(); ``` Without this, `HttpContext.Request.Scheme` will be `http` even when the client uses HTTPS, which causes: * The OIDC redirect URI to be `http://...` (rejected by the identity provider) * The session cookie’s `Secure` flag to have no effect * `HttpContext.Request.Host` to reflect the internal host, breaking the OIDC redirect ## Monitoring and Alerting [Section titled “Monitoring and Alerting”](#monitoring-and-alerting) BFF emits OpenTelemetry metrics and traces. See [Diagnostics](/bff/diagnostics/) for the full list of metric names and activity sources. ### Key Metrics to Alert On [Section titled “Key Metrics to Alert On”](#key-metrics-to-alert-on) | Metric | Alert Condition | Likely Cause | | ----------------------------------------- | ------------------- | ---------------------------------------------------------------------------- | | `session.started` | Sudden drop to 0 | Data Protection key mismatch, pod restart without shared keys | | `session.ended` | Unexpected spike | Back-channel logout sweep, session store purge, Data Protection key rotation | | `session.ended` / `session.started` ratio | Sustained ratio > 1 | Sessions ending faster than starting — investigate IdP or store issues | | HTTP 5xx on `/bff/*` endpoints | Any sustained spike | BFF host error — check logs | ### Recommended Alerts [Section titled “Recommended Alerts”](#recommended-alerts) ```plaintext # Prometheus-style alert examples # Metric names are converted from dot notation to underscores by Prometheus. # No new sessions — potential Data Protection key mismatch alert: BffNoNewSessions expr: rate(session_started_total[5m]) == 0 for: 5m # Abnormal session churn alert: BffSessionChurn expr: rate(session_ended_total[5m]) / rate(session_started_total[5m]) > 2 for: 5m ``` ## See Also [Section titled “See Also”](#see-also) [Middleware Pipeline](/bff/fundamentals/middleware-pipeline/)Correct middleware ordering for production [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/)Recommended for multi-instance deployments [Diagnostics](/bff/diagnostics/)Metrics and distributed tracing reference [Separate Host for UI](/bff/architecture/ui-hosting/)Split-host deployment patterns ----- # Middleware Pipeline > The correct ASP.NET Core middleware order for Duende BFF applications, with explanations of what each component does and common misconfiguration pitfalls. Getting the middleware pipeline order right is critical for BFF to function correctly. Placing middleware in the wrong order can silently disable security features with no obvious error message. ## Canonical Pipeline Order [Section titled “Canonical Pipeline Order”](#canonical-pipeline-order) Program.cs ```csharp var app = builder.Build(); // 1. Forwarded headers (if behind a reverse proxy) app.UseForwardedHeaders(); // 2. HTTPS redirection app.UseHttpsRedirection(); // 3. Static files (serve before auth to avoid unnecessary overhead) app.UseStaticFiles(); // 4. Routing — must come before UseBff and UseAuthorization app.UseRouting(); // 5. Authentication — must come before UseBff app.UseAuthentication(); // 6. BFF middleware — must come AFTER UseAuthentication and UseRouting, // but BEFORE UseAuthorization app.UseBff(); // 7. Authorization app.UseAuthorization(); // 8. Map your endpoints app.MapGet("/api/data", () => Results.Ok("hello")) .RequireAuthorization() .AsBffApiEndpoint(); app.Run(); ``` ## Why Order Matters [Section titled “Why Order Matters”](#why-order-matters) Each middleware in the pipeline can only see the work done by the middleware before it. Here’s why each position is required: | Position | Middleware | Why Here | | ---------------------------------------------------- | --------------------- | ---------------------------------------------------------------------------------------------- | | Before `UseBff` | `UseRouting()` | BFF needs the endpoint route resolved to know which endpoints require anti-forgery protection | | Before `UseBff` | `UseAuthentication()` | BFF reads the authenticated user from the `HttpContext`; without this, the user is always null | | After `UseAuthentication`, before `UseAuthorization` | `UseBff()` | BFF anti-forgery checks run here; placing it after `UseAuthorization` silently disables them | | After `UseBff` | `UseAuthorization()` | Authorization decisions depend on BFF’s pre-processing having already run | Silent failure — no error if pipeline is wrong If `UseBff()` is placed **after** `UseAuthorization()`, anti-forgery enforcement is **silently disabled** — no exception is thrown and no log warning is emitted by default. Always verify pipeline order when debugging authentication issues. The `EnforceBffMiddleware` option (enabled by default) adds a check that throws at startup if the BFF management endpoints are called without the BFF middleware being present. However, this does not catch all ordering mistakes. ## BFF v4 — Automatic Middleware Registration [Section titled “BFF v4 — Automatic Middleware Registration”](#bff-v4--automatic-middleware-registration) In BFF v4, when `AutomaticallyRegisterBffMiddleware` is enabled (the default), the middleware components are registered automatically. You still need to call `UseBff()` yourself in the correct position, but the frontend selection, path mapping, OpenID Connect callbacks, and static file proxying middlewares are added automatically. If you need full control over the pipeline, disable automatic registration: ```csharp builder.Services.AddBff(options => { options.AutomaticallyRegisterBffMiddleware = false; }); ``` Then register each component manually: ```csharp // Before Authentication: app.UseForwardedHeaders(); app.UseBffPreProcessing(); // Frontend selection, path mapping, OIDC callbacks app.UseAuthentication(); app.UseRouting(); // The main BFF middleware (anti-forgery): app.UseBff(); app.UseAuthorization(); // After endpoint mapping: app.UseBffPostProcessing(); // Management endpoints, remote API handling, static file proxying ``` ## Blazor Pipeline Order [Section titled “Blazor Pipeline Order”](#blazor-pipeline-order) Blazor applications need a slightly different order to accommodate Blazor’s own middleware: ```csharp app.UseRouting(); app.UseAuthentication(); // BFF must come after UseAuthentication app.UseBff(); app.UseAuthorization(); // Blazor's anti-forgery protection (separate from BFF's anti-forgery) app.UseAntiforgery(); // In v3, also add: // app.MapBffManagementEndpoints(); app.MapRazorComponents() .AddInteractiveServerRenderMode() .AddInteractiveWebAssemblyRenderMode(); ``` ## Common Mistakes [Section titled “Common Mistakes”](#common-mistakes) | Mistake | Symptom | Fix | | ---------------------------------------- | --------------------------------------------------- | --------------------------------------------------- | | `UseBff()` after `UseAuthorization()` | Anti-forgery silently disabled; `401` on API calls | Move `UseBff()` before `UseAuthorization()` | | Missing `UseAuthentication()` | All users appear anonymous; no redirect to login | Add `app.UseAuthentication()` before `app.UseBff()` | | Missing `UseRouting()` before `UseBff()` | Anti-forgery checks don’t apply correctly to routes | Add `app.UseRouting()` before `app.UseBff()` | | `.AsBffApiEndpoint()` missing | API returns `302` redirect instead of `401` | Add `.AsBffApiEndpoint()` to each API endpoint | ## See Also [Section titled “See Also”](#see-also) [Getting Started: Single Frontend](/bff/getting-started/single-frontend/)Complete setup example with correct pipeline [Getting Started: Blazor](/bff/getting-started/blazor/)Blazor-specific pipeline setup [Troubleshooting](/bff/troubleshooting/)Diagnosing anti-forgery and authentication failures [Configuration Options](/bff/fundamentals/options/)AutomaticallyRegisterBffMiddleware and related settings ----- # Multi-Frontend > Configure a single Duende BFF instance to serve multiple browser-based frontends with independent OpenID Connect settings, cookies, and API surfaces. The Backend For Frontend pattern basically states that there should be a single backend for each frontend. While for some applications / architectures this makes a lot of sense, because there is a 1-to-1 mapping between the API surface and the browser based application, for some other architectures this may not be useful. Especially in micro-service based architectures, where there are many backend APIs and multiple frontends using these APIs, having a dedicated backend service for each frontend introduces quite a lot of operational overhead. To overcome this issue, a single BFF instance can support multiple frontends. Each frontend you configure can: * Define its own OpenID Connect configuration * Define its own Cookie settings * Define its own API surface * Be identified either via path based routing and/or host selection. Adding additional frontends to the BFF has very little impact on the performance on the BFF itself, but keep in mind that the traffic for all the frontends is proxied through the BFF. ## Authentication Configuration [Section titled “Authentication Configuration”](#authentication-configuration) When you use multiple frontends, you can’t rely on [manual authentication configuration](/bff/fundamentals/session/handlers/#manually-configuring-authentication). This is because each frontend requires its own scheme, and potentially its own OpenID Connect and Cookie configuration. Instead, you should rely on [automatic authentication configuration](/bff/fundamentals/session/handlers/#automatic-authentication-configuration). Below is an example on how to configure multiple frontends. ```csharp var bffBuilder = builder.Services .AddBff(); bffBuilder .ConfigureOpenIdConnect(options => { // These are the default values for all frontends. options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }); .AddFrontends( // This frontend will use the default authentication options new BffFrontend(BffFrontendName.Parse("default-frontend")), // This frontend uses most of the same authentication options, new BffFrontend(BffFrontendName.Parse("with-path")) .MapToPath("/with-path") .WithOpenIdConnectOptions(opt => { // but overrides the clientid and client secret. opt.ClientId = "different-client-id"; opt.ClientSecret = "different secret"; }) .WithCookieOptions(opt => { // and overrides the cookie options to use 'lax' cookies. opt.Cookie.SameSite = SameSiteMode.Lax; })); ``` The order in which configuration is applied is 1. programmatic default options (if any) 2. default options from configuration (if any) 3. frontend specific options (if any) Each frontend can have custom OpenID Connect configuration and Cookie Configuration. This can both be configured programmatically as via [Configuration](configuration/). ## Frontend Selection [Section titled “Frontend Selection”](#frontend-selection) Each request to a frontend has to be uniquely defined by either its path, its host or a combination of the two. If you specify neither, then it’s considered the default frontend. Note With “host”, we mean the combination of the schema (http/https), the domain (app1.example.com) and the port. The BFF frontend selection middleware uses the HTTP Host header to select a matching frontend. Frontends are matched using the following algorithm: 1. **Selection by both host and path:** If there is a frontend that matches both the host AND has the most specific match to a path, it’s selected. 2. **Selection by host only:** Then, if there is a frontend with only hosts configured and it matches the path, it’s selected. 3. **Selection by path only:** Then, if there is a frontend with a matching path specified, it’s selected. 4. **Default frontend:** Then, if there is a default frontend configured, it’s selected. In summary, the most specific match will be selected. Note When using path based routing, then the frontend’s path is added to the [`PathBase`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.pathbase) and removed from the [`Path`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.http.httprequest.path). This means that any routing that happens in the application is relative to the path of the frontend. This also includes the OpenID callback paths. ### Implicit Frontend Disabled [Section titled “Implicit Frontend Disabled”](#implicit-frontend-disabled) When you don’t add any frontends, BFF creates an implicit default frontend. This allows BFF to function correctly in single frontend mode. As soon as you add a frontend, this implicit frontend is disabled. If you want to use both explicitly matching frontends (on host headers or paths) and a default (fallback) frontend, you should explicitly add this default frontend. ## Adding A Frontend During Startup [Section titled “Adding A Frontend During Startup”](#adding-a-frontend-during-startup) The simplest way to add frontends is during startup. ```csharp services .AddBff() .AddFrontends(new BffFrontend(BffFrontendName.Parse("frontend1"))); ``` You can call `AddFrontends` with multiple frontends in one go, or call it multiple times. ## Adding / Updating A Frontend Dynamically At Runtime [Section titled “Adding / Updating A Frontend Dynamically At Runtime”](#adding--updating-a-frontend-dynamically-at-runtime) If you want to manipulate the frontends at runtime, you can do so via the `IFrontendCollection` interface. ```csharp var frontends = app.Services.GetRequiredService(); frontends.AddOrUpdate(new BffFrontend(name)); frontends.Remove(name); ``` ## Defining The API Surface [Section titled “Defining The API Surface”](#defining-the-api-surface) A frontend can define its own API surface, by specifying remote APIs. ```csharp var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithRemoteApis( // map the local path /path to the remote api new RemoteApi("/some_path", new Uri("https://remote-api"))) // You can also configure various options, such as the type of token, // retrieval parameters, etc.. new RemoteApi("/with_options", new Uri("https://remote-api"))) .WithAccessToken(RequiredTokenType.UserOrClient), .WithAccessTokenRetriever(), .WithUserAccessTokenParameters(new BffUserAccessTokenParameters { Resource = Resource.Parse("urn:isolated-api") })); ``` See the topic on [Token Management](/bff/fundamentals/tokens/) for more information about the various token management options. ## Handling SPA Static Assets [Section titled “Handling SPA Static Assets”](#handling-spa-static-assets) BFF can be configured to handle the static file assets that are typically used when developing Single-Page Application (SPA)-based app. ### Proxying Only `index.html` [Section titled “Proxying Only index.html”](#proxying-only-indexhtml) When deploying a multi-frontend BFF, it makes most sense to have the frontend’s configured with an `index.html` file that is retrieved from a Content Delivery Network (CDN). This can be done in various ways. For example, if you use Vite, you can publish static assets with a base URL configured. This will make sure that any static asset, (such as images, scripts, etc) are retrieved directly from the CDN for best performance. ```csharp var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithCdnIndexHtmlUrl(new Uri("https://my_cdn/some_app/index.html")) ``` When you do this, the BFF automatically wires up a catch-all route that serves the `index.html` for that specific frontend. See [Serve the index page from the BFF host](/bff/architecture/ui-hosting/#serve-the-index-page-from-the-bff-host) for more information. #### Transforming the `index.html` [Section titled “Transforming the index.html”](#transforming-the-indexhtml) If you need to modify the `index.html` before it is served to the client (for example, to inject frontend-specific configuration or environment variables), you can implement the `IIndexHtmlTransformer` interface: ```csharp public class MyIndexHtmlTransformer : IIndexHtmlTransformer { public Task Transform(string indexHtml, BffFrontend frontend, CancellationToken ct = default) { // Inject a frontend-specific config script tag var transformed = indexHtml.Replace( "", $""); return Task.FromResult(transformed); } } ``` Register the transformer in the service collection: ```csharp services.AddSingleton(); ``` The transformer is called after the `index.html` is fetched from the CDN and before it is cached. The cache duration is controlled by the [`IndexHtmlDefaultCacheDuration`](/bff/fundamentals/options/#cdn--static-assets) option. ### Proxying All Static Assets [Section titled “Proxying All Static Assets”](#proxying-all-static-assets) When developing a Single-Page Application (SPA), it’s very common to use a development webserver such as Vite. While Vite can publish static assets with a base URL, this doesn’t work well during development. The best development experience can be achieved by configuring the BFF to proxy all static assets from the development server: ```csharp var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithProxiedStaticAssets(new Uri("https://localhost:3000")); // https://localhost:3000 would be the URL of your development web server. ``` While this can also be done in production, it will proxy all static assets through BFF. This will increase the bandwidth consumed by the BFF and reduce the overall performance of your application. ### Proxying Assets Based On Environment [Section titled “Proxying Assets Based On Environment”](#proxying-assets-based-on-environment) If you’re using a local development server during development and a CDN in production, you can configure asset proxying as follows: ```csharp // In this example, the environment name from the application builder is used to determine // if we're running in production or not. var runningInProduction = () => builder.Environment.EnvironmentName == Environments.Production; // Then, when configuring the frontend, you can switch when the static assets will be proxied. new BffFrontend(BffFrontendName.Parse("default-frontend")) .WithBffStaticAssets(new Uri("https://localhost:5010/static"), useCdnWhen: runningInProduction); ``` Note This function is evaluated immediately when calling the method `.WithBffStaticAssets()`. If you call this method during startup, the condition is only evaluated at startup time. It’s not evaluated at runtime for every request. ----- # BFF Multi-Frontend Configuration > Configure Duende BFF multi-frontend settings via IConfiguration with dynamic reloading support. Define OIDC, cookie, and API settings for each frontend. It’s possible to configure frontends for the BFF via `IConfiguration`. This enables dynamic loading / changing of frontends, including their OpenID Connect configuration, BFF Configuration, and Remote APIs. ```csharp var bffConfig = new ConfigurationBuilder() .AddJsonFile(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "BffConfig.json"), optional: false, reloadOnChange: true) services .AddBff() .LoadConfiguration(bffConfig); ``` The configuration supports dynamic reloading (so any new frontend added / removed is immediately reflected). ### BffConfiguration [Section titled “BffConfiguration”](#bffconfiguration) * `defaultOidcSettings` OIDC settings applied globally to all frontends unless overridden.\ Type: OidcConfiguration object ([see below](#oidcconfiguration-json-properties)). * `defaultCookieSettings` Cookie settings applied globally to all frontends unless overridden.\ Type: CookieConfiguration object ([see below](#cookieconfiguration-json-properties)). * `frontends` Dictionary of frontend configurations.\ Each key is a frontend name, and the value is a BffFrontendConfiguration object ([see below](#bfffrontendconfiguration-json-properties)). *** ### BffFrontendConfiguration JSON Properties [Section titled “BffFrontendConfiguration JSON Properties”](#bfffrontendconfiguration-json-properties) * `cdnIndexHtmlUrl` The `index.html` that should be used for this frontend (usually on a CDN). When using this property, a fallback route will be created that only proxies the `index.html`. Other static assets are supposed to be retrieved directly from the CDN by the browser. Example: `"https://cdn.yourapp.com/some_app/index.html"` * `staticAssetsUrl` The URL where all static assets can be found. This registers a fallback route that will proxy all static assets from this URL. This is usually used during development, when you’re using a development web server such as Vite. Example: `"https://localhost:3000/"` * `matchingPath` The path prefix for requests routed to this frontend.\ Example: `"/from-config"` * `matchingHostHeader` The host to match for this frontend. Example: `"https://localhost:5005"` * `oidc` OIDC settings specific to this frontend.\ Type: OidcConfiguration object ([see below](#oidcconfiguration-json-properties)). * `cookies` Cookie settings specific to this frontend.\ Type: CookieConfiguration object ([see below](#cookieconfiguration-json-properties)). * `remoteApis` Remote APIs for this frontend. Type: RemoteApiConfiguration object. ([see below](#remoteapiconfiguration-json-properties)). ### RemoteApiConfiguration JSON Properties [Section titled “RemoteApiConfiguration JSON Properties”](#remoteapiconfiguration-json-properties) * `pathMatch` String. The local path that will be used to access the remote API.\ Example: `"/api/user-token"` * `targetUri` String. The target URI of the remote API.\ Example: `"https://localhost:5010"` * `requiredTokenType` String. The token requirement for accessing the remote API.\ Possible values: `"None"`, `"User"`, `"Client"`, `"UserOrClient"`, `"UserOrNone"`\ Default: `"User"` * `tokenRetrieverTypeName` String. The type name of the access token retriever to use for this remote API. * `userAccessTokenParameters` Object. Parameters for retrieving a user access token ([see below](#useraccesstokenparameters-json-properties)). * `activityTimeout` String. How long a request is allowed to remain idle between operations before being canceled.\ Use C# `TimeSpan` serialization format, e.g. `"00:01:40"` for 100 seconds. * `allowResponseBuffering` Boolean. Allows write buffering when sending a response back to the client (if supported by the server).\ Note: Enabling this can break server-sent events (SSE) scenarios. *** ### UserAccessTokenParameters JSON Properties [Section titled “UserAccessTokenParameters JSON Properties”](#useraccesstokenparameters-json-properties) * `signInScheme` String. The scheme used for signing in the user (typically the cookie authentication scheme).\ Example: `"Cookies"` * `challengeScheme` String. The authentication scheme to be used for challenges.\ Example: `"OpenIdConnect"` * `forceRenewal` Boolean. Whether to force renewal of the access token. * `resource` String. The resource for which the access token is requested.\ Example: `"https://api.example.com"` ### OidcConfiguration JSON Properties [Section titled “OidcConfiguration JSON Properties”](#oidcconfiguration-json-properties) * `clientId` The client ID of the OpenID Connect client. * `clientSecret` The client secret of the OpenID Connect client. * `callbackPath` The path or URI to which the OpenID Connect client will redirect after authentication. * `authority` The authority URI, typically the issuer or identity provider endpoint. * `responseType` The response type that the OpenID Connect client will request. * `responseMode` The response mode that the OpenID Connect client will use to return the authentication response. * `mapInboundClaims` Boolean. Whether to map inbound claims from the OpenID Connect provider to the user’s claims in the application. * `saveTokens` Boolean. Whether to save the tokens received from the OpenID Connect provider. * `scope` Array of strings. The scopes that the OpenID Connect client will request from the provider. * `getClaimsFromUserInfoEndpoint` Boolean. Whether to retrieve claims from the UserInfo endpoint of the OpenID Connect provider. ### CookieConfiguration JSON Properties [Section titled “CookieConfiguration JSON Properties”](#cookieconfiguration-json-properties) * `httpOnly` Boolean. Indicates whether the cookie is inaccessible by client-side script. Defaults to true. * `sameSite` String. The SameSite attribute of the cookie. Defaults to `"Strict"`.\ Possible values: `"None"`, `"Lax"`, `"Strict"` * `securePolicy` String. The policy used to determine if the cookie is sent only over HTTPS.\ Possible values: `"Always"`, `"None"`, `"SameAsRequest"` * `name` String. The name of the cookie. * `maxAge` String. The max-age for the cookie. Example: “0:01:00 for 1 minute * `path` String. The cookie path. The BFF will configure the default values for this property. Example: `"/"` * `domain` String. The domain to associate the cookie with. The BFF will configure the default values for this property.\ Example: `"example.com"` ### Example [Section titled “Example”](#example) ```json { "defaultOidcSettings": { "clientId": "global-client", "authority": "https://login.example.com" }, "defaultCookieSettings": null, "frontends": { "some_frontend": { "cdnIndexHtmlUrl": "https://localhost:5005/static/index.html", "matchingPath": "/from-config", "oidc": { "clientId": "frontend1-client", "scope": ["openid", "profile", "email"] }, "remoteApis": [ { "pathMatch": "/todos", "targetUri": "https://localhost:5020/todos/", "requiredTokenType": "User" } ] } } } ``` ----- # Configuration Options > Comprehensive guide to configuring Duende BFF framework including general settings, paths, session management, and API options The `Duende.BFF.BffOptions` allows to configure several aspects of the BFF framework. You set the options at startup time: ```csharp builder.Services.AddBff(options => { // configure options here... }) ``` ## Common Configurations [Section titled “Common Configurations”](#common-configurations) The sections below show complete, annotated options blocks for the most common deployment scenarios. The full reference for every option follows after. ### Production Deployment [Section titled “Production Deployment”](#production-deployment) ```csharp builder.Services.AddBff(options => { // Required for production options.LicenseKey = builder.Configuration["Duende:LicenseKey"]; // Revoke refresh tokens on logout (default: true — keep enabled) options.RevokeRefreshTokenOnLogout = true; // Log out all sessions for a user when back-channel logout is received // Set to true if you want global logout across devices options.BackchannelLogoutAllUserSessions = false; // Session cleanup (v4+): call .AddSessionCleanupBackgroundProcess() instead options.SessionCleanupInterval = TimeSpan.FromMinutes(10); }) // Use Entity Framework for production-grade session storage .AddServerSideSessions() .AddEntityFrameworkServerSideSessions(options => { options.UseSqlServer(connectionString); }); ``` ### Development with a Separate Frontend (Split Host) [Section titled “Development with a Separate Frontend (Split Host)”](#development-with-a-separate-frontend-split-host) When your SPA is served by a separate dev server (e.g., Vite on `localhost:3000`) and the BFF is on a different port: ```csharp builder.Services.AddBff(options => { // Allow the separate frontend origin to use silent login options.AllowedSilentLoginReferers = ["https://localhost:3000"]; }) .ConfigureCookies(options => { // Lax is required when the IDP is on a different site than the BFF options.Cookie.SameSite = SameSiteMode.Lax; }); // Allow CORS requests from the dev server builder.Services.AddCors(options => { options.AddPolicy("DevSpa", policy => policy.WithOrigins("https://localhost:3000") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials()); }); ``` ### Multi-Frontend with Per-Frontend OIDC [Section titled “Multi-Frontend with Per-Frontend OIDC”](#multi-frontend-with-per-frontend-oidc) ```csharp builder.Services.AddBff() // Global OIDC defaults (overridden per-frontend where needed) .ConfigureOpenIdConnect(options => { options.Authority = "https://login.example.com"; options.ClientId = "shared-client"; options.ClientSecret = "secret"; options.SaveTokens = true; options.Scope.Add("offline_access"); }) // Register named frontends .AddFrontends( new BffFrontend(BffFrontendName.Parse("main-app")) .WithCdnIndexHtmlUrl(new Uri("https://cdn.example.com/app/index.html")), new BffFrontend(BffFrontendName.Parse("admin-app")) .MapToPath("/admin") .WithOpenIdConnectOptions(opt => { // Admin frontend uses a different client ID opt.ClientId = "admin-client"; opt.ClientSecret = "admin-secret"; }) .WithCdnIndexHtmlUrl(new Uri("https://cdn.example.com/admin/index.html")) ); ``` ## General [Section titled “General”](#general) * **`EnforceBffMiddleware`** Enables checks in the user management endpoints that ensure that the BFF middleware has been added to the pipeline. Since the middleware performs important security checks, this protects from accidental configuration errors. You can disable this check if it interferes with some custom logic you might have. Defaults to true. * **`LicenseKey`** This sets the license key for Duende.BFF. A license key is required for production deployments. See [licensing](/general/licensing/) for details about how to configure the license key. * **`AnonymousSessionResponse`** (added in 2.0) This sets the response status code behavior on the [user endpoint](/bff/fundamentals/session/management/user/) to either return 401 or 200 with a *null* payload when the user is anonymous. * **`DiagnosticsEnvironments`** The ASP.NET environment names that enable the diagnostics endpoint. Defaults to “Development”. * **`BackChannelHttpHandler`** A HTTP message handler that’s used to configure backchannel communication. Typically used during testing. Configuring this will automatically configure the BackChannelHttpHandler property in *OpenIDConnectOptions* and also set it as the primary http message handler for retrieving the index.html. * **`AutomaticallyRegisterBffMiddleware`** (added in 4.0) When using BFF V4 with multiple frontends, several middlewares are automatically added to the pipeline (frontend selection, path mapping, OpenID Connect callbacks, management endpoints, and static file proxying). If you need full control over the middleware pipeline, set this to `false` and register the middleware manually: ```csharp builder.Services.AddBff(options => { options.AutomaticallyRegisterBffMiddleware = false; }); ``` When disabled, you must call `UseBffPreProcessing()` early in the pipeline (before authentication) and `UseBffPostProcessing()` at the end: ```csharp app.UseForwardedHeaders(); app.UseBffPreProcessing(); // Frontend selection, path mapping, OpenID callbacks app.UseAuthentication(); app.UseRouting(); app.UseBff(); // Anti-forgery checks app.UseAuthorization(); // map your endpoints here... app.UseBffPostProcessing(); // Management endpoints, remote API handling, static file proxying ``` You can also use the individual middleware methods for even more granular control: * `UseBffFrontendSelection()` — Selects the current frontend based on host/path matching * `UseBffPathMapping()` — Adjusts `PathBase` and `Path` for the selected frontend * `UseBffOpenIdCallbacks()` — Handles OpenID Connect callback requests * `UseBffAntiForgery()` — Validates anti-forgery headers (same as `UseBff()`) * `UseBffStaticFileProxying()` — Proxies static file requests to CDN or development server * **`StaticAssetsClientName`** If BFF is configured to automatically retrieve the `index.html`, or to proxy the static assets, it needs an HTTP client to do so. With this name, you can automatically configure this HTTP client in the `HttpClientFactory`. * **`AllowedSilentLoginReferers`** For silent login to work, you normally need to have the BFF backend and the frontend on the same origin. If you have a split host scenario, meaning the backend on a different origin (but same site) as the frontend, then you can use the referer header to differentiate which browser window to post the silent login results to. This array must then contain the list of allowed referer header values. ## Paths [Section titled “Paths”](#paths) * **`LoginPath`** Sets the path to the login endpoint. Defaults to */bff/login*. * **`SilentLoginPath`** Sets the path to the silent login endpoint. Defaults to */bff/silent-login*. * **`SilentLoginCallbackPath`** Sets the path to the silent login callback endpoint. Defaults to */bff/silent-login-callback*. * **`LogoutPath`** Sets the path to the logout endpoint. Defaults to */bff/logout*. * **`UserPath`** Sets the path to the user endpoint. Defaults to */bff/user*. * **`BackChannelLogoutPath`** Sets the path to the backchannel logout endpoint. Defaults to */bff/backchannel*. * **`DiagnosticsPath`** Sets the path to the diagnostics endpoint. Defaults to */bff/diagnostics*. ## Session Management [Section titled “Session Management”](#session-management) * **`ManagementBasePath`** Base path for management endpoints. Defaults to */bff*. * **`RequireLogoutSessionId`** Flag that specifies if the `sid` claim needs to be present in the logout request as query string parameter. Used to prevent cross site request forgery. Defaults to `true`. * **`RevokeRefreshTokenOnLogout`** Specifies if the user’s refresh token is automatically revoked at logout time. Defaults to `true`. * **`BackchannelLogoutAllUserSessions`** Specifies if during backchannel logout all matching user sessions are logged out. If `true`, all sessions for the subject will be revoked. If false, just the specific session will be revoked. Defaults to `false`. * **`~~EnableSessionCleanup~~`** (removed in V4) Indicates if expired server side sessions should be cleaned up. This requires an implementation of IUserSessionStoreCleanup to be registered in the ASP.NET Core service provider. Defaults to `false`. In V4, you need to opt into this value by calling `.AddSessionCleanupBackgroundProcess()` * **`SessionCleanupInterval`** Interval at which expired sessions are cleaned up. Defaults to *10 minutes*. ## APIs [Section titled “APIs”](#apis) * **`AntiForgeryHeaderName`** Specifies the name of the header used for anti-forgery header protection. Defaults to `X-CSRF`. * **`AntiForgeryHeaderValue`** Specifies the expected value of Anti-forgery header. Defaults to `1`. * **`DPoPJsonWebKey`** Specifies the Json Web Key to use when creating DPoP proof tokens. Defaults to null, which is appropriate when not using DPoP. * **`RemoveSessionAfterRefreshTokenExpiration`** Flag that specifies if a user session should be removed after an attempt to use a Refresh Token to acquire a new Access Token fails. This behavior is only triggered when proxying requests to remote APIs with TokenType.User or TokenType.UserOrClient. Defaults to True. * **`DisableAntiForgeryCheck`** (added in V4) A delegate that determines if the anti-forgery check should be disabled for a given request. The default is not to disable anti-forgery checks. ## CDN / Static Assets [Section titled “CDN / Static Assets”](#cdn--static-assets) * **`IndexHtmlDefaultCacheDuration`** (added in V4) If you use CDN Index HTML proxying (via `BffFrontend.CdnIndexHtmlUrl`), this controls how long the fetched `index.html` is cached. Defaults to *5 minutes*. ## Diagnostics [Section titled “Diagnostics”](#diagnostics) * **`Diagnostics`** (added in V4) Options that control the way that diagnostic data is logged. This is a nested options object with the following properties: * **`LogFrequency`** — Frequency at which diagnostic summaries are logged. Defaults to *1 hour*. * **`ChunkSize`** — Max size of diagnostic data log message chunks in bytes. Defaults to *8160 bytes* (8 KB minus 32 bytes for log message formatting overhead). ## BFF Blazor Server Options [Section titled “BFF Blazor Server Options”](#bff-blazor-server-options) In the Blazor Server, you configure the `BffBlazorServerOptions` by using the `AddBlazorServer` method. ```csharp builder.Services.AddBlazorServer(opt => { // configure options here.. }) ``` The following options are available: * **`ServerStateProviderPollingInterval`** The delay, in milliseconds, between polling requests by the BffServerAuthenticationStateProvider to the /bff/user endpoint. Defaults to 5000 ms. ## BFF Blazor Client Options [Section titled “BFF Blazor Client Options”](#bff-blazor-client-options) In WASM, you configure the `BffBlazorClientOptions` using the `AddBffBlazorClient` method: ```csharp builder.Services.AddBffBlazorClient(opt => { // configure options here... }) ``` The following options are available: * **`RemoteApiPath`** The base path to use for remote APIs. * **`RemoteApiBaseAddress`** The base address to use for remote APIs. If unset (the default), the blazor hosting environment’s base address is used. * **`StateProviderBaseAddress`** The base address to use for the state provider’s calls to the /bff/user endpoint. If unset (the default), the blazor hosting environment’s base address is used. * **`WebAssemblyStateProviderPollingDelay`** The delay, in milliseconds, before the BffClientAuthenticationStateProvider will start polling the /bff/user endpoint. Defaults to 1000 ms. * **`WebAssemblyStateProviderPollingInterval`** The delay, in milliseconds, between polling requests by the BffClientAuthenticationStateProvider to the /bff/user endpoint. Defaults to 5000 ms. ## Proxy Servers and Load Balancers v4.0 [Section titled “Proxy Servers and Load Balancers ”v4.0](#proxy-servers-and-load-balancers) When your BFF is hosted behind another reverse proxy or load balancer, you’ll want to use `X-Forwarded-*` headers. BFF automatically registers the `ForwardedHeaders` middleware in the pipeline, without any additional configuration. You will need to configure which headers should be considered by the middleware, typically the `X-Forwarded-For` and `X-Forwarded-Proto` headers. Here’s an example of how you can configure this. Program.cs ```csharp builder.Services.Configure(options => { // Consider configuring the 'KnownProxies' and the 'AllowedHosts' to prevent IP spoofing attacks options.ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto; }); ``` See [proxy servers and load balancers](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer?view=aspnetcore-9.0) in the Microsoft documentation for more information. Note Be careful processing `X-Forwarded-*` headers from untrusted sources. Accepting these headers without validating the proxy IP address or network origin may leave you vulnerable to IP Spoofing attacks. See [Microsoft Security Advisory CVE-2018-0787](https://github.com/aspnet/Announcements/issues/295) for information on an elevation-of-privileges vulnerability that affects systems where the proxy doesn’t validate or restrict `Host` headers to known good values. ----- # Authentication & Session Management > Learn how BFF manages authentication sessions — from the initial OIDC login to server-side session storage, token lifecycle, and back-channel logout. Authentication in a BFF application flows through several layers. Understanding how these layers connect helps you configure sessions correctly and debug problems when they arise. ## How Sessions Work [Section titled “How Sessions Work”](#how-sessions-work) ``` sequenceDiagram participant Browser participant BFF participant IdP as Identity Provider Browser->>BFF: GET /bff/login BFF->>IdP: OIDC Authorization Request IdP-->>Browser: Login UI Browser->>IdP: Credentials IdP-->>BFF: Authorization Code BFF->>IdP: Token Request IdP-->>BFF: Access Token + Refresh Token + ID Token BFF->>BFF: Store tokens in session BFF-->>Browser: Set-Cookie (session cookie) Note over Browser,BFF: Session established Browser->>BFF: GET /api/data (with cookie) BFF->>BFF: Validate session BFF->>BFF: Get/refresh access token BFF-->>Browser: API response ``` ### The Session Cookie [Section titled “The Session Cookie”](#the-session-cookie) After a successful login, BFF sets an **HttpOnly, Secure, SameSite** cookie in the browser. This cookie is the browser’s proof of session — it is sent automatically on every subsequent request to the BFF host. The cookie itself is signed and encrypted by ASP.NET Core’s data protection stack. The browser never has access to the access token or refresh token. These are stored server-side. ### Cookie-Based vs. Server-Side Sessions [Section titled “Cookie-Based vs. Server-Side Sessions”](#cookie-based-vs-server-side-sessions) By default, BFF stores session state (including tokens) inside the encrypted cookie. This works but has limitations: | | Cookie-Based (default) | Server-Side Sessions | | --------------------------- | ---------------------------------------------------- | ---------------------------------- | | **Token storage** | Inside the encrypted cookie | Server-side store (DB, memory) | | **Cookie size** | Grows with claims/tokens — can hit browser 4KB limit | Fixed small size (session ID only) | | **Server-initiated logout** | Not possible | ✅ Possible | | **Back-channel logout** | Not supported | ✅ Supported | | **Session visibility** | None | ✅ Query all active sessions | | **Scale-out** | Cookie encryption keys must be shared | Session store must be shared | Recommended for production Use server-side sessions for any production deployment. They enable back-channel logout support, avoid cookie size issues with large claim sets, and allow the server to forcibly end user sessions. ### Token Lifecycle [Section titled “Token Lifecycle”](#token-lifecycle) Tokens stored in the session are managed automatically: 1. **Access token** — When an API call is made through the BFF, the access token is retrieved from the session. If it is expired or close to expiring, BFF automatically refreshes it using the refresh token. 2. **Refresh token** — Stored server-side (in the session). Revoked automatically at logout. 3. **ID token** — Used during logout to send a `id_token_hint` to the identity provider. See [Token Management](/bff/fundamentals/tokens/) for how to access tokens programmatically. ## Management Endpoints [Section titled “Management Endpoints”](#management-endpoints) The BFF exposes several HTTP endpoints for managing the user’s session. These endpoints are called by the frontend to trigger authentication flows or query session state. | Endpoint | Default Path | Purpose | | ------------------- | ------------------- | --------------------------------------------- | | Login | `/bff/login` | Start the OIDC authentication flow | | Logout | `/bff/logout` | End the session and sign out | | User | `/bff/user` | Return current user claims and session state | | Silent Login | `/bff/silent-login` | Non-interactive login (deprecated in v4) | | Back-Channel Logout | `/bff/backchannel` | Receive server-to-server logout notifications | | Diagnostics | `/bff/diagnostics` | Show current tokens (development only) | ## In This Section [Section titled “In This Section”](#in-this-section) | Page | Description | | -------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | [Authentication Handlers](/bff/fundamentals/session/handlers/) | OIDC and cookie handler configuration | | [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) | Persistent session storage with Entity Framework or custom stores | | [OIDC Prompts](/bff/fundamentals/session/oidc-prompts/) | Controlling interactive vs. silent authentication | | [Login Endpoint](/bff/fundamentals/session/management/login/) | How to trigger login from the frontend | | [Logout Endpoint](/bff/fundamentals/session/management/logout/) | How to trigger logout and CSRF protection | | [User Endpoint](/bff/fundamentals/session/management/user/) | Reading user claims and session state | | [Back-Channel Logout](/bff/fundamentals/session/management/back-channel-logout/) | Server-initiated session termination | | [Silent Login](/bff/fundamentals/session/management/silent-login/) | Non-interactive login (deprecated) | | [Diagnostics](/bff/fundamentals/session/management/diagnostics/) | Development-time token inspection | ## See Also [Section titled “See Also”](#see-also) [IdentityServer Configuration](/identityserver/configuration/)Configure the identity provider your BFF authenticates against [IdentityServer Clients](/identityserver/fundamentals/clients/)Register your BFF as a confidential client [IdentityServer Server-Side Sessions](/identityserver/ui/server-side-sessions/)Coordinate logout across all components [Access Token Management](/accesstokenmanagement/)How tokens are refreshed when sessions are active [Troubleshooting](/bff/troubleshooting/)Common session and authentication issues ----- # ASP.NET Core Authentication System > Learn how to configure and use ASP.NET Core authentication handlers for OpenID Connect and cookie-based session management in BFF applications To configure authentication in the BFF, you’ll need to configure both the OpenID Connect login flow and the cookie handlers. ## Automatic Authentication Configuration V4 [Section titled “Automatic Authentication Configuration ”](#automatic-authentication-configuration) In V4, a simplified mechanism for wiring up authentication has been introduced. The main purpose for the BFF is to handle the OpenID Connect login flow and to protect the APIs using Cookies. In V3, you explicitly had to configure the ASP.NET Core authentication system to enable this. In V4, this is now simplified. A call `BffBuilder.ConfigureOpenIdConnect()` will make sure that: 1. The authentication pipeline is configured with the appropriate authentication schemes. 2. The OpenID Connect pipeline is configured with default values. 3. The CookieHandler is configured using recommended practices. This can be tweaked by calling `BffBuilder.ConfigureCookies()` Below is an example on how to configure the BFF’s authentication pipeline. ```csharp services.AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }); ``` Each frontend can have custom OpenID Connect configuration and Cookie Configuration. This can both be configured programmatically via [Configuration](/bff/fundamentals/multi-frontend/configuration/). ## Manually Configuring Authentication [Section titled “Manually Configuring Authentication”](#manually-configuring-authentication) You typically use the following two ASP.NET Core authentication handlers to implement remote authentication: * the OpenID Connect authentication handler to interact with the remote OIDC / OAuth token service, e.g. Duende IdentityServer * the cookie handler to do local session management The BFF relies on the configuration of the ASP.NET Core default authentication schemes. Both the OpenID Connect authentication handler and cookie handler need to be configured, with the ASP.NET Core authentication system default schemes specified: * `DefaultScheme` should be the cookie handler, so the BFF can do local session management; * `DefaultChallengeScheme` should be the OpenID Connect handler, so the BFF defaults to remote authentication; * `DefaultSignOutScheme` should be the OpenID Connect handler, so the BFF uses remote sign-out. A minimal configuration looks like this: ```csharp builder.Services.AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { // ... }) .AddOpenIdConnect("oidc", options => { // ... }); ``` Now let’s look at some more details! ### The OpenID Connect Authentication Handler [Section titled “The OpenID Connect Authentication Handler”](#the-openid-connect-authentication-handler) The OpenID Connect (OIDC) handler connects the application to the authentication / access token system. It can be configured to use any OpenID Connect provider: [Duende IdentityServer](https://duendesoftware.com/products/identityserver/), [Microsoft Entra ID](https://www.microsoft.com/en-us/security/business/identity-access/microsoft-entra-id), [Auth0](https://auth0.com/), [Google Cloud Identity Platform](https://cloud.google.com/identity-platform), [Amazon Cognito](https://aws.amazon.com/cognito/), and more. The exact settings to use depend on the OIDC provider and its configuration settings. We recommend to: * use authorization code flow with PKCE * use a `response_mode` of `query` since this plays nicer with `SameSite` cookies * use a strong client secret. Since the BFF can be a confidential client, it is possible to use strong client authentication like JWT assertions, JAR, or mTLS. Shared secrets work as well. * turn off inbound claims mapping * save the tokens into the authentication session so they can be automatically managed * request a refresh token using the `offline_access` scope ```csharp builder.Services.AddAuthentication().AddOpenIdConnect("oidc", options => { options.Authority = "https://demo.duendesoftware.com"; // confidential client using code flow + PKCE options.ClientId = "spa"; options.ClientSecret = "secret"; options.ResponseType = "code"; // query response type is compatible with strict SameSite mode options.ResponseMode = "query"; // get claims without mappings options.MapInboundClaims = false; options.GetClaimsFromUserInfoEndpoint = true; // save tokens into authentication session // to enable automatic token management options.SaveTokens = true; // request scopes options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("API"); // and refresh token options.Scope.Add("offline_access"); }); ``` The OIDC handler will use the default sign-in handler (the cookie handler) to establish a session after successful validation of the OIDC response. ### The Cookie Handler [Section titled “The Cookie Handler”](#the-cookie-handler) The cookie handler is responsible for establishing the session and manage authentication session related data. Things to consider: * determine the session lifetime and if the session lifetime should be sliding or absolute * it is recommended to use a cookie name [prefix](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-07#section-4.1.3) if compatible with your application. The BFF validates at startup that cookies using the `__Host-` or `__Secure-` prefix are configured consistently with [RFC 6265bis](https://tools.ietf.org/html/draft-ietf-httpbis-rfc6265bis-07#section-4.1.3) requirements (e.g., `__Host-` requires `SecurePolicy` set to `Always` or `SameAsRequest`, no `Domain`, and `Path` set to `"/"`) * use the highest available `SameSite` mode that is compatible with your application, e.g. `strict`, but at least `lax` ```csharp builder.Services.AddAuthentication().AddCookie("cookie", options => { // set session lifetime options.ExpireTimeSpan = TimeSpan.FromHours(8); // sliding or absolute options.SlidingExpiration = false; // host prefixed cookie name options.Cookie.Name = "__Host-spa"; // strict SameSite handling options.Cookie.SameSite = SameSiteMode.Strict; }); ``` ### Choosing Between SameSite.Lax and SameSite.Strict [Section titled “Choosing Between SameSite.Lax and SameSite.Strict”](#choosing-between-samesitelax-and-samesitestrict) The [SameSite cookie](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie#samesitesamesite-value) is a feature of modern browsers that restricts cookies so that they are only sent to pages originating from the [site](https://developer.mozilla.org/en-US/docs/Glossary/Site) where the cookie was originally issued. This prevents CSRF attacks and helps with improving privacy, because cross-site requests will no longer implicitly include the user’s credentials. If you configure `SameSiteMode.Strict`, this means that if a user originates from an external site and is redirected or linked to the BFF application, then the authentication cookie is not sent automatically. So, the application will consider the user to be not logged in, even though there may be a valid authentication cookie in the cookie jar. If the user refreshes the page, or visits a link on your site that forces a complete page reload, then the authentication cookie will be sent along normally again. This also happens when you have an identity provider that’s hosted on a different site than the BFF, in combination with `SameSiteMode.Strict`. After successful authentication at the IdP, the user will be redirected back to the BFF site. The server will then place an authentication cookie in the browser, but the browser will not automatically include it in subsequent requests until the full page is manually reloaded by the user. This means the user appears to still be logged out, even though the cookie is there. So, if you have an Identity Provider that’s hosted under a different site than your BFF, you may want to configure your cookie policy to be `SameSiteMode.Lax`. Note Chrome will make an exception for cookies set without a `SameSite` attribute less than 2 minutes ago. Such cookies will also be sent with non-idempotent (e.g. POST) top-level cross-site requests despite normal `SameSite=Lax` cookies requiring top-level cross-site requests to have a safe (e.g. GET) HTTP method. Support for this intervention (“Lax + POST”) will be removed in the future. (source: [chromestatus](https://chromestatus.com/feature/5088147346030592)) ----- # BFF Session Management Endpoints > Overview of Duende.BFF endpoints for session management operations including login, logout, and user information retrieval Duende.BFF adds endpoints for performing typical session-management operations such as triggering login and logout and getting information about the currently logged-on user. These endpoint are meant to be called by the frontend. In addition, Duende.BFF adds an implementation of the OpenID Connect back-channel notification endpoint to overcome the restrictions of third party cookies in front-channel notification in modern browsers. You enable the endpoints by adding the relevant services into the ASP.NET Core service provider: Program.cs ```csharp // Add BFF services to DI - also add server-side session management builder.Services.AddBff(options => { // default value options.ManagementBasePath = "/bff"; }; ``` Starting with BFF v4, the BFF automatically wires up the management endpoints. If you disable this behavior (using `AutomaticallyRegisterBffMiddleware`), this is how you can map the management endpoints: Program.cs ```csharp var app = builder.Build(); // Preprocessing pipeline, which would have been automatically added to start of the request the pipeline. app.UseBffPreProcessing(); // Your logic, such as: app.UseRouting(); app.UseBff(); // post processing pipeline that would have been automatically added to the end of the request pipeline. app.UseBffPostProcessing(); app.Run(); ``` The `UsePreprocessing` method adds all handling for multiple frontend support. Alternatively, you can call these methods direct: ```csharp app.UseBffFrontendSelection(); app.UseBffPathMapping(); app.UseBffOpenIdCallbacks(); ``` `UseBffPostProcessing` adds all BFF management endpoints and handlers for proxying `index.html`. You can also map each endpoint individually by calling the various `MapBffManagementXxxEndpoint` methods, for example `endpoints.MapBffManagementLoginEndpoint()`. The following pages describe the default behavior of the management endpoints. See the [extensibility](/bff/extensibility) section for information about how to customize the behavior of the endpoints. Note In V3 and below, only the method `MapBffManagementEndpoints` exists. ----- # BFF Back-Channel Logout Endpoint > Documentation for the OpenID Connect Back-Channel Logout endpoint implementation in BFF, enabling server-to-server session termination without browser involvement. The */bff/backchannel* endpoint is an implementation of the [OpenID Connect Back-Channel Logout](https://openid.net/specs/openid-connect-backchannel-1_0.html) specification. The remote identity provider can use this endpoint to end the BFF’s session via a server to server call, without involving the user’s browser. This design avoids problems with 3rd party cookies associated with front-channel logout. ## Typical Usage [Section titled “Typical Usage”](#typical-usage) The back-channel logout endpoint is invoked by the remote identity provider when it determines that sessions should be ended. IdentityServer will send back-channel logout requests if you [configure](/identityserver/reference/v8/models/client/#authentication--session-management) your client’s `BackChannelLogoutUri`. When a session ends at IdentityServer, any client that was participating in that session that has a back-channel logout URI configured will be sent a back-channel logout request. This typically happens when another application signs out. [Expiration](/identityserver/ui/server-side-sessions/session-expiration/) of [IdentityServer server side sessions](/identityserver/ui/server-side-sessions/) can also be configured to send back-channel logout requests, though this is disabled by default. ## Dependencies [Section titled “Dependencies”](#dependencies) The back-channel logout endpoint depends on [server-side sessions in the BFF](/bff/fundamentals/session/server-side-sessions/), which must be enabled to use this endpoint. Note that such server-side sessions are distinct from server-side sessions in IdentityServer. ## Revoke All Sessions [Section titled “Revoke All Sessions”](#revoke-all-sessions) Back-channel logout tokens include a sub (subject ID) and sid (session ID) claim to describe which session should be revoked. By default, the back-channel logout endpoint will only revoke the specific session for the given subject ID and session ID. Alternatively, you can configure the endpoint to revoke every session that belongs to the given subject ID by setting the `BackchannelLogoutAllUserSessions` [option](/bff/fundamentals/options/#session-management) to true. ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom request processing logic or customize session revocation behavior, see [Back-Channel Logout Endpoint Extensibility](/bff/extensibility/management/back-channel-logout/). ----- # BFF Diagnostics Endpoint > Learn about the BFF diagnostics endpoint that provides access to user and client access tokens for development testing purposes. Note This endpoint is only enabled in `Development` mode. The `/bff/diagnostics` endpoint returns the current user and client access token for testing purposes. The endpoint tries to retrieve and show current tokens. It may invoke both a refresh token flow for the user access token and a client credential flow for the client access token. To use the diagnostics endpoint, make a `GET` request to `/bff/diagnostics`. Typically, this is done in a browser to diagnose a problem during development. ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom logic to the diagnostics endpoint, see [Diagnostics Endpoint Extensibility](/bff/extensibility/management/diagnostics/). ----- # BFF Login Endpoint > Learn how to initiate authentication and handle return URLs using the BFF login endpoint in your frontend applications The */bff/login* endpoint begins the authentication process. To use it, typically javascript code will navigate away from the frontend application to the login endpoint: ```js window.location = "/login"; ``` In Blazor, instead use the `NavigationManager` to navigate to the login endpoint: ```csharp Navigation.NavigateTo($"bff/login", forceLoad: true); ``` The login endpoint triggers an authentication challenge using the default challenge scheme, which will typically use the OpenID Connect [handler](/bff/fundamentals/session/handlers/). ## Return Url [Section titled “Return Url”](#return-url) After authentication is complete, the login endpoint will redirect back to your front end application. By default, this redirect goes to the root of the application. You can use a different URL instead by including a local URL as the *returnUrl* query parameter. ```js window.location = "/login?returnUrl=/logged-in"; ``` ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom logic before or after the login endpoint processes a request, see [Login Endpoint Extensibility](/bff/extensibility/management/login/). ----- # BFF Logout Endpoint > Learn how to use the BFF logout endpoint to sign out users and handle CSRF protection in your application The */bff/logout* endpoint signs out of the appropriate ASP.NET Core [authentication schemes](/bff/fundamentals/session/handlers/) to both delete the BFF’s session cookie and to sign out from the remote identity provider. To use the logout endpoint, typically your javascript code will navigate away from your front end to the logout endpoint, similar to the login endpoint. However, unlike the login endpoint, the logout endpoint requires CSRF protection, otherwise an attacker could destroy sessions by making cross-site GET requests. The session id is used to provide this CSRF protection by requiring it as a query parameter to the logout endpoint (assuming that a session id was included during login). For convenience, the correct logout url is made available as a claim in the */bff/user* endpoint, making typical logout usage look like this: ```js var logoutUrl = userClaims["bff:logout_url"]; // assumes userClaims is the result of a call to /bff/user window.location = logoutUrl; ``` ## Return Url [Section titled “Return Url”](#return-url) After signout is complete, the logout endpoint will redirect back to your front end application. By default, this redirect goes to the root of the application. You can use a different URL instead by including a local URL as the *returnUrl* query parameter. ```js var logoutUrl = userClaims["bff:logout_url"]; window.location = `${logoutUrl}&returnUrl=/logged-out`; ``` ## Revocation Of Refresh Tokens [Section titled “Revocation Of Refresh Tokens”](#revocation-of-refresh-tokens) If the user has a refresh token, the logout endpoint can revoke it. This is enabled by default because revoking refresh tokens that will not be used anymore is generally good practice. Normally any refresh tokens associated with the current session won’t be used after logout, as the session where they are stored is deleted as part of logout. However, you can disable this revocation with the `RevokeRefreshTokenOnLogout` option. ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom logic before or after the logout endpoint processes a request, see [Logout Endpoint Extensibility](/bff/extensibility/management/logout/). ----- # BFF Silent Login Endpoint > Endpoint for non-interactive authentication using an existing session at the remote identity provider Note Deprecated. See [OIDC Prompt support](/bff/fundamentals/session/oidc-prompts/) instead. **Added in v1.2.0.** The */bff/silent-login* endpoint triggers authentication similarly to the login endpoint, but in a non-interactive way. The expected usage pattern is that the application code loads in the browser and triggers a request to the *User Endpoint*. If that indicates that there is no BFF session, then the *Silent Login Endpoint* can be requested to attempt to automatically log the user in, using an existing session at the remote identity provider. This non-interactive design relies upon the use of an *iframe* to make the silent login request. The result of the silent login request in the *iframe* will then use *postMessage* to notify the parent window of the outcome. If the result is that a session has been established, then the application logic can either re-trigger a call to the *User Endpoint*, or reload the entire page (depending on the preferred design). If the result is that a session has not been established, then the application redirects to the login endpoint to log the user in interactively. To trigger the silent login, the application code must have an *iframe* and then set its *src* to the silent login endpoint. For example in your HTML: ```html ``` And then in JavaScript: ```javascript document.querySelector('#bff-silent-login').src = '/bff/silent-login'; ``` Tip In BFF v4, set the iframe’s src attribute to the login endpoint instead using the query parameter `prompt=none`: ```javascript document.querySelector('#bff-silent-login').src = '/bff/login?prompt=none'; ``` See [OIDC Prompt support](/bff/fundamentals/session/oidc-prompts/) for additional information. To receive the result, the application should handle the *message* event in the browser and look for the *data.isLoggedIn* property on the event object: ```javascript window.addEventListener("message", e => { if (e.data && e.data.source === 'bff-silent-login' && e.data.isLoggedIn) { // we now have a user logged in silently, so reload this window window.location.reload(); } }); ``` ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom logic to the silent login endpoint, see [Silent Login Endpoint Extensibility](/bff/extensibility/management/silent-login/). ----- # BFF User Endpoint > The BFF user endpoint provides information about the currently authenticated user and their session status The */bff/user* endpoint returns data about the currently logged-on user and the session. It is typically invoked at application startup to check if the user has authenticated, and if so, to get profile data about the user. It can also be used to periodically query if the session is still valid. ## Output [Section titled “Output”](#output) If there is no current session, the user endpoint returns a response indicating that the user is anonymous. By default, this is a 401 status code, but this can be [configured](#anonymous-session-response-option). If there is a current session, the user endpoint returns a JSON array containing the claims in the ASP.NET Core authentication session and several BFF specific claims. For example: ```json [ { "type": "sid", "value": "173E788068FFB728806501F4F46C52D6" }, { "type": "sub", "value": "88421113" }, { "type": "idp", "value": "local" }, { "type": "name", "value": "Bob Smith" }, { "type": "bff:logout_url", "value": "/logout?sid=173E788068FFB728806501F4F46C52D6" }, { "type": "bff:session_expires_in", "value": 28799 }, { "type": "bff:session_state", "value": "q-Hl1V9a7FCZE5o-vH9qpmyVKOaeVfMQBUJLrq-lDJU.013E58C33C7409C6011011B8291EF78A" } ] ``` ## User Claims [Section titled “User Claims”](#user-claims) Since the user endpoint returns the claims that are in the ASP.NET Core session, anything that changes the session will be reflected in its output. You can customize the contents of the session via the OpenID Connect handler’s [ClaimAction](https://docs.microsoft.com/en-us/dotnet/API/microsoft.aspnetcore.authentication.claimactioncollectionmapextensions?view=aspnetcore-7.0) infrastructure, or by using [claims transformation](https://docs.microsoft.com/en-us/dotnet/API/microsoft.aspnetcore.authentication.iclaimstransformation?view=aspnetcore-7.0). For example, if you add a [claim](/identityserver/fundamentals/claims/) to the [userinfo endpoint](/identityserver/reference/v8/endpoints/userinfo/) at IdentityServer that you would like to include in the */bff/user* endpoint, you need to add a corresponding ClaimAction in the BFF’s OpenID Connect Handler to include the claim in the BFF’s session. ## Management Claims [Section titled “Management Claims”](#management-claims) In addition to the claims in the ASP.NET Core Session, Duende.BFF adds three additional claims: **bff:session\_expires\_in** This is the number of seconds the current session will be valid for. **bff:session\_state** This is the session state value of the upstream OIDC provider that can be used for the JavaScript *check\_session* mechanism (if provided). **bff:logout\_url** This is the URL to trigger logout. If the upstream provider includes a `sid` claim, the BFF logout endpoint requires this value as a query string parameter for CSRF protection. This behavior can be configured with the `RequireLogoutSessionId` in the [options](/bff/fundamentals/options/). ## Typical Usage [Section titled “Typical Usage”](#typical-usage) To use the endpoint, make an HTTP GET request to it from your frontend javascript code. For example, your application could use the fetch API to make requests to the user endpoint like this: session.js ```js var req = new Request("/bff/user", { headers: new Headers({ "X-CSRF": "1", }), }); var resp = await fetch(req); if (resp.ok) { userClaims = await resp.json(); console.log("user logged in", userClaims); } else if (resp.status === 401) { console.log("user not logged in"); } ``` ## Cross-Site Request Forgery [Section titled “Cross-Site Request Forgery”](#cross-site-request-forgery) To protect against cross-site request forgery, you need to add a [static header](https://cheatsheetseries.owasp.org/cheatsheets/Cross-Site_Request_Forgery_Prevention_Cheat_Sheet.html#use-of-custom-request-headers) to the GET request. The header’s name and required value can be configured in the [options](/bff/fundamentals/options/). ## Anonymous Session Response Option [Section titled “Anonymous Session Response Option”](#anonymous-session-response-option) The `AnonymousSessionResponse` option allows you to change the behavior of the user endpoint to return 200 instead of 401 when the user is anonymous. If `AnonymousSessionResponse` is set to `AnonymousSessionResponse.Response200`, then the endpoint’s response will set its status code to 200 and its payload will contain the literal `null` (the response body will be the characters ‘n’, ‘u’, ‘l’, ‘l’ without quotes). ## Cookie Sliding [Section titled “Cookie Sliding”](#cookie-sliding) Note The cookie sliding prevention feature requires either usage of server-side sessions or .NET 6 or higher (or both). If your ASP.NET Core session cookie is configured to use a sliding expiration, you need to be able to query the session state without extending the session’s lifetime; a periodic check for user activity shouldn’t itself count as user activity. To prevent the call to the user endpoint from sliding the cookie, add the *slide=false* parameter to the request. site.js ```js var req = new Request("/bff/user?slide=false", { headers: new Headers({ "X-CSRF": "1", }), }); ``` ## Customize This Endpoint [Section titled “Customize This Endpoint”](#customize-this-endpoint) To add custom logic, enrich user claims, or change the claims returned by this endpoint, see [User Endpoint Extensibility](/bff/extensibility/management/user/). ----- # OpenID Connect Prompts > Learn how to use OpenID Connect prompt parameters (login, consent, select_account, none) in Duende BFF v4 for step-up authentication and enhanced security flows. OpenID Connect supports a `prompt` parameter that can be used to control the user experience as it relates to the current authentication session. Duende BFF v4 supports this parameter by forwarding it to the backing identity provider to allow for more fine-grained control during unique client interactions. This documentation outlines the `prompt` parameter support and what values you might use to achieve different outcomes. ## Prompt parameter options [Section titled “Prompt parameter options”](#prompt-parameter-options) The [OpenID Connect specification](https://openid.net/specs/openid-connect-core-1_0.html) defines an **optional** `prompt` parameter that can be used to control the user experience as it relates to the current authentication session. The following values are supported: | value | description | | ---------------- | ------------------------------------------------------------------------------------------------- | | `none` | Must not display any authentication or consent user interface | | `login` | Should prompt the user to reauthenticate | | `consent` | Should prompt the user for consent | | `select_account` | Should prompt user to choose an account given their are multiple accounts for the current session | These values can be passed to the BFF by adding them to the `prompt` query parameter to the login request URL. For example, the following request would prompt the user to reauthenticate: ```http /bff/login?prompt=login ``` The inclusion of the `prompt` parameter in the login request URL will cause the BFF to forward it to the backing identity provider at which point the identity provider will determine the appropriate user experience based on the value of the `prompt` parameter. For example, if the `prompt` parameter is set to `login`, the identity provider will prompt the user to reauthenticate. Note Be aware that the exact behavior of the `prompt` parameter is not defined by the OpenID Connect specification and may vary between identity providers. Consult the documentation for your identity provider for more information. ## Scenarios and Situations [Section titled “Scenarios and Situations”](#scenarios-and-situations) The `prompt` parameter can be used in situations where additional security is required, you want to reestablish the account identity, or a high-impact action is about to be taken. For example, the following hypothetical scenarios might require the use of the `prompt` parameter: * Attempting to transfer funds from a bank account to another * A destructive action such as deleting an account * Performing an action that alters a high-value account setting such as an email address ## Silent Login Deprecation (v3 to v4) [Section titled “Silent Login Deprecation (v3 to v4)”](#silent-login-deprecation-v3-to-v4) When migrating from Duende BFF v3 to v4, you may notice deprecation warnings regarding the [silent login](/bff/fundamentals/session/management/silent-login/) feature located at the management endpoint `/silent-login`. To resolve the warning, update the silent login URL in your frontend applications to point to the login endpoint instead, including the `prompt=none` query parameter: ```diff const silentLoginPath = '/bff/silent-login'; const silentLoginPath = '/bff/login?prompt=none'; ``` By default, BFF v4 [automatically registers the management endpoints](/bff/fundamentals/session/management/). In case you opted out of the automatic registration feature, you may still need to explicitly call `app.MapBffManagementSilentLoginEndpoints()` if you are manually mapping the management endpoints. ----- # Server-Side Sessions > Learn how to implement and configure server-side sessions in BFF to manage user session data storage and enable session revocation capabilities By default, ASP.NET Core’s cookie handler will store all user session data in a protected cookie. This works very well unless cookie size or revocation becomes an issue. Duende.BFF includes all the plumbing to store your sessions server-side. The cookie will then only be used to transmit the session ID between the browser and the BFF host. This has the following advantages * the cookie size will be very small and constant - regardless how much data (e.g. token or claims) is stored in the authentication session * the session can be also revoked outside the context of a browser interaction, for example when receiving a back-channel logout notification from the upstream OpenID Connect provider ## Configuring Server-Side Sessions [Section titled “Configuring Server-Side Sessions”](#configuring-server-side-sessions) Server-side sessions can be enabled in the application’s startup: ```csharp builder.Services.AddBff() .AddServerSideSessions(); ``` The default implementation stores the session in-memory. This is useful for testing, but for production you typically want a more robust storage mechanism. We provide an implementation of the session store built with EntityFramework (EF) that can be used with any database with an EF provider (e.g. Microsoft SQL Server). You can also use a custom store. See [extensibility](/bff/extensibility/sessions/#user-session-store) for more information. In-memory store limitations Even with the in-memory store, tokens (including refresh tokens) are never placed in the cookie. The cookie always contains only the session ID. However, the in-memory store does not survive process restarts and is not shared across instances. Sessions will be lost when the process restarts or when traffic is routed to a different instance in a load-balanced deployment. It is recommended to use a persistent store in multi-node BFF deployments. ## Using Entity Framework for the Server-Side Session Store [Section titled “Using Entity Framework for the Server-Side Session Store”](#using-entity-framework-for-the-server-side-session-store) To use the EF session store, install the `Duende.BFF.EntityFramework` NuGet package: ```bash dotnet add package Duende.BFF.EntityFramework ``` Next, you can register the session store by calling `AddEntityFrameworkServerSideSessions`, like this: ```csharp var cn = _configuration.GetConnectionString("db"); builder.Services.AddBff() .AddEntityFrameworkServerSideSessions(options=> { options.UseSqlServer(cn); }); ``` The method of `AddEntityFrameworkServerSideSessions` registers the `SessionDbContext` along with a `UserSessionStore` as transient dependencies. For developers looking to take advantage of DbContext pooling or have more fine-grained control over their DbContext creation registration and creation process, you can use the `AddEntityFrameworkServerSideSessionsServices` method instead. This method registers all the required services for server-side session except for the `SessionDbContext`, which will now be managed by the DbContext pooling mechanism. ```csharp var cn = _configuration.GetConnectionString("db"); builder.Services.AddDbContextPool(opt => { // configure your db context pool options here options.UseSqlServer(cn); }); builder.Services.AddBff() .AddEntityFrameworkServerSideSessionsServices() ``` Note, you’ll still need to let the server side session store know about the `SessionDbContext` by calling `AddEntityFrameworkServerSideSessions` with the `SessionDbContext` implementation as a generic argument. ### Entity Framework Migrations [Section titled “Entity Framework Migrations”](#entity-framework-migrations) Most data stores that you might use with Entity Framework use a schema to define the structure of their data. `Duende.BFF.EntityFramework` doesn’t make any assumptions about the underlying datastore, how (or indeed even if) it defines its schema, or how schema changes are managed by your organization. For these reasons, Duende does not directly support database creation, schema changes, or data migration by publishing database scripts. You are expected to manage your database in the way your organization sees fit. Using EF migrations is one possible approach to that, which Duende facilitates by publishing entity classes in each version of `Duende.BFF.EntityFramework`. An example project that uses those entities to create migrations is [here](https://github.com/DuendeSoftware/products/tree/main/bff/migrations/UserSessionDb). To quickly create Entity Framework migrations, run the following command in the project directory that has access to Entity Framework Core’s tools: ```bash dotnet ef migrations add UserSessions -o Migrations -c SessionDbContext ``` The project must also reference the `Duende.BFF.EntityFramework` NuGet package and the `Microsoft.EntityFrameworkCore.Design` NuGet package, along with a specific database provider and its corresponding configuration, including the connection string. ## Session Store Cleanup [Section titled “Session Store Cleanup”](#session-store-cleanup) Added in v1.2.0. Abandoned sessions will remain in the store unless something removes the stale entries. * V4 If you wish to have such sessions cleaned up periodically, then you can add the session cleanup host and configure the `SessionCleanupInterval` options: Program.cs ```csharp builder.Services.AddBff(options => { options.SessionCleanupInterval = TimeSpan.FromMinutes(5); }) .AddServerSideSessions(); ``` This requires an implementation of [`IUserSessionStoreCleanup`](/bff/extensibility/sessions#user-session-store-cleanup) in the ASP.NET Core service provider. If using Entity Framework Core, then the `IUserSessionStoreCleanup` implementation is provided for you when you use `AddEntityFrameworkServerSideSessions`. You can then add the `SessionCleanupBackgroundProcess`: Program.cs ```csharp var cn = _configuration.GetConnectionString("db"); builder.Services.AddBff() .AddEntityFrameworkServerSideSessions(options => { options.UseSqlServer(cn); }) .AddSessionCleanupBackgroundProcess(); ``` Note In V4, we changed how you enable session cleanup. We no longer automatically register the session cleanup hosted service. This has to be done manually. In a load-balanced environment, you can choose to run the cleanup job on all instances the BFF. However, you can also decide to spin up a separate host that’s responsible for background jobs such as this cleanup job. * V3 If you wish to have such sessions cleaned up periodically, then you can configure the `EnableSessionCleanup` and `SessionCleanupInterval` options: Program.cs ```csharp builder.Services.AddBff(options => { options.EnableSessionCleanup = true; options.SessionCleanupInterval = TimeSpan.FromMinutes(5); }) .AddServerSideSessions(); ``` This requires an implementation of [`IUserSessionStoreCleanup`](/bff/extensibility/sessions#user-session-store-cleanup) in the ASP.NET Core service provider. If using Entity Framework Core, then the `IUserSessionStoreCleanup` implementation is provided for you when you use `AddEntityFrameworkServerSideSessions`. Just enable session cleanup: Program.cs ```csharp var cn = _configuration.GetConnectionString("db"); builder.Services.AddBff(options => { options.EnableSessionCleanup = true; }) .AddEntityFrameworkServerSideSessions(options => { options.UseSqlServer(cn); }); ``` ----- # Token Management > Learn how to manage and utilize access tokens in BFF applications for secure API communication Duende.BFF includes an automatic token management feature. This uses the access and refresh token stored in the authentication session to always provide a current access token for outgoing API calls. For most scenarios, there is no additional configuration necessary. The token management will infer the configuration and token endpoint URL from the metadata of the OpenID Connect provider. The easiest way to retrieve the current access token is to use an extension method on `HttpContext`: ```csharp var token = await HttpContext.GetUserAccessTokenAsync(); ``` You can then use the token to set it on an `HttpClient`instance: ```csharp var client = new HttpClient(); client.SetBearerToken(token); ``` We recommend to use the `HttpClientFactory` to create HTTP clients that are already aware of the token management plumbing. For this you would register a named client in your application startup e.g. like this: Program.cs ```csharp // registers HTTP client that uses the managed user access token builder.Services.AddUserAccessTokenHttpClient("apiClient", configureClient: client => { client.BaseAddress = new Uri("https://remoteServer/"); }); ``` And then retrieve a client instance like this: ```csharp app.MapGet("/myApi", async (IHttpClientFactory httpClientFactory, HttpContext context) => { // create HTTP client with automatic token management var client = httpClientFactory.CreateClient("apiClient"); // call remote API var response = await client.GetAsync("remoteApi"); // rest omitted }); ``` If you prefer to use typed clients, you can do that as well: ```csharp // registers a typed HTTP client with token management support services.AddHttpClient(client => { client.BaseAddress = new Uri("https://remoteServer/"); }).AddUserAccessTokenHandler(); ``` And then use that client, for example like this on an endpoint: ```csharp app.MapGet("/myApi", async (MyTypedClient client) => { var response = await client.GetData(); // rest omitted }); ``` The client will internally always try to use a current and valid access token. If for any reason this is not possible, the 401 status code will be returned to the caller. ### Reuse of Refresh Tokens [Section titled “Reuse of Refresh Tokens”](#reuse-of-refresh-tokens) We recommend that you configure IdentityServer to issue reusable refresh tokens to BFF clients. Because the BFF is a confidential client, it does not need one-time use refresh tokens. Reusable refresh tokens are desirable because they avoid performance and user experience problems associated with one time use tokens. See the discussion on [rotating refresh tokens](/identityserver/tokens/refresh/) and the [OAuth 2.0 Security Best Current Practice](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-security-topics#section-2.2.2) for more details. ### Manually revoking refresh tokens [Section titled “Manually revoking refresh tokens”](#manually-revoking-refresh-tokens) Duende.BFF revokes refresh tokens automatically at logout time. This behavior can be disabled with the *RevokeRefreshTokenOnLogout* option. If you want to manually revoke the current refresh token, you can use the following code: ```csharp await HttpContext.RevokeUserRefreshTokenAsync(); ``` This will invalidate the refresh token at the token service. ## See Also [Section titled “See Also”](#see-also) * [Access Token Management](/accesstokenmanagement/) — The `Duende.AccessTokenManagement` library that powers BFF token refresh * [User Token Management](/accesstokenmanagement/web-apps/) — Detailed user token lifecycle documentation * [Client Credential Tokens](/accesstokenmanagement/workers/) — Machine-to-machine token management * [IdentityServer Refresh Tokens](/identityserver/tokens/refresh/) — Configuring refresh token rotation and reuse * [IdentityServer Client Configuration](/identityserver/configuration/dcr/) — Setting up confidential BFF clients * [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) — Where tokens are stored server-side ----- # Getting started > Get started with Duende BFF Security Framework. Choose from single frontend, multi-frontend, or Blazor quickstart guides to secure your browser-based applications. Currently, the most recent version is v4. If you’re upgrading from a previous version, please check our [upgrade guides](/bff/upgrading). If you’re starting a new BFF project, consider the following startup guides: * [Single frontend BFF](/bff/getting-started/single-frontend/) * [Multi-frontend BFF](/bff/getting-started/multi-frontend/) * [Blazor](/bff/getting-started/blazor/) ## Applying the Duende Backend for Frontend (BFF) Security Framework [Section titled “Applying the Duende Backend for Frontend (BFF) Security Framework”](#applying-the-duende-backend-for-frontend-bff-security-framework) [YouTube video player](https://www.youtube.com/embed/6zMSwlGBmxs) ----- # Blazor Applications > A walkthrough showing how to set up and configure a BFF (Backend For Frontend) application using Blazor This quickstart walks you through how to create a BFF Blazor application. The source code for this quickstart is available on [GitHub](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/BlazorAutoRendering). Version This guide targets **Duende BFF v4**. If you are still on v3, expand the v3 tabs in each step below. See the [v3 → v4 upgrade guide](/bff/upgrading/bff-v3-to-v4/) when you are ready to migrate. ## What You’ll Build [Section titled “What You’ll Build”](#what-youll-build) By the end of this guide you will have a Blazor application (Server + WASM) that authenticates users via OpenID Connect, stores session state server-side through the BFF, and calls a weather API using a BFF-managed HTTP client — with no access tokens exposed to the browser. Prerequisites * .NET 8.0 SDK or later * Familiarity with Blazor’s hosting models (Server vs. WASM) * An OpenID Connect-compatible identity provider (e.g., [Duende IdentityServer](/identityserver/), Auth0, Azure AD) ## Creating the project structure [Section titled “Creating the project structure”](#creating-the-project-structure) 1. **Create a Blazor App** ```shell mkdir BlazorBffApp cd BlazorBffApp dotnet new blazor --interactivity auto --all-interactive ``` This creates a Blazor application with a Server project and a client project. 2. **Configure the BffApp Server Project** To configure the server, the first step is to add the BFF Blazor package. ```shell cd BlazorBffApp dotnet add package Duende.BFF.Blazor ``` Then configure the application to use BFF. Add this to your services: * Duende BFF v4 ```csharp // BFF setup for Blazor builder.Services.AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }) .ConfigureCookies(options => { // Because we use an identity server that's configured on a different site // (duendesoftware.com vs localhost), we need to configure the SameSite property to Lax. // Setting it to Strict would cause the authentication cookie not to be sent after logging in. // The user would have to refresh the page to get the cookie. // Recommendation: Set it to 'strict' if your IDP is on the same site as your BFF. options.Cookie.SameSite = SameSiteMode.Lax; }) .AddServerSideSessions() // Add in-memory implementation of server-side sessions .AddBlazorServer(); // Make sure authentication state is available to all components. builder.Services.AddCascadingAuthenticationState(); builder.Services.AddAuthorization(); ``` * Duende BFF v3 ```csharp // BFF setup for Blazor (v3) builder.Services.AddBff() .AddServerSideSessions() // Add in-memory implementation of server-side sessions .AddBlazorServer(); // Configure the authentication builder.Services .AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { // Configure the cookie with __Host prefix for maximum security options.Cookie.Name = "__Host-blazor"; // Because we use an identity server that's configured on a different site // (duendesoftware.com vs localhost), we need to configure the SameSite property to Lax. // Setting it to Strict would cause the authentication cookie not to be sent after logging in. // The user would have to refresh the page to get the cookie. // Recommendation: Set it to 'strict' if your IDP is on the same site as your BFF. options.Cookie.SameSite = SameSiteMode.Lax; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }); // Make sure authentication state is available to all components. builder.Services.AddCascadingAuthenticationState(); builder.Services.AddAuthorization(); ``` To configure the web app pipeline, add the following after `builder.Build()`: * Duende BFF v4 ```csharp app.UseRouting(); app.UseAuthentication(); // Add the BFF middleware which performs anti-forgery protection app.UseBff(); app.UseAuthorization(); app.UseAntiforgery(); // In v4, management endpoints (/bff/login, /bff/logout, etc.) are // registered automatically — no call to MapBffManagementEndpoints() needed. ``` * Duende BFF v3 ```csharp app.UseRouting(); app.UseAuthentication(); // Add the BFF middleware which performs anti-forgery protection app.UseBff(); app.UseAuthorization(); app.UseAntiforgery(); // In v3, management endpoints must be registered explicitly app.MapBffManagementEndpoints(); ``` ## Configuring the BffApp.Client project [Section titled “Configuring the BffApp.Client project”](#configuring-the-bffappclient-project) 1. **Configure the Client Project** To add the BFF to the client project, add the following: ```shell cd .. cd BlazorBffApp.Client dotnet add package Duende.BFF.Blazor.Client ``` Then add the following to your `Program.cs`: ```csharp builder.Services .AddBffBlazorClient(); // Provides auth state provider that polls the /bff/user endpoint builder.Services .AddCascadingAuthenticationState(); ``` Your application is ready to use BFF now. ## Configuring your application to use BFF’s features [Section titled “Configuring your application to use BFF’s features”](#configuring-your-application-to-use-bffs-features) Add the following components to your `BlazorBffApp.Client/Components` folder: 1. **LoginDisplay.razor** The following code shows a login / logout button depending on your authentication state. Note: use the logout link from the `LogoutUrl` claim, because it contains both the correct route and the session id. BlazorBffApp.Client/Components/LoginDisplay.razor ```razor @using Duende.Bff.Blazor.Client @using Microsoft.AspNetCore.Components.Authorization @using Microsoft.Extensions.Options @rendermode InteractiveAuto @inject IOptions Options Hello, @context.User.Identity?.Name Log Out Log in Log in @code { string BffLogoutUrl(AuthenticationState context) { var logoutUrl = context.User.FindFirst(Constants.ClaimTypes.LogoutUrl); return $"{Options.Value.StateProviderBaseAddress}{logoutUrl?.Value}"; } } ``` 2. **RedirectToLogin.razor** The following code will redirect users to the identity provider for authentication. Once authentication is complete, users will be redirected back to where they came from. BlazorBffApp.Client/Components/RedirectToLogin.razor ```razor @inject NavigationManager Navigation @rendermode InteractiveAuto @code { protected override void OnInitialized() { var returnUrl = Uri.EscapeDataString("/" + Navigation.ToBaseRelativePath(Navigation.Uri)); Navigation.NavigateTo($"bff/login?returnUrl={returnUrl}", forceLoad: true); } } ``` 3. **Modifications to Routes.razor** Replace the contents of `Routes.razor` so it matches below: BlazorBffApp.Client/Routes.razor ```razor @using Microsoft.AspNetCore.Components.Authorization @using BlazorBffApp.Client.Components @if (context.User.Identity?.IsAuthenticated != true) { } else {

You (@context.User.Identity?.Name) are not authorized to access this resource.

}
``` This ensures that accessing a page that requires authorization automatically redirects the user to the identity provider for authentication. 4. **Modifications to MainLayout.razor** Modify your `MainLayout.razor` to include the `LoginDisplay`: BlazorBffApp.Client/Layout/MainLayout.razor ```razor @inherits LayoutComponentBase @using BlazorBffApp.Client.Components
@Body
An unhandled error has occurred. Reload 🗙
``` Now your application supports logging in and out. ## Exposing APIs [Section titled “Exposing APIs”](#exposing-apis) Now we’re going to expose an embedded API for weather forecasts to Blazor WebAssembly (WASM) and call it via an `HttpClient`. Note By default, the system will perform both pre-rendering on the server AND WASM-based rendering on the client. For this reason, you’ll need to register both a server and client version of a component that retrieves data. See the [Microsoft documentation](https://learn.microsoft.com/en-us/aspnet/core/blazor/components/render-modes?view=aspnetcore-9.0#client-side-services-fail-to-resolve-during-prerendering) for more information. 1. **Configuring the Client app** Add a class called `WeatherHttpClient` to the `BlazorBffApp.Client` project: BlazorBffApp.Client/WeatherHttpClient.cs ```csharp public class WeatherHttpClient(HttpClient client) : IWeatherClient { public async Task GetWeatherForecasts() => await client.GetFromJsonAsync("WeatherForecast") ?? throw new JsonException("Failed to deserialize"); } public class WeatherForecast { public DateOnly Date { get; set; } public int TemperatureC { get; set; } public string? Summary { get; set; } public int TemperatureF => 32 + (int)(TemperatureC / 0.5556); } // The IWeatherClient interface abstracts between server and client implementations. public interface IWeatherClient { Task GetWeatherForecasts(); } ``` Then register this in `Program.cs`: BlazorBffApp.Client/Program.cs ```csharp builder.Services .AddBffBlazorClient() // Provides auth state provider that polls the /bff/user endpoint // Register an HTTP client configured to fetch data from the BFF host. .AddLocalApiHttpClient(); // Register the concrete implementation with the abstraction builder.Services.AddSingleton(); ``` 2. **Configuring the server** Add a class called `ServerWeatherClient` to your `BlazorBffApp` server project: BlazorBffApp/ServerWeatherClient.cs ```csharp public class ServerWeatherClient : IWeatherClient { public Task GetWeatherForecasts() { var startDate = DateOnly.FromDateTime(DateTime.Now); string[] summaries = [ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching" ]; return Task.FromResult(Enumerable.Range(1, 5).Select(index => new WeatherForecast { Date = startDate.AddDays(index), TemperatureC = Random.Shared.Next(-20, 55), Summary = summaries[Random.Shared.Next(summaries.Length)] }).ToArray()); } } ``` Then add an endpoint to your HTTP pipeline and register the server implementation: BlazorBffApp/Program.cs ```csharp // Register the server-side implementation builder.Services.AddSingleton(); // ... app.MapGet("/WeatherForecast", (IWeatherClient weatherClient) => weatherClient.GetWeatherForecasts()) .RequireAuthorization() .AsBffApiEndpoint(); ``` 3. **Displaying Weather Information From The API** By default, the Blazor template ships with a weather page. Change the content of `Weather.razor` to this: BlazorBffApp.Client/Pages/Weather.razor ```razor @page "/weather" @using BlazorBffApp.Client.Components @using Microsoft.AspNetCore.Authorization @rendermode InteractiveWebAssembly @attribute [Authorize] Weather ``` Now add a `WeatherComponent.razor`: BlazorBffApp.Client/Components/WeatherComponent.razor ```razor @inject IWeatherClient WeatherClient

Weather

This component demonstrates showing data.

@if (forecasts == null) {

Loading...

} else { @foreach (var forecast in forecasts) { }
Date Temp. (C) Temp. (F) Summary
@forecast.Date.ToShortDateString() @forecast.TemperatureC @forecast.TemperatureF @forecast.Summary
} @code { private WeatherForecast[]? forecasts; protected override async Task OnInitializedAsync() { forecasts = await WeatherClient.GetWeatherForecasts(); } } ``` Token availability in Blazor components Access tokens are managed server-side by the BFF host and are never available in Blazor WASM components directly. Always use `AddLocalApiHttpClient()` to create HTTP clients that route through the BFF host — never try to retrieve or pass tokens to client-side components. See the [Troubleshooting guide](/bff/troubleshooting/) if tokens appear unavailable. ## See Also [Section titled “See Also”](#see-also) [Single Frontend Getting Started](/bff/getting-started/single-frontend/)Simpler setup for a single SPA. [Blazor Fundamentals](/bff/fundamentals/blazor/)Rendering modes, data access patterns, and auth state. [Local APIs](/bff/fundamentals/apis/local/)Embedding API endpoints in the BFF host. [Token Management](/bff/fundamentals/tokens/)How BFF handles access token refresh automatically. [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/)Persisting sessions with Entity Framework. [Access Token Management](/accesstokenmanagement/)The underlying token lifecycle library. [Troubleshooting](/bff/troubleshooting/)Common Blazor BFF issues and fixes. ----- # Getting Started - Multiple Frontends > A guide on how to create a BFF application with multiple frontends. Duende.BFF (Backend for Frontend) supports multiple frontends in a single BFF host. This is useful for scenarios where you want to serve several SPAs or frontend apps from the same backend, each with their own authentication and API proxying configuration. ## What You’ll Build [Section titled “What You’ll Build”](#what-youll-build) By the end of this guide you will have a single BFF host that serves multiple frontend applications, each with independently configurable OpenID Connect settings and remote API proxying. Prerequisites * .NET 8.0 SDK or later * Familiarity with the [Single Frontend setup](/bff/getting-started/single-frontend/) * Duende.BFF v4 or later (multi-frontend is a v4+ feature) Multi-frontend is v4+ Multi-frontend support is available in Duende.BFF v4 and later. The v3-style of wiring up BFF is not supported for this scenario. ## Setting Up A BFF Project For Multiple Frontends [Section titled “Setting Up A BFF Project For Multiple Frontends”](#setting-up-a-bff-project-for-multiple-frontends) 1. **Create A New ASP.NET Core Project** Terminal ```bash dotnet new web -n MyMultiBffApp cd MyMultiBffApp ``` 2. **Add The Duende.BFF NuGet Package** Terminal ```bash dotnet add package Duende.BFF ``` 3. **OpenID Connect Configuration** Configure OpenID Connect authentication for your BFF host. This is similar to the single frontend setup, but applies to all frontends unless overridden per frontend. Program.cs ```csharp builder.Services.AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); // Add this scope if you want to receive refresh tokens options.Scope.Add("offline_access"); }) .ConfigureCookies(options => { // Because we use an identity server that's configured on a different site // (duendesoftware.com vs localhost), we need to configure the SameSite property to Lax. // Setting it to Strict would cause the authentication cookie not to be sent after logging in. // The user would have to refresh the page to get the cookie. // Recommendation: Set it to 'strict' if your IDP is on the same site as your BFF. options.Cookie.SameSite = SameSiteMode.Lax; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseRouting(); // adds antiforgery protection for local APIs app.UseBff(); // adds authorization for local and remote API endpoints app.UseAuthorization(); app.Run(); ``` 4. **Configure BFF In `Program.cs`** * Static Register multiple frontends directly in code using `AddFrontends`: Program.cs ```csharp builder.Services.AddBff() .AddFrontends( new BffFrontend(BffFrontendName.Parse("default-frontend")) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/static/index.html")), new BffFrontend(BffFrontendName.Parse("admin-frontend")) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/admin/index.html")) ); // ...existing code for authentication, authorization, etc. ``` * From Config You can also load frontend configuration from an `IConfiguration` source, such as a JSON file: Example `bffconfig.json`: ```json { "defaultOidcSettings": null, "defaultCookieSettings": null, "frontends": { "from_config": { "cdnIndexHtmlUrl": "https://localhost:5005/static/index.html", "matchingPath": "/from-config", "oidc": { "clientId": "bff.multi-frontend.config" }, "remoteApis": [ { "pathMatch": "/api/client-token", "targetUri": "https://localhost:5010", "requiredTokenType": "Client" } ] } } } ``` Load and use the configuration in `Program.cs`: Program.cs ```csharp var bffConfig = new ConfigurationBuilder() .AddJsonFile("bffconfig.json") .Build(); builder.Services.AddBff() .LoadConfiguration(bffConfig); // ...existing code for authentication, authorization, etc. ``` 5. **Remote API Proxying** You can configure remote API proxying in two ways: * **Single YARP proxy for all frontends:** You can set up a single YARP proxy for all frontends, as shown in the [Single Frontend Guide](/bff/getting-started/single-frontend/). * **Direct proxying per frontend:** You can configure remote APIs for each frontend individually: Program.cs ```csharp builder.Services.AddBff() .AddFrontends( new BffFrontend(BffFrontendName.Parse("default-frontend")) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/static/index.html")) .WithRemoteApis( new RemoteApi("/api/user-token", new Uri("https://localhost:5010")) ) ); ``` This allows each frontend to have its own set of proxied remote APIs. 6. **Server Side Sessions** Server side session configuration is the same as in the single frontend scenario. See the [Single Frontend Guide](/bff/getting-started/single-frontend/) for details. ## See Also [Section titled “See Also”](#see-also) [Single Frontend Getting Started](/bff/getting-started/single-frontend/)Simpler BFF setup for one frontend. [Multi-Frontend Fundamentals](/bff/fundamentals/multi-frontend/)Deep-dive into multi-frontend configuration. [Remote APIs](/bff/fundamentals/apis/remote/)Proxying calls to upstream services. [YARP Integration](/bff/fundamentals/apis/yarp/)Advanced proxy configuration. [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/)Persisting sessions in production. [Access Token Management](/accesstokenmanagement/)Token lifecycle managed by BFF. ----- # Getting Started - Single Frontend > A guide on how to create a BFF application with a single frontend. Duende.BFF (Backend for Frontend) is a library that helps you build secure, modern web applications by acting as a security gateway between your frontend and backend APIs. This guide will walk you through setting up a simple BFF application with a single frontend. ## What You’ll Build [Section titled “What You’ll Build”](#what-youll-build) By the end of this guide you will have an ASP.NET Core host that: * Authenticates users via OpenID Connect and stores the session server-side * Exposes secure local API endpoints with CSRF protection * Optionally proxies remote API calls with automatic token attachment Prerequisites * .NET 8.0 SDK or later * A frontend application (e.g., React, Angular, Vue, or plain JavaScript) * An OpenID Connect-compatible identity provider (e.g., [Duende IdentityServer](/identityserver/), Auth0, Azure AD) BFF v4 default frontend Duende.BFF V4 introduced a new way of configuring the BFF, which automatically configures the BFF using recommended practices. If you’re upgrading from V3, please refer to the [upgrade guide](/bff/upgrading/bff-v3-to-v4/). When in single frontend mode, an implicit default frontend is automatically registered. This ensures all the management routes and OpenID Connect-handling routes are available for your frontend. When you call `.AddFrontend()` to add a new frontend, the system switches to multi-frontend mode. If you wish to have a default frontend in multi-frontend mode, you’ll need to explicitly add it. See [multi-frontend support](/bff/fundamentals/multi-frontend/) for more information on this topic. ## Setting Up A BFF project [Section titled “Setting Up A BFF project”](#setting-up-a-bff-project) 1. **Create A New ASP.NET Core Project** Create a new ASP.NET Core Web Application: ```sh dotnet new web -n MyBffApp cd MyBffApp ``` 2. **Add The Duende.BFF NuGet Package** Install the Duende.BFF package: ```sh dotnet add package Duende.BFF ``` 3. **Configure BFF In `Program.cs`** Add the following to your `Program.cs`: * Duende BFF v4 ```csharp builder.Services.AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); // Add this scope if you want to receive refresh tokens options.Scope.Add("offline_access"); }) .ConfigureCookies(options => { // Because we use an identity server that's configured on a different site // (duendesoftware.com vs localhost), we need to configure the SameSite property to Lax. // Setting it to Strict would cause the authentication cookie not to be sent after logging in. // The user would have to refresh the page to get the cookie. // Recommendation: Set it to 'strict' if your IDP is on the same site as your BFF. options.Cookie.SameSite = SameSiteMode.Lax; }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseRouting(); // adds antiforgery protection for local APIs app.UseBff(); // adds authorization for local and remote API endpoints app.UseAuthorization(); app.Run(); ``` * Duende BFF v3 ```csharp builder.Services.AddBff(); // Configure the authentication builder.Services .AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { // Configure the cookie with __Host prefix for maximum security options.Cookie.Name = "__Host-blazor"; // Because we use an identity server that's configured on a different site // (duendesoftware.com vs localhost), we need to configure the SameSite property to Lax. // Setting it to Strict would cause the authentication cookie not to be sent after logging in. // The user would have to refresh the page to get the cookie. // Recommendation: Set it to 'strict' if your IDP is on the same site as your BFF. options.Cookie.SameSite = SameSiteMode.Lax; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); // Add this scope if you want to receive refresh tokens options.Scope.Add("offline_access"); }); builder.Services.AddAuthorization(); var app = builder.Build(); app.UseAuthentication(); app.UseRouting(); // adds antiforgery protection for local APIs app.UseBff(); // adds authorization for local and remote API endpoints app.UseAuthorization(); // login, logout, user, backchannel logout... app.MapBffManagementEndpoints(); app.Run(); ``` Make sure to replace the Authority, ClientID and ClientSecret with values from your identity provider. Also consider if the scopes are correct. 4. **Adding Local APIs** If your browser-based application uses local APIs, you can add those directly to your BFF app. The BFF supports both controllers and minimal APIs to create local API endpoints. It’s important to mark up the APIs with .AsBffApiEndpoint(), because this adds CSRF protection. Tip Always call `.AsBffApiEndpoint()` on your local API routes. Without it, the `X-CSRF` header is not enforced and your endpoints are vulnerable to CSRF attacks. See [Local APIs](/bff/fundamentals/apis/local/) for details. * Minimal Apis Program.cs ```csharp // Adds authorization for local and remote API endpoints app.UseAuthorization(); // Place your custom routes after the 'UseAuthorization()' app.MapGet("/hello-world", () => "hello-world") .AsBffApiEndpoint(); // Adds CSRF protection to the controller endpoints ``` * Controllers Program.cs ```csharp builder.Services.AddControllers(); // ... app.UseAuthorization(); // When mapping the api controllers, place this after // UseAuthorization() app.MapControllers() .RequireAuthorization() .AsBffApiEndpoint(); // This statement adds CSRF protection to the controller endpoints ``` LocalApiController.cs ```csharp [Route("hello")] public class LocalApiController : ControllerBase { [Route("world")] [HttpGet] public IActionResult SelfContained() { return Ok("hello world"); } } ``` 5. **Adding Remote APIs** If you also want to call remote api’s from your browser based application, then you should proxy the calls through the BFF. The BFF extends the capabilities of [Yarp](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/servers/yarp/getting-started?view=aspnetcore-9.0) in order to achieve this. Tip For a comparison of local vs. remote vs. YARP-based APIs, see the [API Overview](/bff/fundamentals/apis/). Terminal ```bash dotnet add package Duende.BFF.Yarp ``` * Direct forwarding Program.cs ```csharp builder.Services.AddBff() .AddRemoteApis(); // Adds the capabilities needed to perform proxying to remote APIs. // ... // Map any call (including child routes) from /api/remote to https://remote-api-address app.MapRemoteBffApiEndpoint("/api/remote", new Uri("https://remote-api-address")) .WithAccessToken(RequiredTokenType.Client); ``` * Yarp Program.cs ```csharp builder.Services.AddBff() .AddRemoteApis() // This adds the capabilities needed to perform proxying to remote api's. .AddYarpConfig(new RouteConfig() // This statement configures yarp. { RouteId = "route_id", ClusterId = "cluster_id", Match = new RouteMatch { Path = $"api/remote/{{**catch-all}}" } }, new ClusterConfig() { ClusterId = "cluster_id", Destinations = new Dictionary(StringComparer.OrdinalIgnoreCase) { { "destination_1", new DestinationConfig { Address = "https://remote-api-address" } } } }); // ... app.UseAuthorization(); // Add the Yarp middleware that will proxy the requests. app.MapReverseProxy(proxyApp => { proxyApp.UseAntiforgeryCheck(); }); ``` You can also use an `IConfiguration` instead of programmatically configuring the proxy. 6. **Adding Server-Side Sessions** * In-Memory By default, Duende.BFF uses an in-memory session store. This is suitable for development and testing, but not recommended for production as sessions will be lost when the application restarts. Program.cs ```csharp builder.Services.AddBff() .AddServerSideSessions(); // Uses in-memory session store by default // ...existing code for authentication, authorization, etc. ``` * Entity Framework For production scenarios, you can use Entity Framework to persist sessions in a database. First, add the NuGet package: Terminal ```bash dotnet add package Duende.BFF.EntityFramework ``` Then configure the session store in your `Program.cs`: Program.cs ```csharp builder.Services.AddBff() .AddServerSideSessions() .AddEntityFrameworkServerSideSessions(options => { options.UseSqlServer(builder.Configuration.GetConnectionString("DefaultConnection")); }); // ...existing code for authentication, authorization, etc. ``` You will also need to run the Entity Framework migrations to create the necessary tables. In-memory sessions are not production-ready The default in-memory session store loses all sessions on application restart and cannot be shared across multiple instances. Always use Entity Framework-backed sessions in production. ## Frontend Integration [Section titled “Frontend Integration”](#frontend-integration) With the BFF host running, your frontend (JavaScript SPA, React, Angular, etc.) needs to call a few BFF endpoints for authentication and to make API calls. Below is a minimal vanilla JavaScript pattern you can adapt. ### Check the current user session [Section titled “Check the current user session”](#check-the-current-user-session) On load, call `/bff/user` to check whether the user is logged in. This endpoint returns the user’s claims as JSON when authenticated, or a `401`/empty response when anonymous. ```javascript // Fetch the current user from the BFF session async function getUser() { const response = await fetch('/bff/user', { headers: { 'X-CSRF': '1' } }); if (response.ok) { return await response.json(); // Array of { type, value } claim objects } return null; // Not authenticated } ``` ### Login and logout links [Section titled “Login and logout links”](#login-and-logout-links) Use plain anchor tags pointing to the BFF management endpoints. Do **not** use `fetch` for these — they must trigger a full browser redirect. ```html Log in Log out ``` ```javascript // Wire up logout link with the session-bound URL from /bff/user const user = await getUser(); if (user) { const logoutUrlClaim = user.find(c => c.type === 'bff:logout_url'); document.getElementById('logout-link').href = logoutUrlClaim?.value ?? '/bff/logout'; } ``` ### Calling BFF API endpoints [Section titled “Calling BFF API endpoints”](#calling-bff-api-endpoints) Every request to a BFF API endpoint **must** include the `X-CSRF: 1` header. Without it, the BFF will reject the request with `401 Unauthorized`. ```javascript // Centralized fetch wrapper — always add the X-CSRF header async function bffFetch(url, options = {}) { const response = await fetch(url, { ...options, headers: { 'X-CSRF': '1', ...options.headers, }, }); // Redirect to login if the session has expired if (response.status === 401) { window.location.href = `/bff/login?returnUrl=${encodeURIComponent(window.location.pathname)}`; return; } return response; } // Example usage const data = await bffFetch('/api/weather'); const json = await data.json(); ``` Tip Use `bffFetch` (or an equivalent interceptor in your framework) consistently throughout your frontend. This ensures every authenticated request includes the CSRF header and gracefully handles session expiry. ### Proactive session polling (optional) [Section titled “Proactive session polling (optional)”](#proactive-session-polling-optional) To detect server-initiated session termination (e.g., back-channel logout), poll `/bff/user` periodically: ```javascript // Poll every 60 seconds; redirect to login if session ends setInterval(async () => { const user = await getUser(); if (!user) { window.location.href = '/bff/login'; } }, 60_000); ``` ## See Also [Section titled “See Also”](#see-also) [Multiple Frontends](/bff/getting-started/multi-frontend/)Serve several SPAs from the same BFF host. [Blazor Applications](/bff/getting-started/blazor/)BFF setup for Blazor Server and WASM. [Local APIs](/bff/fundamentals/apis/local/)Full reference for embedded API endpoints and CSRF protection. [Remote APIs](/bff/fundamentals/apis/remote/)Direct forwarding to upstream services. [Token Management](/bff/fundamentals/tokens/)How BFF handles access token refresh automatically. [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/)Persistent session configuration. [Access Token Management](/accesstokenmanagement/)The underlying token lifecycle library used by BFF. ----- # Getting Started - Templates > Install and use Duende BFF project templates for .NET. Quickly scaffold BFF applications with remote APIs, local APIs, or Blazor using dotnet new commands. Project templates for Duende BFF are shipped as part of the Duende .NET project templates. Refer the [templates documentation](/identityserver/overview/packaging/#templates) for more information on how to install the templates. ## Available templates [Section titled “Available templates”](#available-templates) ### BFF Remote API [Section titled “BFF Remote API”](#bff-remote-api) ```shell dotnet new duende-bff-remoteapi ``` Creates a basic JavaScript-based BFF host that configures and invokes a [remote API via the BFF proxy](/bff/fundamentals/apis/remote/). ### BFF Local API [Section titled “BFF Local API”](#bff-local-api) ```shell dotnet new duende-bff-localapi ``` Creates a basic JavaScript-based BFF host that invokes a [local API](/bff/fundamentals/apis/local/) co-hosted with the BFF. ### BFF Blazor [Section titled “BFF Blazor”](#bff-blazor) ```shell dotnet new duende-bff-blazor ``` Creates a Blazor application that [uses the interactive auto render mode](/bff/fundamentals/blazor/), and secures the application across all render modes consistently using Duende.BFF.Blazor. ----- # Backend For Frontend (BFF) Samples > A collection of sample applications demonstrating how to use the BFF security framework with different frontend technologies. This section contains a collection of clients using our BFF security framework. ## JavaScript Frontend [Section titled “JavaScript Frontend”](#javascript-frontend) This sample demonstrates a vanilla JavaScript SPA secured by the BFF. You will learn how to call `/bff/user` to retrieve session claims, wire up login/logout links, and make CSRF-protected API calls using `X-CSRF: 1` — without any JS framework dependencies. [JavaScript Frontend Sample](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/JsBffSample)Vanilla JS SPA with BFF: session claims, login/logout, and CSRF-protected API calls ## ReactJs Frontend [Section titled “ReactJs Frontend”](#reactjs-frontend) This sample shows how to integrate React with the BFF framework. You will learn how to manage login state via `/bff/user`, protect routes based on session claims, and proxy API requests through the BFF with automatic token forwarding. [ReactJS Frontend Sample](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/React)React SPA with BFF: session-driven auth state, protected routes, and token-forwarded API calls ## Angular Frontend [Section titled “Angular Frontend”](#angular-frontend) This sample shows how to integrate Angular with the BFF framework. You will learn how to build an Angular auth service backed by `/bff/user`, add an HTTP interceptor for the CSRF header, and handle 401 redirects gracefully. [Angular Frontend Sample](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/Angular)Angular SPA with BFF: auth service, CSRF interceptor, and 401 redirect handling ## Vue Frontend Community [Section titled “Vue Frontend ”Community](#vue-frontend) This sample (contributed by [@Marco Cabrera](https://github.com/mck231)) shows how to integrate Vue 3 with the BFF framework. You will learn how to expose session state from `/bff/user` in a Vue composable and make authenticated API calls with CSRF protection. [Vue Frontend Sample (Community)](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/Vue)Vue 3 SPA with BFF: session composable, CSRF-protected API calls ## Blazor WASM [Section titled “Blazor WASM”](#blazor-wasm) This sample shows how to use Blazor WebAssembly as the frontend with the BFF host. You will learn how to configure `AuthorizationMessageHandler` to forward tokens from the BFF session and call backend APIs securely from client-side Blazor code. [Blazor WASM Sample](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/BlazorWasm)Blazor WASM with BFF: AuthorizationMessageHandler and secure API calls from the browser ## Blazor Auto Rendering [Section titled “Blazor Auto Rendering”](#blazor-auto-rendering) This sample demonstrates Blazor Auto rendering mode (server-side prerender + WASM hydration) combined with BFF authentication. You will learn how to share auth state across render modes and avoid common pitfalls with interactive components that call protected APIs. [Blazor Auto Rendering Sample](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/BlazorAutoRendering)Blazor Auto mode with BFF: shared auth state across server-side and WASM render modes ## YARP Integration [Section titled “YARP Integration”](#yarp-integration) This sample shows how to use the Duende BFF extensions for [Microsoft YARP](https://microsoft.github.io/reverse-proxy/) to proxy API requests. You will learn how to configure YARP routes with BFF token forwarding, eliminating the need for manual `AddRemoteApis` registration. [YARP Integration Sample](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/JsBffYarpSample)BFF with YARP: token-forwarding reverse proxy routes for remote APIs ## OpenAPI [Section titled “OpenAPI”](#openapi) This sample shows how to expose and consume an OpenAPI (Swagger) spec from a BFF-protected API. You will learn how to configure Swagger UI to authenticate via the BFF session and make test requests without needing a separate bearer token. [OpenAPI Sample](https://github.com/DuendeSoftware/samples/tree/main/BFF/v4/OpenApi)BFF with OpenAPI: Swagger UI authenticated via BFF session cookies ## Separate Host for UI [Section titled “Separate Host for UI”](#separate-host-for-ui) This sample shows how to run the frontend (e.g. a dev Vite server) on a different origin from the BFF host and use CORS to allow cross-site session and API requests. You will learn how to configure `AllowedOrigins`, CORS policy, and cookie `SameSite` settings for split-host development and production deployments. [Separate Host for UI Sample](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/SplitHosts)Split-host BFF: CORS configuration for frontend and backend on different origins ## Docker Hosting Community [Section titled “Docker Hosting ”Community](#docker-hosting) This sample (contributed by [@Marco Cabrera](https://github.com/mck231)) shows how to run the BFF host and IdentityServer together using Docker Compose. You will learn how to configure networking between containers, set authority URLs, and handle Data Protection key persistence in a containerized environment. [Docker Sample (Community)](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/docker)BFF + IdentityServer in Docker Compose: container networking and Data Protection key persistence ## DPoP [Section titled “DPoP”](#dpop) This sample shows how to configure the BFF for [DPoP (Demonstrating Proof of Possession)](/identityserver/tokens/pop/) so that all tokens are sender-constrained. You will learn how to enable DPoP on both the BFF and the downstream API, preventing token replay attacks even if tokens are intercepted. [DPoP Sample](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/DPoP)BFF with DPoP: sender-constrained tokens to prevent token replay attacks ## Token Exchange using the IAccessTokenRetriever [Section titled “Token Exchange using the IAccessTokenRetriever”](#token-exchange-using-the-iaccesstokenretriever) This sample shows how to implement a custom `IAccessTokenRetriever` that performs RFC 8693 token exchange for impersonation. When logged in as Alice you receive a token scoped to Bob, and vice versa — demonstrating how to swap or enrich tokens before they are forwarded to downstream APIs. [Token Exchange Sample](https://github.com/DuendeSoftware/Samples/tree/main/BFF/v4/TokenExchange)Custom IAccessTokenRetriever with RFC 8693 token exchange for user impersonation ## New User Onboarding with Blazor Auto Rendering Community [Section titled “New User Onboarding with Blazor Auto Rendering ”Community](#new-user-onboarding-with-blazor-auto-rendering) This sample (contributed by [@hugh-maaskant](https://github.com/hugh-maaskant)) shows how to handle a new-user onboarding flow where additional profile data is collected by the application — not the identity provider. You will learn how to intercept post-login redirects, store onboarding data in the application database, and resume the original request after onboarding completes. [New User Onboarding Sample (Community)](https://github.com/hugh-maaskant/BlazorBffOnboarding)New user onboarding with Blazor Auto: intercept post-login, collect profile data in app DB ## Feedback [Section titled “Feedback”](#feedback) Feel free to [ask the developer community](https://github.com/DuendeSoftware/community/discussions) if you are looking for a particular sample and can’t find it here. [Developer Community Forum](https://github.com/DuendeSoftware/community/discussions)Join the Duende Developer Community for discussions and feedback ----- # Troubleshooting > Diagnose and fix common problems with Duende BFF: anti-forgery failures, CORS errors, session expiration, YARP misconfigurations, Blazor token issues, and more. This page covers the most common problems encountered when building and operating a Duende BFF application. Each scenario is described in **symptom → cause → solution** format. *** ### Anti-Forgery Token Validation Failures [Section titled “Anti-Forgery Token Validation Failures”](#anti-forgery-token-validation-failures) **Cause:** The BFF enforces the presence of a custom `X-CSRF: 1` header on all API endpoints decorated with `.AsBffApiEndpoint()`. Requests that do not include this header are rejected. **Solution:** Add the `X-CSRF: 1` header to every `fetch()` call targeting a BFF API endpoint. The easiest approach is a centralized wrapper: ```javascript function bffFetch(url, options = {}) { return fetch(url, { ...options, headers: { 'X-CSRF': '1', ...options.headers, }, }); } ``` Also verify that: * `app.UseBff()` appears **after** `app.UseRouting()` and `app.UseAuthentication()`, and **before** `app.UseAuthorization()` in your middleware pipeline. * The endpoint is decorated with `.AsBffApiEndpoint()` (Minimal API) or `[BffApi]` / `.AsBffApiEndpoint()` at mapping time (MVC). See [Middleware Pipeline](/bff/fundamentals/middleware-pipeline/) for the canonical order and a table of common mistakes. Caution If `UseBff()` is placed after `UseAuthorization()`, anti-forgery enforcement is silently disabled with no error. Always verify middleware order. *** ### CORS Errors With BFF Endpoints [Section titled “CORS Errors With BFF Endpoints”](#cors-errors-with-bff-endpoints) **Cause:** The BFF and the SPA are on different origins. CORS errors here are usually a sign that the BFF and frontend are not being served from the same origin, which defeats part of the BFF pattern’s security model. **Solution:** The BFF is designed to serve the frontend from the same origin. If you must host them on different origins, configure a CORS policy that explicitly allows the SPA origin and allows credentials: ```csharp builder.Services.AddCors(options => { options.AddPolicy("SpaPolicy", policy => { policy.WithOrigins("https://app.example.com") .AllowAnyHeader() .AllowAnyMethod() .AllowCredentials(); // Required for cookie-based auth }); }); // Must come before UseAuthentication and UseBff app.UseCors("SpaPolicy"); ``` Tip Whenever possible, serve the SPA’s `index.html` from the BFF host itself. This makes the frontend and backend same-origin and eliminates CORS complexity entirely. See [UI Hosting](/bff/architecture/ui-hosting/) for options. *** ### Session Expiration Causing Silent Failures [Section titled “Session Expiration Causing Silent Failures”](#session-expiration-causing-silent-failures) **Cause:** The BFF session (stored in the authentication cookie) has expired. BFF API endpoints return `401` instead of a redirect when the session expires, so the SPA must handle this explicitly. **Solution:** Detect `401` responses in your fetch wrapper and redirect to the BFF login endpoint: ```javascript async function bffFetch(url, options = {}) { const response = await fetch(url, { ...options, headers: { 'X-CSRF': '1', ...options.headers }, }); if (response.status === 401) { window.location.href = `/bff/login?returnUrl=${encodeURIComponent(window.location.pathname)}`; return; } return response; } ``` Also consider: * Polling `/bff/user` periodically to detect session expiry proactively. * Configuring absolute and sliding session lifetimes on the cookie handler to match your requirements. * Using [server-side sessions](/bff/fundamentals/session/server-side-sessions/) to enable server-initiated session termination. *** ### YARP Proxy Misconfiguration [Section titled “YARP Proxy Misconfiguration”](#yarp-proxy-misconfiguration) **Cause:** Common YARP configuration mistakes include: * Missing `UseAntiforgeryCheck()` in the `MapReverseProxy` pipeline. * Typos in metadata keys when using `appsettings.json` configuration. * Route patterns that don’t include `{**catch-all}` to capture sub-paths. **Solution:** Ensure `UseAntiforgeryCheck()` is explicitly included: ```csharp app.MapReverseProxy(proxyApp => { proxyApp.UseAntiforgeryCheck(); // Required — not automatic for YARP }); ``` When configuring via `appsettings.json`, metadata keys are case-sensitive: ```json "Metadata": { "Duende.Bff.Yarp.TokenType": "User", "Duende.Bff.Yarp.AntiforgeryCheck": "true" } ``` For route patterns, ensure sub-paths are captured: ```json "Match": { "Path": "/api/{**catch-all}" } ``` Caution A typo in a YARP metadata key fails silently — no token is attached and no anti-forgery check is enforced. Always test proxied routes with an authenticated request and verify the `Authorization` header reaches the upstream service. *** ### Blazor WASM — Token Not Available in Components [Section titled “Blazor WASM — Token Not Available in Components”](#blazor-wasm--token-not-available-in-components) **Cause:** In Blazor WASM, `HttpContext` is not available. Access tokens are managed server-side by the BFF host and must never be exposed to client-side components. **Solution:** Use `AddLocalApiHttpClient()` to register a typed HTTP client that routes through the BFF host. The BFF host attaches the token server-side before forwarding: ```csharp // Client-side Program.cs builder.Services .AddBffBlazorClient() .AddLocalApiHttpClient(); ``` The `WeatherHttpClient` then calls the BFF host’s local API endpoint (which does have access to `HttpContext` and can call `GetUserAccessTokenAsync()`), rather than calling the remote API directly. Caution Never attempt to retrieve an access token in a Blazor WASM component and pass it to JavaScript or store it in the component state. This defeats the BFF security model. *** ### Silent Login Failures [Section titled “Silent Login Failures”](#silent-login-failures) **Cause:** Modern browsers block third-party cookies. The `prompt=none` / silent renew flow in traditional SPAs relies on an iframe that sends a cookie to the identity provider — this breaks when third-party cookies are blocked. **Solution:** The BFF pattern is specifically designed to avoid this problem. Token renewal is handled server-side using refresh tokens, which do not rely on third-party cookies. Ensure: 1. `offline_access` scope is requested so a refresh token is issued. 2. `SaveTokens = true` is set on the OIDC handler. 3. The BFF’s `Duende.AccessTokenManagement` integration is active (it is by default). ```csharp options.Scope.Add("offline_access"); // Required for refresh tokens options.SaveTokens = true; // Required to store tokens in the session ``` See [Third-Party Cookies](/bff/architecture/third-party-cookies/) for a deeper discussion of how browser cookie restrictions affect authentication flows. *** ### 302 Redirect Instead of 401 on API Endpoints [Section titled “302 Redirect Instead of 401 on API Endpoints”](#302-redirect-instead-of-401-on-api-endpoints) **Cause:** The API endpoint is not marked as a BFF API endpoint, so ASP.NET Core’s default challenge behavior (302 redirect) applies instead of BFF’s 401 response. **Solution:** Add `.AsBffApiEndpoint()` to the endpoint: ```csharp // Minimal API app.MapGet("/api/data", () => Results.Ok("data")) .RequireAuthorization() .AsBffApiEndpoint(); // Converts 302 challenge to 401 // MVC controllers app.MapControllers() .RequireAuthorization() .AsBffApiEndpoint(); ``` This instructs the BFF middleware to return `401` for unauthenticated requests rather than issuing a redirect challenge. Your SPA can then detect the `401` and navigate to `/bff/login`. *** ### Cookie Size Exceeding Browser Limits [Section titled “Cookie Size Exceeding Browser Limits”](#cookie-size-exceeding-browser-limits) **Cause:** All claims are stored in the authentication cookie by default. Large numbers of claims (e.g., from many roles or large identity tokens) can cause the cookie to exceed the 4KB browser limit. ASP.NET Core chunks cookies, but excessively large sessions still cause issues. **Solution:** Switch to [server-side sessions](/bff/fundamentals/session/server-side-sessions/). The browser cookie then only holds a session ID (a small opaque value), and all claims are stored in the server-side session store: ```csharp builder.Services.AddBff() .AddEntityFrameworkServerSideSessions(options => { options.UseSqlServer(connectionString); }); ``` Additionally, filter unnecessary claims from the session using an `IClaimsTransformation` or by configuring the OIDC handler to not request unnecessary scopes: ```csharp // Only request claims you actually need options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); // Don't add scopes whose claims you don't use ``` Tip Server-side sessions are recommended for all production BFF deployments, regardless of claim volume. They also enable server-initiated logout and better session visibility. See [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/) for setup instructions. *** ## See Also [Section titled “See Also”](#see-also) [Getting Started: Single Frontend](/bff/getting-started/single-frontend/)Correct initial setup. [Getting Started: Blazor](/bff/getting-started/blazor/)Blazor-specific configuration. [Local APIs](/bff/fundamentals/apis/local/)CSRF protection for embedded API endpoints. [YARP Integration](/bff/fundamentals/apis/yarp/)Advanced proxy configuration. [Server-Side Sessions](/bff/fundamentals/session/server-side-sessions/)Production session persistence. [Token Management](/bff/fundamentals/tokens/)Access token refresh and revocation. [Third-Party Cookies](/bff/architecture/third-party-cookies/)Browser cookie restrictions and BFF. [Access Token Management](/accesstokenmanagement/)The underlying token lifecycle library. ----- # Upgrading BFF Security Framework > Guide for upgrading Duende.BFF versions, including NuGet package updates Upgrading to a new Duende.BFF version is done by updating the NuGet package and handling any breaking changes. [GitHub Repository](https://github.com/DuendeSoftware/products/tree/main/bff)View the source code for this library on GitHub. [NuGet Package](https://www.nuget.org/packages/Duende.BFF)View the package on NuGet.org. ----- # Duende BFF Security Framework v2.x to v3.0 > Guide for upgrading Duende BFF Security Framework from version 2.x to version 3.0, including migration steps for custom implementations and breaking changes. Duende BFF Security Framework v3.0 is a significant release that includes: * .NET 9 support * Blazor support * Several fixes and improvements ## Upgrading [Section titled “Upgrading”](#upgrading) If you rely on the default extension methods for wiring up the BFF, then V3 should be a drop-in replacement. ### Migrating From Custom Implementations Of IHttpMessageInvokerFactory [Section titled “Migrating From Custom Implementations Of IHttpMessageInvokerFactory”](#migrating-from-custom-implementations-of-ihttpmessageinvokerfactory) In Duende.BFF V2, there was an interface called `IHttpMessageInvokerFactory`. This class was responsible for creating and wiring up yarp’s `HttpMessageInvoker`. This interface has been removed in favor YARP’s `IForwarderHttpClientFactory`. One common scenario for creating a custom implementation of this class was for mocking the http client during unit testing. If you wish to inject a http handler for unit testing, you should now inject a custom `IForwarderHttpClientFactory`. For example: ```csharp // A Forwarder factory that forwards the messages to a message handler (which can be easily retrieved from a testhost) public class BackChannelHttpMessageInvokerFactory(HttpMessageHandler backChannel) : IForwarderHttpClientFactory { public HttpMessageInvoker CreateClient(ForwarderHttpClientContext context) => new HttpMessageInvoker(backChannel); } // Wire up the forwarder in your application's test host: services.AddSingleton( new BackChannelHttpMessageInvokerFactory(_apiHost.Server.CreateHandler())); ``` ### Migrating From Custom Implementations Of IHttpTransformerFactory [Section titled “Migrating From Custom Implementations Of IHttpTransformerFactory”](#migrating-from-custom-implementations-of-ihttptransformerfactory) The `IHttpTransformerFactory` was a way to globally configure the YARP tranform pipeline. In V3, the way that the default `endpoints.MapRemoteBffApiEndpoint()` method builds up the YARP transform has been simplified significantly. Most of the logic has been pushed down to the `AccessTokenRequestTransform`. Here are common scenario’s for implementing your own `IHttpTransformerFactory` and how to upgrade: #### Replacing Defaults [Section titled “Replacing Defaults”](#replacing-defaults) If you used a custom implementation of `IHttpTransformerFactory` to change the default behavior of `MapRemoteBffApiEndpoint()`, for example to add additional transforms, then you can now inject a custom delegate into the ASP.NET Core service provider: ```csharp services.AddSingleton(CustomDefaultYarpTransforms); //... // This is an example of how to add a response header to ALL invocations of MapRemoteBffApiEndpoint() private void CustomDefaultBffTransformBuilder(string localpath, TransformBuilderContext context) { context.AddResponseHeader("added-by-custom-default-transform", "some-value"); DefaultBffYarpTransformerBuilders.DirectProxyWithAccessToken(localpath, context); } ``` Another way of doing this is to create a custom extensionmethod `MyCustomMapRemoteBffApiEndpoint()` that wraps the `MapRemoteBffApiEndpoint()` and use that everywhere in your application. This is a great way to add other defaults that should apply to all endpoints, such as requiring a specific type of access token. #### Configuring Transforms For A Single Route [Section titled “Configuring Transforms For A Single Route”](#configuring-transforms-for-a-single-route) Another common usecase for overriding the `IHttpTransformerFactory` was to have a custom transform for a single route, by applying a switch statement and testing for specific routes. Now, there is an overload on the `endpoints.MapRemoteBffApiEndpoint()` that allows you to configure the pipeline directly: ```csharp endpoints.MapRemoteBffApiEndpoint( "/local-path", _apiHost.Url(), context => { // do something custom: IE: copy request headers context.CopyRequestHeaders = true; // wire up the default transformer logic DefaultTransformers.DirectProxyWithAccessToken("/local-path", context); }) // Continue with normal BFF configuration, for example, allowing optional user access tokens .WithOptionalUserAccessToken(); ``` ### Removed method RemoteApiEndpoint.Map(localpath, apiAddress). [Section titled “Removed method RemoteApiEndpoint.Map(localpath, apiAddress).”](#removed-method-remoteapiendpointmaplocalpath-apiaddress) The Map method was no longer needed as most of the logic had been moved to either the `MapRemoteBffApiEndpoint` and the DefaultTransformers. The map method also wasn’t very explicit about what it did and a number of test scenario’s tried to verify if it wasn’t called wrongly. You are now expected to call the method `MapRemoteBffApiEndpoint`. This method now has a nullable parameter that allows you to inject your own transformers. ### AccessTokenRetrievalContext Properties Are Now Typed [Section titled “AccessTokenRetrievalContext Properties Are Now Typed”](#accesstokenretrievalcontext-properties-are-now-typed) The LocalPath and ApiAddress properties are now typed. They used to be strings. If you rely on these, for example for implementing a custom `IAccessTokenRetriever`, then you should adjust their usage accordingly. ```csharp /// /// The locally requested path. /// public required PathString PathMatch { get; set; } /// /// The remote address of the API. /// public required Uri ApiAddress { get; set; } ``` ### AddAddEntityFrameworkServerSideSessionsServices Renamed To AddEntityFrameworkServerSideSessionsServices [Section titled “AddAddEntityFrameworkServerSideSessionsServices Renamed To AddEntityFrameworkServerSideSessionsServices”](#addaddentityframeworkserversidesessionsservices-renamed-to-addentityframeworkserversidesessionsservices) If you used the method `AddAddEntityFrameworkServerSideSessionsServices()` in your code, please replace it with the corrected `AddEntityFrameworkServerSideSessionsServices()`. ### StateProviderPollingDelay and StateProviderPollingInterval Split Into Separate Options For WebAssembly and Server. [Section titled “StateProviderPollingDelay and StateProviderPollingInterval Split Into Separate Options For WebAssembly and Server.”](#stateproviderpollingdelay-and-stateproviderpollinginterval-split-into-separate-options-for-webassembly-and-server) If you used `BffBlazorOptions.StateProviderPollingInterval` or `BffBlazorOptions.StateProviderPollingDelay` to configure different polling settings, you should now consider if this same setting applies to either Server, WASM or both. Set the appropriate properties accordingly. ### Server Side Sessions Database Migrations [Section titled “Server Side Sessions Database Migrations”](#server-side-sessions-database-migrations) No [Entity Framework database migrations](/bff/fundamentals/session/server-side-sessions/#entity-framework-migrations) are required for the server side sessions feature when using the `Duende.BFF.EntityFramework` package. The database structure remains the same: serversidesessions.sql ```sqlite CREATE TABLE "UserSessions" ( "Id" INTEGER NOT NULL CONSTRAINT "PK_UserSessions" PRIMARY KEY AUTOINCREMENT, "ApplicationName" TEXT NULL, "SubjectId" TEXT NOT NULL, "SessionId" TEXT NULL, "Created" TEXT NOT NULL, "Renewed" TEXT NOT NULL, "Expires" TEXT NULL, "Ticket" TEXT NOT NULL, "Key" TEXT NOT NULL ); CREATE UNIQUE INDEX "IX_UserSessions_ApplicationName_Key" ON "UserSessions" ("ApplicationName", "Key"); CREATE UNIQUE INDEX "IX_UserSessions_ApplicationName_SessionId" ON "UserSessions" ("ApplicationName", "SessionId"); CREATE UNIQUE INDEX "IX_UserSessions_ApplicationName_SubjectId_SessionId" ON "UserSessions" ("ApplicationName", "SubjectId", "SessionId"); CREATE INDEX "IX_UserSessions_Expires" ON "UserSessions" ("Expires"); ``` ----- # Duende BFF Security Framework v3.0 to v4.0 > Guide for upgrading Duende BFF Security Framework from version 3.x to version 4.0, including migration steps for custom implementations and breaking changes. ## Migration Checklist [Section titled “Migration Checklist”](#migration-checklist) Use this checklist to track your upgrade. Each item links to the detailed section below. * [ ] Update `Duende.BFF` NuGet package to v4.x * [ ] [Replace `TokenType` enum with `RequiredTokenType`](#remote-apis) — move `using` to `Duende.Bff.AccessTokenManagement` * [ ] [Replace `.RequireAccessToken()` with `.WithAccessToken()`](#remote-apis) on all remote API registrations * [ ] [Replace `.WithOptionalUserAccessToken()` with `.WithAccessToken(RequiredTokenType.UserOrNone)`](#remote-apis) * [ ] [Update YARP token type config](#configuring-token-types-in-yarp) to use `RequiredTokenType` enum values * [ ] [Rename custom service classes](#service-to-endpoint-updates) (`IUserService` → `IUserEndpoint`, etc.) and update to new extensibility pattern * [ ] [Update `IUserSessionStore` implementations](#custom-session-store) — replace `string key` with `UserSessionKey` struct * [ ] [Update `GetUserAccessTokenAsync` namespace](#access-token-retrieval) — use `Duende.AccessTokenManagement.OpenIdConnect` * [ ] [Optionally migrate to new simplified wireup](#simplified-wireup-without-explicit-authentication-setup) (`.ConfigureOpenIdConnect()` + `.ConfigureCookies()`) * [ ] [Run EF Core database migration](#server-side-sessions-database-migrations) if using server-side sessions (`ApplicationName` → `PartitionKey`) * [ ] Verify YARP-based API proxying still works end-to-end Database schema breaking change The `UserSessions.ApplicationName` column is renamed to `PartitionKey`. If multiple BFF v3 apps share the same session database, upgrade all of them simultaneously or provision a new database for the v4 instance. *** Duende BFF Security Framework v4.0 is a significant release that includes: * Multi-frontend support * OpenTelemetry support * Support for login prompts * Several fixes and improvements The extensibility approach has been drastically changed, and many `virtual` methods containing implementation logic are now internal instead. ## Upgrading [Section titled “Upgrading”](#upgrading) This release introduces many breaking changes. This upgrade guide covers cases where a breaking change was introduced. ### Remote APIs [Section titled “Remote APIs”](#remote-apis) The syntax for configuring remote APIs has changed slightly: Program.cs ```diff // Use a client credentials token -app.MapRemoteBffApiEndpoint("/api/client-token", "https://localhost:5010") -.RequireAccessToken(TokenType.Client); +app.MapRemoteBffApiEndpoint("/api/client-token", new Uri("https://localhost:5010")) +.WithAccessToken(RequiredTokenType.Client); // Use the client token only if the user is logged in -app.MapRemoteBffApiEndpoint("/api/optional-user-token", "https://localhost:5010") -.WithOptionalUserAccessToken(); +app.MapRemoteBffApiEndpoint("/api/optional-user-token", new Uri("https://localhost:5010")) +.WithAccessToken(RequiredTokenType.UserOrNone); ``` * The enum `TokenType` has been renamed to `RequiredTokenType`, and moved from the `Duende.Bff` to `Duende.Bff.AccessTokenManagement` namespace. * The methods to configure the token type have all been replaced with a new method `WithAccessToken()` * Requesting an optional access token should no longer be done by calling `WithOptionalUserAccessToken()`. Use `WithAccessToken(RequiredTokenType.UserOrNone)` instead. ### Configuring Token Types In YARP [Section titled “Configuring Token Types In YARP”](#configuring-token-types-in-yarp) The required token type configuration in YARP has also changed slightly. It uses the enum values from `RequiredTokenType`. ### Extending The BFF [Section titled “Extending The BFF”](#extending-the-bff) #### Service To Endpoint Updates [Section titled “Service To Endpoint Updates”](#service-to-endpoint-updates) Service interfaces and their default implementations have been renamed and have changed, resulting in an updated extensibility model: * Generally, the interfaces have been renamed, e.g. from `IUserService` to `IUserEndpoint`. * Default implementation is now internal, but can be used when overriding the endpoint: ```diff -public class MyUserService : DefaultUserService -{ -public override Task ProcessRequestAsync(HttpContext context, CancellationToken ct) - { // Custom logic here -return base.ProcessRequestAsync(context); - } -} +var bffOptions = app.Services.GetRequiredService>().Value; +app.MapGet(bffOptions.UserPath, async (HttpContext context, CancellationToken ct) => +{ // ... custom logic before calling the endpoint implementation ... +var endpointProcessor = context.RequestServices.GetRequiredService(); +await endpointProcessor.ProcessRequestAsync(context, ct); // ... custom logic after calling the endpoint implementation ... +}); ``` For more information, see the [endpoints documentation](/bff/extensibility/management/). #### Custom Session Store [Section titled “Custom Session Store”](#custom-session-store) If you have a custom implementation of `IUserSessionStore`, the interface has changed to support multiple frontends. In all methods, the `string key` has been replaced with a strongly typed `UserSessionKey` struct, which contains the `PartitionKey` and `SessionId`: * `PartitionKey` - Corresponds to the frontend name (or `ApplicationName` in V3). * `SessionId` - The user’s session identifier. ```diff public class MySessionStore : IUserSessionStore { -public Task GetUserSessionAsync(string key, CancellationToken cancellationToken) + public Task GetUserSessionAsync(UserSessionKey key, CancellationToken cancellationToken) { // ... } // ... } ``` Also see [related database changes and migrations](#server-side-sessions-database-migrations). #### Access Token Retrieval [Section titled “Access Token Retrieval”](#access-token-retrieval) The `HttpContext.GetUserAccessTokenAsync` extension method has been removed from the `Duende.Bff` namespace. You should now use the extension method from the `Duende.AccessTokenManagement.OpenIdConnect` namespace. ```csharp using Duende.AccessTokenManagement.OpenIdConnect; // ... var token = await HttpContext.GetUserAccessTokenAsync(); ``` #### Simplified Wireup Without Explicit Authentication Setup [Section titled “Simplified Wireup Without Explicit Authentication Setup”](#simplified-wireup-without-explicit-authentication-setup) The V3 style of wireup still works, but BFF V4 comes with a newer style of wireup: ```csharp services.AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "your authority"; options.ClientId = "your client id"; options.ClientSecret = "secret"; // ... other OpenID Connect options. } .ConfigureCookies(options => { // The cookie options are automatically configured with recommended practices. // However, you can change the config here. }); ``` Adding this will automatically configure a Cookie and OpenID Connect flow. #### Adding Multiple Frontends [Section titled “Adding Multiple Frontends”](#adding-multiple-frontends) You can statically add a list of frontends by calling the `AddFrontends` method. ```csharp .AddFrontends( new BffFrontend(BffFrontendName.Parse("default-frontend")) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/static/index.html")), new BffFrontend(BffFrontendName.Parse("with-path")) .WithOpenIdConnectOptions(opt => { opt.ClientId = "bff.multi-frontend.with-path"; opt.ClientSecret = "secret"; }) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/static/index.html")) .MapToPath("/with-path"), new BffFrontend(BffFrontendName.Parse("with-domain")) .WithOpenIdConnectOptions(opt => { opt.ClientId = "bff.multi-frontend.with-domain"; opt.ClientSecret = "secret"; }) .WithCdnIndexHtmlUrl(new Uri("https://localhost:5005/static/index.html")) .MapToHost(HostHeaderValue.Parse("https://app1.localhost:5005")) .WithRemoteApis( new RemoteApi("/api/user-token", new Uri("https://localhost:5010")), new RemoteApi("/api/client-token", new Uri("https://localhost:5010")) ) ``` #### Loading Configuration From `IConfiguration` [Section titled “Loading Configuration From IConfiguration”](#loading-configuration-from-iconfiguration) Loading configuration, including OpenID Connect configuration from `IConfiguration` is now supported: ```csharp services.AddBff().LoadConfiguration(bffConfig); ``` This enables you to configure your OpenID Connect options, including secrets, and configure the list of frontends. This also adds a file watcher, to automatically add / remove frontends from the config file. See the type `BffConfiguration` to see what settings can be configured. ## Handling SPA Static Assets [Section titled “Handling SPA Static Assets”](#handling-spa-static-assets) The BFF can be configured to handle the static file assets that are typically used when developing SPA based apps. ### Proxying Only `index.html` [Section titled “Proxying Only index.html”](#proxying-only-indexhtml) When deploying a multi-frontend BFF, it makes most sense to have the frontends configured with an `index.html` file that is retrieved from a Content Delivery Network (CDN). This can be done in various ways. For example, if you use Vite, you can publish static assets with a base URL configured. This will make sure that any static asset, (such as images, scripts, etc.) are retrieved directly from the CDN for best performance. ```csharp var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithCdnIndexHtml(new Uri("https://my_cdn/some_app/index.html")) ``` The BFF automatically wires up a catch-all route that serves`index.html` for that specific frontend. See [Serve the index page from the BFF host](/bff/architecture/ui-hosting/#serve-the-index-page-from-the-bff-host) for more information. ### Proxying All Static Assets [Section titled “Proxying All Static Assets”](#proxying-all-static-assets) When developing a Single-Page Application (SPA), it’s very common to use a development webserver such as Vite. While Vite can publish static assets with a base URL, this doesn’t work well during development. The best development experience can be achieved by configuring the BFF to proxy all static assets from the development server: ```csharp var frontend = new BffFrontend(BffFrontendName.Parse("frontend1")) .WithProxiedStaticAssets(new Uri("https://localhost:3000")); // https://localhost:3000 would be the URL of your development web server. ``` While this can also be done in production, it will proxy all static assets through the BFF. This will increase the bandwidth consumed by the BFF and reduce the overall performance of your application. ### Proxying Assets Based On Environment [Section titled “Proxying Assets Based On Environment”](#proxying-assets-based-on-environment) If you’re using a local development server during development and a CDN in production, you can configure this as follows: ```csharp // In this example, the environment name from the application builder is used to determine // if we're running in production or not. var runningInProduction = () => builder.Environment.EnvironmentName == Environments.Production; // Then, when configuring the frontend, you can switch when the static assets will be proxied. new BffFrontend(BffFrontendName.Parse("default-frontend")) .WithBffStaticAssets(new Uri("https://localhost:5010/static"), useCdnWhen: runningInProduction); ``` Note This function is evaluated immediately when calling the`.WithBffStaticAssets()` extension method. When you call this method during startup, the condition is only evaluated at startup time. It’s not evaluated at runtime for every request. ### Server Side Sessions Database Migrations [Section titled “Server Side Sessions Database Migrations”](#server-side-sessions-database-migrations) When using the server side sessions feature backed by the `Duende.BFF.EntityFramework` package, you will need to script [Entity Framework database migrations](/bff/fundamentals/session/server-side-sessions/#entity-framework-migrations) and apply these changes to your database. ```shell dotnet ef migrations add BFFUserSessionsV4 -o Migrations -c SessionDbContext ``` In the `UserSessions` table, a number of changes were introduced: * The `ApplicationName` column was renamed to `PartitionKey`. This column will contain the BFF frontend name. * Related indexes were updated. serversidesessions.sql ```sqlite ALTER TABLE "UserSessions" RENAME COLUMN "ApplicationName" TO "PartitionKey"; DROP INDEX "IX_UserSessions_ApplicationName_SubjectId_SessionId"; CREATE UNIQUE INDEX "IX_UserSessions_PartitionKey_SubjectId_SessionId" ON "UserSessions" ("PartitionKey", "SubjectId", "SessionId"); DROP INDEX "IX_UserSessions_ApplicationName_SessionId"; CREATE UNIQUE INDEX "IX_UserSessions_PartitionKey_SessionId" ON "UserSessions" ("PartitionKey", "SessionId"); DROP INDEX "IX_UserSessions_ApplicationName_Key"; CREATE UNIQUE INDEX "IX_UserSessions_PartitionKey_Key" ON "UserSessions" ("PartitionKey", "Key"); ``` Note This is a breaking database schema change. If you have multiple BFF V3 applications that share the same database table, you either need to update all BFF applications to V4 at the same time or use a new database for the upgraded BFF V4 application. ----- # AI Agent Skills and MCP Server > Enhance your AI coding assistant with Duende-specific knowledge using Agent Skills for domain expertise and an MCP server for documentation and samples retrieval. When you use AI coding assistants with Duende products, you may find that general-purpose models lack deep expertise on Duende-specific configuration patterns, protocol nuances, and production best practices. Generic responses can miss critical details, like how to configure refresh token rotation, set up a federation gateway, or wire IdentityServer into .NET Aspire. To address this, Duende provides two complementary tools that give your AI coding assistant specialized knowledge: **Duende Agent Skills** and the **Duende Documentation MCP Server**. You can use either or both, depending on your workflow. Agent Skills and the MCP server address different aspects of the same problem and work well together: * **Agent Skills** provide *knowledge*: structured, curated guidance on *what* to look up, *when*, and *how* to apply it. They are static files that run locally in your development environment. When your AI assistant encounters an identity-related task, skills give it the judgment to produce accurate, Duende-specific answers. * **MCP Server** provides *tools*: search, fetch, and sample retrieval against the full Duende documentation, blog, and sample code. It runs as a local server process and gives the AI assistant direct access to the latest published content. Think of skills as the expertise and the MCP server as the reference library. Skills help the AI *know what to do*; the MCP server helps it *look things up*. Together, they give your AI assistant both deep domain knowledge and access to up-to-date authoritative content. ## Which Tool Should You Use? [Section titled “Which Tool Should You Use?”](#which-tool-should-you-use) Choose the approach that fits your workflow: * **Want domain expertise baked into every response?** Install Agent Skills. Your AI assistant will automatically use the relevant skill when it encounters identity-related tasks. * **Want to search and fetch the latest docs and samples?** Register the MCP Server. Your AI assistant gains tools to look up current documentation on demand. * **Want both?** Install both. They are independent and complementary: skills provide structured knowledge while the MCP server provides live content retrieval. [Get Started with Agent Skills](https://github.com/DuendeSoftware/duende-skills)Installation instructions, skill catalog, and benchmarks [Get Started with the MCP Server](https://github.com/DuendeSoftware/products/blob/main/docs-mcp/README.md)Setup instructions for VS Code, Rider, and Claude Code ## Agent Skills [Section titled “Agent Skills”](#agent-skills) Duende IdentityServer Agent Skills are a set of `SKILL.md` files following the open [Agent Skills format](https://agentskills.io/). Each skill is a structured knowledge module covering a specific area of identity and access management. ### What They Cover [Section titled “What They Cover”](#what-they-cover) The skills library includes a number of skills and specialized agents across these areas: * **IdentityServer configuration and hosting**: setup, middleware pipeline, clients, resources, scopes, signing credentials, server-side sessions, Dynamic Client Registration (DCR) * **Token management**: token types, refresh token rotation, token exchange, DPoP, mTLS, Pushed Authorization Requests (PAR), FAPI 2.0 compliance * **API protection**: JWT bearer authentication, reference token introspection, scope-based authorization, proof-of-possession * **UI flows**: login, logout, consent, error pages, federation gateways, external providers, Home Realm Discovery * **ASP.NET Core authentication and authorization**: OIDC, JWT Bearer, cookies, policies, claims-based authorization * **Duende BFF**: Backend-for-Frontend security for SPAs, session management, API proxying * **Deployment and operations**: reverse proxy configuration, data protection, health checks, OpenTelemetry, key management, SAML 2.0 * **Testing**: integration testing with `WebApplicationFactory`, mock token issuance, protocol validation * **Specialized agents**: an IdentityServer specialist and an OAuth/OIDC specialist for complex troubleshooting Note Agent Skills focus on **identity and security**. For general .NET development skills (C# coding standards, EF Core, dependency injection, concurrency patterns, .NET Aspire, and more), consider exploring [dotnet-skills](https://github.com/Aaronontheweb/dotnet-skills) alongside Duende skills. The two sets are complementary with no overlap. ### Setup [Section titled “Setup”](#setup) Clone the [Duende Agent Skills](https://github.com/DuendeSoftware/duende-skills) repository and copy the skill folders into the skills directory for your AI coding assistant. Each skill is a folder containing a `SKILL.md` file. Copy the individual skill folders into the path your AI assistant expects: | AI Coding Assistant | Skills Path | | :------------------ | :--------------------------- | | GitHub Copilot | `.github/skills/` | | Claude Code | `.claude/skills/` | | OpenCode | `~/.config/opencode/skills/` | | Cursor | `.cursor/skills/` | | Gemini CLI | `.gemini/skills/` | | Codex CLI | `.codex/skills/` | For example, to set up skills for GitHub Copilot: * Windows (PowerShell) PowerShell ```powershell git clone https://github.com/DuendeSoftware/duende-skills.git New-Item -ItemType Directory -Force -Path .github\skills Copy-Item -Recurse duende-skills\skills\* .github\skills\ ``` * macOS / Linux Terminal ```bash git clone https://github.com/DuendeSoftware/duende-skills.git mkdir -p .github/skills cp -r duende-skills/skills/* .github/skills/ ``` Adjust the target path for your AI coding assistant (see the table above). For example, replace `.github/skills/` with `.claude/skills/` for Claude Code, or `~/.config/opencode/skills/` for OpenCode. Once the skill folders are in place, your AI assistant discovers and loads them automatically. No further configuration is needed. When your assistant encounters an identity-related task like configuring token lifetimes or setting up an external provider, it loads the relevant skill without any explicit prompting from you. ### Verify It Works [Section titled “Verify It Works”](#verify-it-works) Ask your AI assistant an identity-specific question, for example: `How do I configure refresh token rotation in IdentityServer?`. If the skills are loaded correctly, the response references Duende-specific configuration and mentions IdentityServer options like `RefreshTokenUsage`. ### Measured Impact [Section titled “Measured Impact”](#measured-impact) Every skill is evaluated using realistic prompts with concrete assertions. In benchmarks, AI responses with skills loaded significantly outperform baseline responses, with the biggest gains in deeply Duende-specific areas like UI flows, API protection, and SAML configuration. See the repository for the latest benchmark results, or run them against your model of choice. [Duende Agent Skills](https://github.com/DuendeSoftware/duende-skills)Installation instructions, full skill catalog, and benchmark results ## MCP Server [Section titled “MCP Server”](#mcp-server) The Duende Documentation MCP Server implements the open [Model Context Protocol (MCP)](https://modelcontextprotocol.io/) to give AI coding assistants direct access to Duende documentation, blog posts, and sample code. It runs locally and uses SQLite full-text search to index content from multiple sources. ### What It Can Do [Section titled “What It Can Do”](#what-it-can-do) The MCP server provides several tools to your AI assistant: * **Free-text search** across documentation, blog posts, or samples * **Fetch a specific page** from the documentation site * **Get all content for a sample**: retrieve the full code of a Duende sample project * **Get a specific file from a sample**: retrieve individual files from sample code The server indexes content from three sources, keeping its local database up to date with background indexing: * **Documentation**: parsed from the Duende documentation site’s [`llms.txt`](https://docs.duendesoftware.com/llms.txt) * **Blog**: indexed from the RSS feed at [duendesoftware.com/blog](https://duendesoftware.com/blog/) * **Samples**: downloaded from GitHub, including all `.cs`, `.cshtml`, and relevant `.js` files ### Requirements [Section titled “Requirements”](#requirements) * **.NET 10 SDK**: the MCP server is distributed via the `dnx` tool included in the SDK * **Network access**: the server indexes content from remote sources (documentation site, RSS feed, GitHub) * **A compatible AI coding assistant**: any IDE or CLI tool that supports the MCP protocol No Duende license is required to use the MCP server. ### Setup [Section titled “Setup”](#setup-1) To run the Duende Documentation MCP Server, you need the `dnx` tool (included in the .NET 10 SDK) in your system’s `PATH`. The `dnx` tool can download and run applications packaged and distributed through NuGet. Here are some examples of how to register the MCP server in your IDE: * VS Code You can register the MCP server [in your user settings](https://code.visualstudio.com/docs/agent-customization/mcp-servers#_add-an-mcp-server) to make it available in any workspace, or add a `.vscode/mcp.json` file to your workspace: .vscode/mcp.json ```json { "servers": { "duende-mcp": { "type": "stdio", "command": "dnx", "args": [ "Duende.Documentation.Mcp", "--yes", "--", "--database", "/path/to/database.db" ], "env": {} } } } ``` Replace `/path/to/database.db` with the location where the MCP server should store its SQLite index. * JetBrains Rider In Rider settings, navigate to **Tools | AI Assistant | Model Context Protocol (MCP)**. Add a new MCP server, select **As JSON**, and enter: ```json { "mcpServers": { "duende-mcp": { "command": "dnx", "args": [ "Duende.Documentation.Mcp", "--yes", "--", "--database", "/path/to/database.db" ] } } } ``` Replace `/path/to/database.db` with the location where the MCP server should store its SQLite index. * Claude Code Run the following command: PowerShell ```powershell # Windows (PowerShell) claude mcp add --transport stdio duende-mcp ` -- dnx Duende.Documentation.Mcp --yes ` -- --database C:\path\to\database.db ``` Terminal ```bash # macOS / Linux claude mcp add --transport stdio duende-mcp \ -- dnx Duende.Documentation.Mcp --yes \ -- --database /path/to/database.db ``` Replace the database path with the location where the MCP server should store its SQLite index. The MCP server creates its SQLite database at the path you specify in the `--database` parameter. On first run, it indexes documentation, blog posts, and samples in the background. Subsequent starts reuse the existing index and refresh it incrementally. ### Verify It Works [Section titled “Verify It Works”](#verify-it-works-1) Ask your AI assistant a Duende-specific question, for example: `What is automatic key management?`. If the MCP server is working, the response draws on the indexed documentation and references Duende-specific content. Adding `use Duende` to a prompt can help direct the AI assistant to query the MCP server when the topic could match multiple sources. ### Example Prompts [Section titled “Example Prompts”](#example-prompts) Once the MCP server is registered, you can ask your AI assistant questions like: * `What is a client in OpenID Connect?` * `How can I validate a JWT token in ASP.NET Core?` * `What is automatic key management?` * `Can I add passkeys to Razor Pages? Use Duende.` [Duende Documentation MCP Server](https://github.com/DuendeSoftware/products/blob/main/docs-mcp/README.md)Setup instructions for VS Code, Rider, and Claude Code Securing MCP endpoints If you are building your own MCP server and want to secure it with OpenID Connect and OAuth 2.0, see the [MCP Client sample](/identityserver/samples/clients/#model-context-protocol-mcp-client) for a working example. ## Support and Feedback [Section titled “Support and Feedback”](#support-and-feedback) For questions, feedback, or to report issues with either the Agent Skills or the MCP server, visit the [Duende community](https://github.com/DuendeSoftware/community/discussions). [Duende Community Forum](https://github.com/DuendeSoftware/community/discussions)Ask questions and discuss with the Duende developer community ## Disclaimer [Section titled “Disclaimer”](#disclaimer) Duende’s AI developer tools (including the Duende Documentation MCP Server and Duende Agent Skills) are designed to provide Large Language Models (LLMs) with verified, structured context from Duende’s documentation and product knowledge. These tools improve the quality and relevance of AI-assisted development with Duende products, including IdentityServer, BFF and our Open Source offerings, but they do not guarantee the correctness, security, or completeness of AI-generated output. All code, configuration, and architectural decisions produced with the assistance of these tools must be reviewed and validated by qualified developers before deployment to any environment. Duende Software is not responsible for AI-generated output that results from the use of these tools. ----- # ASP.NET Core Data Protection > Comprehensive guide covering key aspects of ASP.NET Core Data Protection. Any Duende server-side application, like IdentityServer or BFF, is developed and deployed as an ASP.NET Core application. While there are a lot of decisions to make, this also means that your implementation can be built, deployed, hosted, and managed with the same technology you’re using for any other ASP.NET applications you have. It is important to correctly configure ASP.NET Core Data Protection in your application. Tip Some of our most common support requests are related to [Data Protection Keys](#data-protection-keys). We strongly encourage you to review the rest of this page before deploying to production. ## About ASP.NET Core Data Protection [Section titled “About ASP.NET Core Data Protection”](#about-aspnet-core-data-protection) Duende’s SDKs, like IdentityServer and BFF, make extensive use of ASP.NET’s [data protection](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/) feature. It is crucial that you configure data protection correctly when deploying your application in production. ## Data Protection Keys [Section titled “Data Protection Keys”](#data-protection-keys) In local development, ASP.NET automatically creates data protection keys, but in a deployed environment, you will need to ensure that your data protection keys are stored in a persistent way and shared across all load balanced instances of your implementation. This means you’ll need to choose where to store and how to protect the data protection keys, as appropriate for your environment. Microsoft has [extensive documentation on data protection](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview) describing how to configure storage and protection of data protection keys. A typical implementation should include data protection configuration code, like this: Program.cs ```csharp builder.Services.AddDataProtection() // Choose an extension method for key persistence, such as // PersistKeysToFileSystem, PersistKeysToDbContext, // PersistKeysToAzureBlobStorage, PersistKeysToAWSSystemsManager, or // PersistKeysToStackExchangeRedis .PersistKeysToFoo() // Choose an extension method for key protection, such as // ProtectKeysWithCertificate, ProtectKeysWithAzureKeyVault .ProtectKeysWithBar() // Explicitly set an application name to prevent issues with // key isolation. .SetApplicationName("My.Duende.IdentityServer"); ``` Ensure data protection keys are persisted Always make sure data protection is configured to persist data protection keys to storage, using `.PersistKeysTo...()` for your storage mechanism. If you lose your data protection keys, all data protected with those keys is no longer be readable. Additionally, ensure the storage mechanism itself is durable. For example, if you are using the default file system based key store, make sure that the configured path is not stored on ephemeral storage. If you are using Redis to store data protection keys using `PersistKeysToStackExchangeRedis()`, ensure that your Redis service is configured to persist data to a database backup or append-only file. Otherwise, you will lose all data protection keys when your Redis instance reboots. For a more advanced setup, you can create a [key escrow sink](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/extensibility/key-management?view=aspnetcore-10.0#xmlkeymanager), allowing you to store new data protection keys into a secure storage (e.g., Azure Key Vault) before the new keys are encrypted. This enables you to restore existing data protection keys in case they become corrupted or lost. ## Common Problems [Section titled “Common Problems”](#common-problems) Common data protection problems occur when data is protected with a key that is not available when the data is later read. A common symptom is `CryptographicException`s in the application logs. For example, when IdentityServer’s automatic key management fails to read its signing keys due to a data protection failure, it will log an error message such as `"Error unprotecting key with kid {Signing Key ID}."`, and log the underlying `System.Security.Cryptography.CryptographicException`, with a message like `"The key {Data Protection Key ID} was not found in the key ring."` Failures to read automatic signing keys are often the first place where a data protection problem manifests, but any of many places where ASP.NET uses data protection might also throw `CryptographicException`s. There are several ways that data protection problems can occur: 1. In load balanced environments, every instance of a Duende server-side app needs to be configured to share data protection keys. Without shared data protection keys, each load balanced instance will only be able to read the data that it writes. 2. Data protected data could be generated in a development environment and then accidentally included into the build output. This is most commonly the case for automatically managed signing keys that are stored on disk. If you are using automatic signing key management with the default file system based key store, you should exclude the `~/keys` directory from source control and make sure keys are not included in your builds. Note that if you are using our Entity Framework based implementation of the operational data stores, then the keys will instead be stored in the database. 3. Data protection derives keys isolated per application name from the generated key material. If you don’t specify a name, the content root path of the application will be used. In .NET 6.0, Microsoft introduced a breaking change: they changed how ASP.NET Core sets the content root path, which can cause Data Protection issues. This change was reverted in .NET 7.0, and Microsoft has [documented a workaround in case your application has to restore the correct application name](https://learn.microsoft.com/en-us/aspnet/core/security/data-protection/configuration/overview#set-the-application-name-setapplicationname). A better solution is to always specify an explicit application name, but know that changing the application name will cause all existing data protected with the previous application name to become unreadable. 4. When hosting your web application on Microsoft IIS, [special configuration may be required for data protection](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/iis/advanced#data-protection). In most default deployments, IIS falls back to using an ephemeral storage for data protection keys, which means that new keys are generated every time the application pool restarts. We recommend storing data protection keys in a shared location, such as a protected file share or database, and configuring IIS to use that location for data protection. ----- # Glossary > A comprehensive glossary of security and identity management terms, including features and concepts used in Duende IdentityServer The glossary below provides definitions and explanations of commonly used terms and features within the security and identity management domain. Explore each term to gain a deeper understanding of its functionality and relevance. ## Fundamentals [Section titled “Fundamentals”](#fundamentals) ### Client [Section titled “Client”](#client) A client is a piece of software that requests tokens from your IdentityServer - either for authenticating a user ( requesting an identity token) or for accessing a resource (requesting an access token). A client must be first registered with your IdentityServer before it can request tokens and is identified by a unique client ID. There are many different client types, e.g. web applications, native mobile or desktop applications, SPAs, server processes, etc. [Documentation](/identityserver/fundamentals/clients)Learn more about clients ### Connected Application [Section titled “Connected Application”](#connected-application) A connected application is any application or service registered with your Duende IdentityServer instance that relies on it for identity, access, or federation. Each connected application has a unique registration that defines how it interacts with IdentityServer and what it is allowed to do. Connected applications fall into four categories: 1. Interactive applications use OpenID Connect (OIDC) to authenticate users and obtain tokens. These include web applications, native mobile or desktop applications, and SPAs, each identified by its own [Client ID](/general/glossary/#client). 2. Machine-to-machine clients request access tokens without user interaction, typically using the client credentials grant. Background services, APIs calling other APIs, and MCP clients are common examples. 3. Third-party API consumer that requires a client ID and client secret, typically in a SaaS situation or B2B situation. 4. SAML Service Providers use SAML 2.0 to establish federated trust with IdentityServer acting as the Identity Provider (IdP), enabling single sign-on for applications that rely on SAML-based authentication. ### Subject ID [Section titled “Subject ID”](#subject-id) A Subject ID (`sub`) is a stable, unique identifier assigned to a user within the system. It is analogous to, and typically maps directly to, the `sub` claim in OpenID Connect tokens. The Subject ID remains constant across sessions and should be used as the canonical reference to a user rather than mutable attributes such as username or email address. ## Protocols [Section titled “Protocols”](#protocols) ### Core Protocols [Section titled “Core Protocols”](#core-protocols) **License: Lite, Standard, Advanced, Custom** Core protocols cover the OpenID Connect (OIDC) and OAuth 2.0 foundations that most applications need. If you’re building a typical web app, API, or mobile client, core protocols handle authentication, token issuance, and token validation out of the box. Here’s what’s included: | Protocol | What It Does | | :------------------------------------------------- | :--------------------------------------------------------------------- | | OAuth 2.0 Core | Token-based authorization for APIs | | OpenID Connect Core | User authentication on top of OAuth 2.0 | | OIDC Discovery / Authorization Server Metadata | Clients auto-discover your server’s endpoints and capabilities | | Authorization Code Flow | The recommended flow for interactive apps (with PKCE) | | Client Credentials Flow | Machine-to-machine token acquisition | | Token Exchange | Swap one token for another across trust boundaries | | Step-up Authentication | Require stronger authentication for sensitive operations | | JSON Web Tokens (JWT) | Standard token format for access and identity tokens | | JWT Access Token Profile | Structured JWT access tokens per the IETF profile | | JWT Client Authentication | Clients authenticate using signed JWTs instead of shared secrets | | JWT Introspection Response | Token metadata returned as a signed JWT | | Bearer Token Usage | Standard `Authorization: Bearer` header for API calls | | Token Revocation | Invalidate tokens before they expire | | Token Introspection | APIs verify opaque tokens against the server | | RP-Initiated / Front-Channel / Back-Channel Logout | Full suite of logout flows | | PAR (Pushed Authorization Requests) | Clients send auth parameters directly to the server before redirect | | DPoP (Demonstrating of Proof-of-Possession) | Binds tokens to a client’s key pair so stolen tokens can’t be replayed | | Form Post Response Mode | Auth responses delivered via POST instead of the query string | | Multiple Response Types | Support for different OAuth response modes | For the majority of .NET applications, core protocols handle every scenario: users log in, APIs validate tokens, machines talk to machines, and sessions end cleanly. ### Extended Protocols [Section titled “Extended Protocols”](#extended-protocols) **License: Standard, Advanced, Custom** Extended protocols address requirements that go beyond typical web and API scenarios. | Protocol | What It Does | | :------------------------------------------------- | :-------------------------------------------------------------------------------- | | mTLS (Mutual TLS) | Binds tokens to a client certificate for sender-constrained access | | JAR (JWT-Secured Authorization Requests) | Wraps the authorization request in a signed JWT for integrity and confidentiality | | Resource Indicators | Lets a client specify which API it’s targeting when requesting tokens | | CIBA (Client-Initiated Backchannel Authentication) | Authentication triggered by a backend service, not a browser redirect | | Device Authorization Grant | Login flow for devices without a browser (smart TVs, CLI tools, IoT) | | DCR (Dynamic Client Registration) | Clients register themselves programmatically instead of being pre-configured | These protocols solve real problems, but they’re problems that surface in specific business contexts rather than in every project. ### Pushed Authorization Requests [Section titled “Pushed Authorization Requests”](#pushed-authorization-requests) **License: Business (legacy), Lite, Standard, Advanced, Custom** Implementation of [RFC 9126](https://www.rfc-editor.org/rfc/rfc9126.html). Provides a more secure way to start a browser-based token/authentication request. [Documentation](/identityserver/tokens/par)Learn more about Pushed Authorization Requests ### Dynamic Client Registration [Section titled “Dynamic Client Registration”](#dynamic-client-registration) **License: Business (legacy), Enterprise (legacy), Standard, Advanced, Custom** Implementation of [RFC 8707](https://tools.ietf.org/html/rfc8707). Provides a standards-based endpoint to register clients and their configuration. [Documentation](/identityserver/configuration)Learn more about Dynamic Client Registration ### Client-Initiated Backchannel Authentication (CIBA) [Section titled “Client-Initiated Backchannel Authentication (CIBA)”](#client-initiated-backchannel-authentication-ciba) **License: Enterprise (legacy), Standard, Advanced, Custom** Duende IdentityServer supports the Client-Initiated Backchannel Authentication Flow (also known as CIBA). This allows a user to log in with a higher security device (e.g. their mobile phone) than the device on which they are using an application (e.g. a public kiosk). CIBA is one of the requirements to support the Financal-grade API compliance. [More Details](https://duendesoftware.com/blog/20220107-ciba)Client-Initiated Backchannel Authentication post [Documentation](/identityserver/ui/ciba/)Learn more about CIBA ### Proof-of-Possession At The Application Layer / DPoP [Section titled “Proof-of-Possession At The Application Layer / DPoP”](#proof-of-possession-at-the-application-layer--dpop) **License: Enterprise (legacy), Standard, Advanced, Custom** A mechanism for sender-constraining OAuth 2.0 tokens via a proof-of-possession mechanism on the application level. This mechanism allows for the detection of replay attacks with access and refresh tokens. [Documentation](/identityserver/tokens/pop/)Learn more about Proof-of-Possession ### SAML 2.0 Identity Provider [Section titled “SAML 2.0 Identity Provider”](#saml-20-identity-provider) **License: Enterprise (legacy), Standard (add-on), Advanced, Custom** IdentityServer can act as a SAML 2.0 Identity Provider (IdP), issuing SAML assertions to Service Providers (SPs) that use the SAML 2.0 protocol rather than OAuth 2.0 / OpenID Connect. This is useful for integrating with enterprise SaaS applications (e.g. Salesforce, ServiceNow) or legacy SSO systems that require SAML-based federation. [Documentation](/identityserver/saml/)Learn more about SAML 2.0 Identity Provider support ### Financial-Grade Security and Conformance (FAPI 2.0) [Section titled “Financial-Grade Security and Conformance (FAPI 2.0)”](#financial-grade-security-and-conformance-fapi-20) **License: Standard (add-on), Advanced (add-on), Custom (add-on)** The [Financial-grade API (FAPI) 2.0 Security Profile](https://openid.net/specs/fapi-security-profile-2_0-final.html) is an API security profile based on OAuth 2.0 designed to protect APIs in high-value scenarios such as e-health and e-government. Duende IdentityServer implements the FAPI 2.0 best current practice features and includes a built-in conformance report that assesses your server and client configuration against OAuth 2.1 and FAPI 2.0 specifications. [Documentation](/identityserver/tokens/fapi-2-0-specification/)Learn more about FAPI 2.0 compliance [Conformance Report](/identityserver/diagnostics/conformance-report/)Assess your configuration against FAPI 2.0 ## Features [Section titled “Features”](#features) ### Automatic Key Management [Section titled “Automatic Key Management”](#automatic-key-management) **License: Business (legacy), Enterprise (legacy), Standard (add-on), Advanced, Custom** The automatic key management feature creates and manages key material for signing tokens and follows best practices for handling this key material, including storage and rotation. [More Details](https://duendesoftware.com/blog/20201028-key-management)Automatic Key Management post [Documentation](/identityserver/fundamentals/key-management/#automatic-key-management)Learn more about key rotation ### Server-side Session Management [Section titled “Server-side Session Management”](#server-side-session-management) **License: Business (legacy), Enterprise (legacy), Standard, Advanced, Custom** The server-side session management feature extends the ASP.NET Core cookie authentication handler to maintain a user’s authentication session state in a server-side store, rather than putting it all into a self-contained cookie. Using server-side sessions enables more architectural features in your IdentityServer, such as: * query and manage active user sessions (e.g. from an administrative app). * detect session expiration and perform cleanup, both in IdentityServer and in client apps. * centralize and monitor session activity in order to achieve a system-wide inactivity timeout. [More Details](https://duendesoftware.com/blog/20220406-session-management)Server-side Session Management post [Documentation](/identityserver/ui/server-side-sessions/)Learn more about Server-side Session Management ### BFF Security Framework [Section titled “BFF Security Framework”](#bff-security-framework) **License: Lite, Standard, Advanced, Custom** The Duende Backend For Frontend (BFF) security framework packages up guidance and the necessary components to secure browser-based frontends (e.g. SPAs or Blazor WASM applications) with ASP.NET Core backends. [More Details](https://duendesoftware.com/blog/20210326-bff)BFF Security Framework post [Documentation](/bff/)Learn more about BFF ### Dynamic Authentication Providers [Section titled “Dynamic Authentication Providers”](#dynamic-authentication-providers) **License: Enterprise (legacy), Advanced, Custom** The dynamic configuration feature allows dynamic loading of configuration for OpenID Connect providers from a store. This is designed to address the performance concern and allowing changes to the configuration to a running server. [More Details](https://duendesoftware.com/blog/20210517-dynamic-providers)Dynamic Authentication Providers post [Documentation](/identityserver/fundamentals/key-management/#automatic-key-management)Learn more about Dynamic Authentication Providers ### Resource Isolation [Section titled “Resource Isolation”](#resource-isolation) **License: Enterprise (legacy), Standard, Advanced, Custom** The resource isolation feature allows a client to request access tokens for an individual resource server. This allows API-specific features such as access token encryption and isolation of APIs that are not in the same trust boundary. [More Details](https://duendesoftware.com/blog/20260210-implementing-zero-trust-with-resource-isolation)Resource Isolation post [Documentation](/identityserver/fundamentals/resources/isolation/)Learn more about Resource Isolation ## Authentication [Section titled “Authentication”](#authentication) ### FIDO2 [Section titled “FIDO2”](#fido2) FIDO2 is the FIDO Alliance’s umbrella standard for strong, passwordless authentication. It encompasses two specifications: **WebAuthn** (the browser/platform API) and **CTAP** (Client to Authenticator Protocol, which defines how authenticators such as hardware security keys communicate with a platform). FIDO2 credentials are based on public-key cryptography and are phishing-resistant by design. ### MFA (Multi-Factor Authentication) [Section titled “MFA (Multi-Factor Authentication)”](#mfa-multi-factor-authentication) Multi-Factor Authentication requires a user to present two or more independent verification factors before being granted access. Factors are typically categorized as something you *know* (e.g. a password or PIN), something you *have* (e.g. a TOTP authenticator app or hardware key), and something you *are* (e.g. a biometric). Requiring multiple factors significantly reduces the risk of account compromise from stolen credentials. ### OTP (One-Time Password) [Section titled “OTP (One-Time Password)”](#otp-one-time-password) A One-Time Password is a temporary, single-use code delivered out-of-band (typically via email or SMS) that authenticates a user without requiring a traditional password. Because each code is valid for only a short window and cannot be reused, OTPs provide a simple form of passwordless or second-factor authentication. ### Passkey [Section titled “Passkey”](#passkey) A passkey is a FIDO2 credential that replaces a traditional password. It is bound to a specific device (or synced across a user’s devices via a platform credential manager) and is unlocked locally using biometrics or a PIN. The private key never leaves the device, making passkeys resistant to phishing and server-side credential theft. ### Recovery Code [Section titled “Recovery Code”](#recovery-code) A recovery code is a one-time backup code generated at enrollment time and stored securely by the user. It can be used to regain account access when primary authenticators (such as a TOTP app or passkey device) are unavailable. Each code is valid for a single use only. ### TOTP (Time-Based One-Time Password) [Section titled “TOTP (Time-Based One-Time Password)”](#totp-time-based-one-time-password) A Time-Based One-Time Password is a short-lived numeric code generated by an authenticator app (e.g. Google Authenticator, Microsoft Authenticator) according to [RFC 6238](https://www.rfc-editor.org/rfc/rfc6238). The code is derived from a shared secret and the current time, and is typically valid for 30 seconds. TOTP is widely used as a second factor in MFA flows. ### WebAuthn (Web Authentication) [Section titled “WebAuthn (Web Authentication)”](#webauthn-web-authentication) WebAuthn is the [W3C Web Authentication standard](https://www.w3.org/TR/webauthn/) that defines a browser API for creating and using public-key credentials. It is the web-facing component of FIDO2. Relying parties (web applications) use WebAuthn to register and authenticate users with device-bound or synced credentials (passkeys), hardware security keys, or platform authenticators, all without transmitting a password. ## User Management [Section titled “User Management”](#user-management) ### User Management - Licensed Users [Section titled “User Management - Licensed Users”](#user-management---licensed-users) The total of unique identity records stored in Duende User Management, identified by User Management user ID per billing period. ### Space (User Management) [Section titled “Space (User Management)”](#space-user-management) In Duende User Management, a *space* is the fundamental isolation unit. Each space has its own isolated user store, configuration, and authentication settings. This enables deployments where users, roles, and groups in one space are completely separated from those in another. A typical use case is supporting multiple tenants in a single deployment. ## Deployment and Licensing [Section titled “Deployment and Licensing”](#deployment-and-licensing) ### Single Deployment [Section titled “Single Deployment”](#single-deployment) A single deployment acts as a single OpenID Connect / OAuth authority hosted at a single URL. It can consist of multiple physical or virtual nodes for load-balancing or fail-over purposes. ### Multiple Deployments [Section titled “Multiple Deployments”](#multiple-deployments) Can be either completely independent single deployments, or a single deployment that acts as multiple authorities. ### Multiple Authorities [Section titled “Multiple Authorities”](#multiple-authorities) A single logical deployment that acts as multiple logical token services on multiple URLs or host names (e.g. for branding, isolation or multi-tenancy reasons). ### Redistribution [Section titled “Redistribution”](#redistribution) Redistribution occurs when you bundle Duende IdentityServer as an integrated component of a product or service that you sell, lease, or provide to third parties. Redistribution typically applies to Independent Software Vendors (ISVs) who ship IdentityServer as part of a larger solution that customers host on their own local or cloud infrastructure. Each customer installation of IdentityServer is considered a separate redistribution. ## Support [Section titled “Support”](#support) ### Standard Developer Support [Section titled “Standard Developer Support”](#standard-developer-support) Online [developer community forum](https://github.com/DuendeSoftware/community/discussions) for Duende Software product issues and bugs. [Duende Developer Community](https://github.com/DuendeSoftware/community/discussions)Learn more about the Duende Developer Community [Support Options](/general/support-and-issues/#support)Learn more about support options ### Priority / Premium Developer Support [Section titled “Priority / Premium Developer Support”](#priority--premium-developer-support) **License: Enterprise (legacy), Standard, Advanced, Custom** Helpdesk system with guaranteed response time for Duende Software product issues and bugs. [More Details](https://duendesoftware.com/license/PrioritySupportLicense.pdf)Download the Priority Support License PDF [Support Options](/general/support-and-issues/#support)Learn more about support options ----- # Licensing > Details about Duende IdentityServer and BFF licensing requirements, editions, configuration options, and trial mode functionality. Duende products, except for our [open source tools](https://duendesoftware.com/products/opensource), require a license for production use. The [Duende Software website](https://duendesoftware.com/) provides an overview of different products and license editions. Licenses can be configured via a file system, programmatic startup, or external configuration services like Azure Key Vault, with trial mode available for development and testing. ## IdentityServer [Section titled “IdentityServer”](#identityserver) Duende IdentityServer requires a [paid license](https://duendesoftware.com/products/identityserver) for production use, with plans available that offer various features based on organizational needs. A [community edition](https://duendesoftware.com/products/communityedition/) is also available. For some longer-term customers, we still honor customers continuing on our previous Starter, Business and Enterprise licenses. Free for development IdentityServer is [free](#trial-mode) for development, testing and personal projects, but production use requires a [license](https://duendesoftware.com/products/identityserver). ### Editions [Section titled “Editions”](#editions) There are three license editions which include different [features](https://duendesoftware.com/products/features). #### Lite Edition [Section titled “Lite Edition”](#lite-edition) The Lite edition includes the core OIDC and OAuth protocol implementation. This is an economical option that is a good fit for organizations with basic needs. It’s also a great choice if you have an aging [IdentityServer4 implementation that needs to be updated](/identityserver/upgrades/identityserver4-to-duende-identityserver-v8/) and licensed. The Lite edition includes all the features that were part of IdentityServer4, along with support for the latest .NET releases, improved observability through [OpenTelemetry support](/identityserver/diagnostics/otel/), and years of bug fixes and enhancements. #### Standard Edition [Section titled “Standard Edition”](#standard-edition) The Standard edition adds additional features that go beyond the core protocol support included in the Starter edition. This is a popular license because it adds the most commonly needed tools and features outside a basic protocol implementation. Feature highlights include resource isolation, the OpenId Connect CIBA flow support, and server side sessions. #### Advanced Edition [Section titled “Advanced Edition”](#advanced-edition) Finally, the Advanced edition includes everything in the Standard edition and adds support for features that are typically used by enterprises with particularly complex architectures or that handle particularly sensitive data. Highlights include automatic key management, SAML, and priority developer support. This is the best option when you have a specific threat model or architectural need for these features. #### Starter Edition (legacy) [Section titled “Starter Edition (legacy)”](#starter-edition-legacy) The (legacy) Starter edition includes the core OIDC and OAuth protocol implementation. #### Business Edition (legacy) [Section titled “Business Edition (legacy)”](#business-edition-legacy) The (legacy) Business edition adds additional features that go beyond the core protocol support included in the Starter edition. Feature highlights include support for server side sessions and automatic signing key management. #### Enterprise Edition (legacy) [Section titled “Enterprise Edition (legacy)”](#enterprise-edition-legacy) The (legacy) Enterprise edition includes everything in the Business edition and adds resource isolation, the OpenId Connect CIBA flow, and dynamic federation. ### Redistribution [Section titled “Redistribution”](#redistribution) If you want to redistribute Duende IdentityServer to your customers as part of a product, you can use our [redistributable license](https://duendesoftware.com/products/identityserverredist). ### License Validation and Logging [Section titled “License Validation and Logging”](#license-validation-and-logging) All license validation happens at runtime and is self-contained. It does not leave the host, and there are no outbound network calls related to license validation. #### Startup Validation [Section titled “Startup Validation”](#startup-validation) IdentityServer loads and parses the license key at startup. If the key is present but invalid, an error is logged at that point. Beyond that, no further checks happen at startup. IdentityServer does not compare your configuration against the license at startup; that all happens at runtime, when features are actively used. IdentityServer 7 and earlier In v7 and earlier, IdentityServer performed validation checks at startup. If no license was configured, it logged a warning and entered [Trial Mode](#trial-mode). If a license was configured, it compared the license against the current configuration and logged any discrepancies it found. #### Runtime Validation [Section titled “Runtime Validation”](#runtime-validation) IdentityServer validates feature usage based on licensing at runtime. To avoid unnecessary downtime, the runtime validator prefers to log a message. It will only throw an exception for certain features. The following features are validated at runtime. If you use one of them without the required license entitlement, IdentityServer logs a warning (rate-limited to once every 5 minutes per feature): * [Demonstrating Proof-of-Possession (DPoP)](/identityserver/tokens/pop/) * [Resource Isolation](/identityserver/fundamentals/resources/isolation/) * [Client Initiated Backchannel Authentication (CIBA)](/identityserver/ui/ciba/) * [Dynamic Identity Providers](/identityserver/ui/login/dynamicproviders/) * [Financial-Grade Security and Conformance Report](/identityserver/diagnostics/conformance-report/) * [User Management](/identityserver/identity/user-management/) Some features do require a license with the proper entitlement(s). If you use one of the following features without the required license entitlement, IdentityServer throws an exception during startup validation. This only applies when a license is present. If you do not have a license configured because you are developing locally, or IdentityServer is deployed to a non-production environment, a log message will be output instead. * [Server Side Sessions](/identityserver/ui/server-side-sessions/) * [Automatic Key Management](/identityserver/fundamentals/key-management/) * [SAML IdP and SAML Service Provider](/identityserver/saml/) Note When developing, you may use your production license key in *any* environment as [detailed below](#using-a-license-in-non-production-environments). For quantized limits like client count and issuer count, IdentityServer logs a warning when you exceed your licensed limit but stay within the grace threshold. If you exceed the grace threshold, it logs an error instead. An expired license also results in an error being logged. IdentityServer 7 and earlier In IdentityServer 7 and earlier, some features were actually disabled at runtime when the license did not include them. The features that could be disabled were: Server Side Sessions, DPoP, Resource Isolation, PAR, Dynamic Identity Providers, and CIBA. Tip When rolling over to a renewed license, you can configure the new license before the old license expires. While the expiration timestamp of a license is used to validate a license is active, the start date is an administrative data point IdentityServer does not take into account for license validation. In other words, you can safely configure the new license before the old one lapses. #### New License Key File Format [Section titled “New License Key File Format”](#new-license-key-file-format) With Duende IdentityServer v8, a new license key file format was introduced. The following table shows which license key formats are compatible with which versions of IdentityServer: | License key issued for | Used with IdentityServer v7 or BFF | Used with IdentityServer v8 | | ---------------------- | ---------------------------------- | --------------------------- | | v7 (or earlier) | ✅ Works | ✅ Works (no add-on support) | | v8 | ❌ Does not work | ✅ Works (full support) | Key points: * **No new purchase required for v8**: If you have an active license from a previous version, the older key format works with IdentityServer v8. No new subscription or purchase is necessary. * **Add-ons require a v8 key**: To use add-ons like [SAML](/identityserver/saml/) or [Duende User Management](/identityserver/identity/user-management/) in production on IdentityServer v8, a license key in the new v8 format is required. * **v8 keys are not backwards-compatible**: A license key issued for v8 cannot be used with IdentityServer v7 or earlier. If you use a license issued for IdentityServer v8 in IdentityServer v7 or BFF, you may see the following error logged: ```text Error validating the Duende software license key - You do not have a valid license key for the Duende software. This is allowed for development and testing scenarios. If you are running in production, you are required to have a licensed version. IDX10503: Signature validation failed. Token does not have a kid. Keys tried: "[PIl of type System.Text.StringBuilder' is hidden. For more details, see https://aka.ms/ldentityModel/PIl. ``` Please [contact our sales team](https://duendesoftware.com/contact/sales) to request an updated license file, and make sure to communicate your version of IdentityServer. #### Trial Mode [Section titled “Trial Mode”](#trial-mode) Running IdentityServer without a license is perfectly fine for development, testing, and personal projects. There is no request limit and no automatic shutdown. All features remain available. The only difference you will notice is that IdentityServer logs a warning when you use a licensed feature without a license configured: ```text {FeatureName} is being used but no Duende license is configured. Please start a conversation with us: https://duende.link/l/contact ``` These warnings are rate-limited to once per five minutes per feature, so they won’t flood your logs. You can silence them entirely by configuring a license key, even in non-production environments. IdentityServer 7 and earlier In IdentityServer 7 and earlier, running without a license was called Trial Mode and was limited to 500 protocol requests. This included all HTTP requests that IdentityServer itself handled, such as requests for the discovery, authorize, and token endpoints. UI requests, such as the login page, were not included in this limit. Beginning in IdentityServer 7.1, IdentityServer logged a warning when the trial mode threshold was exceeded: ```text You are using IdentityServer in trial mode and have exceeded the trial threshold of 500 requests handled by IdentityServer. In a future version, you will need to restart the server or configure a license key to continue testing. ``` This limit is not currently being enforced. #### Redistribution [Section titled “Redistribution”](#redistribution-1) If you want to redistribute Duende IdentityServer to your customers as part of a product, you can use our [redistributable license](https://duendesoftware.com/products/identityserverredist). It can be cumbersome to deploy updated licenses in redistribution scenarios, especially if your deployment cycle does not coincide with the duration of your IdentityServer license. In that situation, update the license key at the next deployment to your redistribution customers. You are always responsible for ensuring your license is renewed. #### Log Severity [Section titled “Log Severity”](#log-severity) The severity of log messages depends on the nature of the message. All messages are rate-limited to once per 5 minutes per feature or SKU. | Type of message | Severity | | ------------------------------------------------- | ------------- | | Feature used, no license configured | Warning | | Feature used, not covered by license | Warning | | Quantized limit exceeded (within grace threshold) | Warning | | Quantized limit exceeded (beyond grace threshold) | Error | | License expired | Error | | License valid | Informational | IdentityServer 7 and earlier In IdentityServer 7 and earlier, log severity depended on both the nature of the message and the type of license. | Type of Message | Standard License | Redistribution License (development\*) | Redistribution License (production\*) | | ----------------------------- | ---------------- | -------------------------------------- | ------------------------------------- | | Startup, missing license | Warning | Warning | Warning | | Startup, license details | Debug | Debug | Trace | | Startup, valid license notice | Informational | Informational | Trace | | Startup, violations | Error | Error | Trace | | Runtime, violations | Error | Error | Trace | \* as determined by `IHostEnvironment.IsDevelopment()` #### Using a License in Non-Production Environments [Section titled “Using a License in Non-Production Environments”](#using-a-license-in-non-production-environments) When running in non-production environments (development, test, QA, or anywhere else) without a license key, you can use your production license key to suppress the warnings. IdentityServer is [free](#trial-mode) for development, testing, and personal projects. Using your production license in these environments is fully supported. Using the production license key in non-production environments can also help ensure that a feature is not accidentally being used when the license does not allow for it. For example, if attempting to use the Server-Side Sessions feature but the production license does not have the entitlement for it, an exception will be seen in the lower environments before reaching production. If you have feedback on trial mode, or specific use cases where you prefer other options, please [open a community discussion](https://github.com/DuendeSoftware/community/discussions). ## BFF Security Framework [Section titled “BFF Security Framework”](#bff-security-framework) The Duende BFF Security Framework requires a license for production use, with two editions available (Starter and Enterprise) that offer various features based on organizational needs. Trial mode Duende BFF has a [limited trial mode](#bff-trial-mode) for development and testing. For small organizations or personal projects, consider the [community edition](https://duendesoftware.com/products/communityedition/). For production use, a [license](https://duendesoftware.com/products/bff) is required. ### Editions [Section titled “Editions”](#editions-1) BFF is a library designed to enhance the security of browser-based applications by moving authentication flows to the server side. The Duende BFF Security Framework requires a license for production use, and is available in two editions that [include different functionality](https://duendesoftware.com/products/bff) based on organizational needs. ### Redistribution [Section titled “Redistribution”](#redistribution-2) If you want to redistribute Duende BFF to your customers as part of a product, please [reach out to sales](https://duendesoftware.com/contact/sales). ### License Validation and Logging [Section titled “License Validation and Logging”](#license-validation-and-logging-1) The BFF license is validated during runtime. All license validation is self-contained and does not leave the host. There are no outbound network calls related to license validation. #### BFF v3.1+ Runtime Validation [Section titled “BFF v3.1+ Runtime Validation”](#bff-v31-runtime-validation) BFF v3.1 does not technically enforce the presence of a license key. At runtime, if no license is present, an error message will be logged. #### BFF v4 Runtime Validation [Section titled “BFF v4 Runtime Validation”](#bff-v4-runtime-validation) BFF v4 requires a valid license in production environments. When no license is present, the system operates in [trial mode](#bff-trial-mode) with a limitation of maximum of five sessions per host (not technically enforced) with any excess resulting in error logging. Trial mode is also enabled when the license could not be validated, for example when the signature validation fails. When an expired license is used, the system will continue to function with only a warning written to the logs, and not fall back to trial mode. #### BFF Trial Mode [Section titled “BFF Trial Mode”](#bff-trial-mode) Using BFF without a license is considered Trial Mode. When running in Trial Mode, you will see the following error logged on startup: ```text You do not have a valid license key for the Duende software. BFF will run in trial mode. This is allowed for development and testing scenarios. If you are running in production you are required to have a licensed version. Please start a conversation with us: https://duende.link/l/bff/contact ``` In Trial Mode, BFF will be limited to a maximum of five (5) sessions per host. Sessions exceeding the limit will cause the host to log an error for every consecutive authenticated session: ```text BFF is running in trial mode. The maximum number of allowed authenticated sessions (5) has been exceeded. See https://duende.link/l/bff/trial for more information. ``` The trial mode session limit is not distributed or shared across multiple nodes. Note When operating non-production environments, such as development, test, or QA, without a valid license key, you may run into this trial mode limitation. If you require a larger number of sessions, we support using your production license in these environments when trial mode is not enough. ## License Key [Section titled “License Key”](#license-key) The license key can be configured in one of three ways: * Via a well-known file on the file system * Via `IConfiguration` (for example, `appsettings.json` or environment variables) * Programmatically in your startup code You can also use other configuration sources such as Azure Key Vault, by using the programmatic approach. Redistributable license If you use our [redistributable license](https://duendesoftware.com/products/identityserverredist), we recommend loading the license at startup from an embedded resource. We consider the license key to be private to your organization, but not necessarily a secret. If you’re using private source control that is scoped to your organization, storing your license key within it is acceptable. ### File System [Section titled “File System”](#file-system) Duende products like IdentityServer and the BFF Security Framework look for a file named `Duende_License.key` in the [ContentRootPath](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostenvironment.contentrootpath?#microsoft-extensions-hosting-ihostenvironment-contentrootpath) of your application. If present, the content of the file will be used as the license key. By default, `ContentRootPath` is the directory that contains the application’s `.csproj` file during development, and the application’s base directory in published deployments. Place the license key file there: ```text MyIdentityServer/ ├── Duende_License.key ← place license here ├── MyIdentityServer.csproj ├── Program.cs ├── appsettings.json └── ... ``` Tip To verify your `ContentRootPath` at runtime, inspect `builder.Environment.ContentRootPath`. ### Configuration v8.0 [Section titled “Configuration ”v8.0](#configuration) IdentityServer can read the license key directly from `IConfiguration`, so you do not need to write any startup code. If `LicenseKey` is not set in your `AddIdentityServer` call, IdentityServer checks the following configuration keys in order, using the first non-empty value it finds: 1. `Duende:IdentityServer:LicenseKey` 2. `Duende:LicenseKey` Whitespace is trimmed, and empty or whitespace-only values are ignored. Add the license key to `appsettings.json` using the IdentityServer-specific key: appsettings.json ```json { "Duende": { "IdentityServer": { "LicenseKey": "eyJhbG..." } } } ``` Or use the shorter key: appsettings.json ```json { "Duende": { "LicenseKey": "eyJhbG..." } } ``` Because [`IConfiguration`](https://learn.microsoft.com/en-us/dotnet/core/extensions/configuration) supports many providers, you can also supply the key via environment variables (for example, `Duende__IdentityServer__LicenseKey` or `Duende__LicenseKey`), Azure App Configuration, Azure Key Vault, or any other configuration source. Note Loading the license key from configuration is not currently supported in Duende BFF. ### Startup [Section titled “Startup”](#startup) If you prefer to load the license key programmatically, you can do so in your startup code. This allows you to use the ASP.NET configuration system to load the license key from any [configuration provider](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-7.0#cp), including environment variables, `appsettings.json`, external configuration services such as Azure App Configuration, Azure Key Vault, etc. #### IdentityServer [Section titled “IdentityServer”](#identityserver-1) The `AddIdentityServer` method accepts a lambda expression to configure various options in your IdentityServer, including the `LicenseKey`. Set the value of this property to the content of the license key file. Program.cs ```csharp builder.Services.AddIdentityServer(options => { // the content of the license key file options.LicenseKey = "eyJhbG..."; }); ``` #### BFF Security Framework [Section titled “BFF Security Framework”](#bff-security-framework-1) The `AddBff` method accepts a lambda expression to configure various options in your BFF host, including the `LicenseKey`. Set the value of this property to the content of the license key file. Program.cs ```csharp builder.Services.AddBff(options => { // the content of the license key file options.LicenseKey = "eyJhbG..."; }); ``` ### Azure Key Vault [Section titled “Azure Key Vault”](#azure-key-vault) When deploying your application to Microsoft Azure, you can make use of [Azure Key Vault](https://azure.microsoft.com/products/key-vault/) to load the Duende license key at startup. Similarly to setting the license key programmatically, you can use the `AddIdentityServer` or `AddBff` method, and use the overload that accepts a lambda expression to configure the `LicenseKey` property. Program.cs ```csharp var keyVaultUrl = new Uri("https://.vault.azure.net/"); var secretClient = new Azure.Security.KeyVault.Secrets.SecretClient( keyVaultUrl, new Azure.Identity.DefaultAzureCredential() ); KeyVaultSecret licenseKeySecret = secretClient.GetSecret(""); var licenseKey = licenseKeySecret.Value; // Inject the secret (license key) into the IdentityServer configuration builder.Services.AddIdentityServer(options => { options.LicenseKey = licenseKey; }); ``` If you are using [Azure App Configuration](https://azure.microsoft.com/products/app-configuration/), you can use a similar approach to load the license key into your application host. ----- # Logging Fundamentals > General guidance on configuring logging for Duende Software products using Microsoft.Extensions.Logging and Serilog. All Duende Software products ([IdentityServer](/identityserver/), [Backend for Frontend (BFF)](/bff/), [Access Token Management](/accesstokenmanagement/), etc.) use the standard logging facilities provided by ASP.NET Core (`Microsoft.Extensions.Logging`). This means they integrate seamlessly with whatever logging provider you choose for your application. This guide provides general instructions for setting up logging that apply to all our products. Sensitive data in logs **`Trace` and `Debug` logs may contain sensitive information**, including token values, token hashes, and personally identifiable information (PII). Never enable these levels in production unless you are actively diagnosing an issue under controlled conditions and have taken steps to secure your log output. ## Log Levels [Section titled “Log Levels”](#log-levels) We follow the [standard Microsoft guidelines for log levels](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/logging#log-level). The spectrum below shows where each level sits on the operational urgency scale, with a quick action guide for each. TraceDev only **What to do:** Do not enable unless Duende Support requests it. Expect sensitive data like tokens and PII. DebugDev/Staging **What to do:** Enable temporarily during development or debugging. Disable before deploying to production. May contain sensitive data. InformationMonitor **What to do:** No immediate action needed. Use for auditing and request correlation in production. WarningInvestigate **What to do:** Review when convenient. May indicate misconfiguration or an edge case affecting some users. ErrorAct soon **What to do:** Investigate promptly. Something failed. Determine if users are affected and check for recurring patterns. CriticalAct now! **What to do:** Act immediately. The system may be unable to issue tokens. Escalate to your team or contact Duende Support. ← CalmUrgent → | Level | Urgency | Action | | ----------- | ----------- | ----------------------------------------------------------------------------- | | Trace | Dev only | Support-requested diagnostics only. Contains sensitive data. | | Debug | Dev/Staging | Enable in dev/staging. Disable before production. May contain sensitive data. | | Information | Monitor | Normal operations. Good default for production. | | Warning | Investigate | Review when convenient. May indicate misconfiguration. | | Error | Act soon | Investigate promptly. Something failed. | | Critical | Act now! | Act immediately. System may be down. | Scroll down for detailed guidance on what each level means, who it affects, and what you should do when you see it. ### Trace [Section titled “Trace”](#trace) **Meaning:** Extremely detailed diagnostic information intended for developers debugging complex, hard-to-reproduce issues. **Customer impact:** None directly. Trace is never enabled in production under normal circumstances. **Action:** 1. Do **not** enable in production unless [Duende Support](/general/support-and-issues/) explicitly requests it. 2. If you must enable it, use a scoped namespace (e.g., `Duende.IdentityServer`) to limit output. 3. Secure and rotate your logs immediately after capturing diagnostics. 4. Disable as soon as the issue is diagnosed. **Example:** ```plaintext [Trace] Duende.IdentityServer.Validation.TokenValidator Validating JWT token: eyJhbGciOiJSUzI1NiIs... ``` *** ### Debug [Section titled “Debug”](#debug) **Meaning:** Internal flow details: why decisions were made, which code paths were taken (e.g., policy evaluation, token validation steps). **Customer impact:** None directly. Debug is for developer understanding, not user-facing operations. **Action:** 1. Enable during local development or staging to understand application behavior. 2. Use `appsettings.Development.json` to isolate Debug logging from production config. 3. Disable before deploying to production, or set an expiry reminder. **Example:** ```plaintext [Debug] Duende.IdentityServer.ResponseHandling.AuthorizeResponseGenerator Creating authorization code response for client 'spa-app' ``` *** ### Information [Section titled “Information”](#information) **Meaning:** High-level events that track the normal flow of the application: requests starting, tokens issued, sessions created. **Customer impact:** Minimal. These are expected events. Absence of expected Information logs may indicate a problem. **Action:** 1. No immediate action required. This is normal operational noise. 2. Use Information logs for auditing user activity and correlating requests by ID. 3. This is often the recommended default level for production. **Example:** ```plaintext [Information] Duende.IdentityServer.Hosting.IdentityServerMiddleware Invoking IdentityServer endpoint: /connect/token (TokenEndpoint) ``` *** ### Warning [Section titled “Warning”](#warning) **Meaning:** Unexpected events that did not stop the application, but may indicate misconfiguration, an edge case, or degraded behavior. **Customer impact:** Possible. Some users may be experiencing issues. Investigate to confirm impact. **Action:** 1. Review the warning message and context (correlation ID, client ID, user). 2. Check if the warning is recurring or isolated. Recurring warnings deserve prompt attention. 3. Common causes: invalid client configuration, deprecated settings, transient infrastructure issues. 4. If in doubt, open a support ticket with Duende. **Example:** ```plaintext [Warning] Duende.IdentityServer.Validation.ClientSecretValidator Client secret validation failed for client 'legacy-app' ``` *** ### Error [Section titled “Error”](#error) **Meaning:** An operation failed and could not recover. An exception was thrown and not handled gracefully. **Customer impact:** Likely. A user request probably failed. Investigate promptly. **Action:** 1. Check the full stack trace in your log sink. 2. Correlate with the request ID or user subject ID to identify scope. 3. Determine if the error is recurring or isolated. 4. If recurring: escalate to your team and open a [Duende Support](/general/support-and-issues/) ticket if needed. 5. Check whether downstream dependencies (database, external IdP) are healthy. **Example:** ```plaintext [Error] Duende.IdentityServer.Validation.TokenRequestValidator Failed validation of token request: invalid_grant ``` *** ### Critical [Section titled “Critical”](#critical) **Meaning:** A catastrophic failure that requires immediate attention. The system may be partially or fully unable to function. **Customer impact:** High. Users are likely unable to authenticate or obtain tokens. **Action:** 1. **Act immediately.** Page your on-call team. 2. Check startup logs. Critical events often occur at startup (missing signing key, missing store implementation). 3. Verify database connectivity and key material availability. 4. Review deployment changes made immediately before the issue started. 5. Contact [Duende Support](/general/support-and-issues/) with your logs if the cause is unclear. **Example:** ```plaintext [Critical] Duende.IdentityServer.Startup No signing key material found. IdentityServer cannot issue tokens. ``` *** ## Environment Configuration [Section titled “Environment Configuration”](#environment-configuration) Use this table as a quick reference for which log levels to enable in each environment: | Level | Development | Staging | Production | | --------------- | ----------------------------------------- | ----------------------------------------- | --------------------------------------------------- | | **Trace** | ⚠️ Temporarily, for active investigations | ❌ Disable | ❌ Disable (Enable under extreme circumstances only) | | **Debug** | ✅ Recommended | ⚠️ Temporarily, for active investigations | ❌ Disable | | **Information** | ✅ | ✅ Recommended | ⚠️ Temporarily, for active investigations | | **Warning** | ✅ | ✅ | ✅ Recommended | | **Error** | ✅ | ✅ | ✅ | | **Critical** | ✅ | ✅ | ✅ (with alerts) | Tip In production, set your minimum log level to `Warning` to reduce volume while still capturing actionable events. Drop to `Information` when investigating a reported issue, and back to `Warning` when resolved. ## Setup for Microsoft.Extensions.Logging [Section titled “Setup for Microsoft.Extensions.Logging”](#setup-for-microsoftextensionslogging) This is the default logging provider for ASP.NET Core. If you haven’t configured a third-party logger, this is what you are using. You can configure log levels in your `appsettings.json` file. To get detailed logs from Duende products, you often want to set the `Duende` namespace (or specific sub-namespaces) to `Debug`. appsettings.json ```json { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information", // Enable Debug logs for all Duende products "Duende": "Debug" } } } ``` ## Setup for Serilog [Section titled “Setup for Serilog”](#setup-for-serilog) [Serilog](https://serilog.net) is a popular structured logging library for .NET. We highly recommend it for its flexibility and rich sink ecosystem (Console, File, Seq, Elasticsearch, etc.). ### 1. Installation [Section titled “1. Installation”](#1-installation) Install the necessary packages: ```bash dotnet add package Serilog.AspNetCore ``` ### 2. Configuration In `Program.cs` [Section titled “2. Configuration In Program.cs”](#2-configuration-in-programcs) Configure Serilog early in your application startup to capture all logs, including startup errors. Program.cs ```csharp using Serilog; var builder = WebApplication.CreateBuilder(args); // Configure Serilog builder.Host.UseSerilog((ctx, lc) => lc .WriteTo.Console(outputTemplate: "[{Timestamp:HH:mm:ss} {Level}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}") .Enrich.FromLogContext() .ReadFrom.Configuration(ctx.Configuration)); var app = builder.Build(); app.UseSerilogRequestLogging(); // Optional: cleaner HTTP request logging // ... rest of your pipeline ``` ### 3. Configuration In `appsettings.json` [Section titled “3. Configuration In appsettings.json”](#3-configuration-in-appsettingsjson) You can then control log levels via `appsettings.json`. This approach allows you to change log levels without recompiling your code. ```json { "Serilog": { "MinimumLevel": { "Default": "Information", "Override": { "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information", "System": "Warning", // Enable detailed logging for Duende products "Duende": "Debug" } } } } ``` ## Troubleshooting Specific Products and Components [Section titled “Troubleshooting Specific Products and Components”](#troubleshooting-specific-products-and-components) If you are debugging a specific component, you can target its namespace to reduce noise. | Product | Namespace | | --------------------------- | ------------------------------ | | **IdentityServer** | `Duende.IdentityServer` | | **BFF** | `Duende.Bff` | | **User Management** | `Duende.UserManagement` | | **Access Token Management** | `Duende.AccessTokenManagement` | Example `appsettings.json` for debugging only BFF interactions: ```json "Duende.Bff": "Debug", "Duende.IdentityServer": "Information" ``` ## Product-Specific Guides [Section titled “Product-Specific Guides”](#product-specific-guides) Each Duende product has its own logging page with product-specific configuration namespaces, key events to watch for, and advanced topics. [IdentityServer Logging](/identityserver/diagnostics/logging)Exception filtering, OpenTelemetry integration, and key events for IdentityServer. [User Management Logging](/identityserver/identity/user-management/logging)Log categories by feature area: passwords, passkeys, OTP, TOTP, profiles, and more. [Access Token Management Logging](/accesstokenmanagement/advanced/logging)Configuring log output for the Access Token Management library. [OidcClient Logging](/identitymodel-oidcclient/logging)Wiring up ILoggerFactory to OidcClient for native and mobile app diagnostics. ----- # Security Best Practices > A comprehensive guide to security practices and procedures used in Duende Software development lifecycle This document describes how the integrity of software produced by Duende Software is maintained during the software development life cycle. ## Data processing [Section titled “Data processing”](#data-processing) Our products are off-the shelf downloadable developer components. They are not managed services or SaaS - nor do we store, have access to, or process any of our customers’ data or their customers’ data. ## Systems Access [Section titled “Systems Access”](#systems-access) * Multiple systems are used in the development life cycle, including GitHub, NuGet, and Microsoft Azure Key Vault. * Multi-factor authentication is required for all services mentioned above. * Only a limited subset of Duende Software employees act as administrators for each system. ## Software Development [Section titled “Software Development”](#software-development) * All code is stored in [GitHub](https://github.com/duendesoftware). * Any code added to a project must be added via pull request. * At least one other staff member must review a pull request before it can be merged to a release branch. * Static code security analysis is performed for every check-in (using GitHub [CodeQL](https://codeql.github.com/)). ## Testing [Section titled “Testing”](#testing) * Automated test suites are run on code in every pull request branch. * Pull requests cannot be merged if the automated test suite fails. ## Deployment [Section titled “Deployment”](#deployment) * Merging a pull request does not immediately release new features to users, this requires an additional release step. * All compiled software packages with associated source are available as GitHub releases. * Compiled software libraries (such as Duende IdentityServer) are published to [NuGet](https://www.nuget.org/). * Packages must be pushed to NuGet by a Duende Software staff member only after additional validation by the staff member. * All NuGet packages are signed with a code signing certificate * The private key (RSA 4096 bits) is stored in Azure Key Vault. * The private key never leaves Key Vault and the signature process is performed by Key Vault. * NuGet will validate the package signature with Duende’s public key to verify they were legitimately built by Duende Software and have not been compromised or tampered with. * NuGet client tooling can be configured to accept signed packages only. * Once on NuGet, the package is available for end users to update their own solutions. * End users still must take explicit action to upgrade after reviewing the package’s release notes. ## Vulnerability Management Process [Section titled “Vulnerability Management Process”](#vulnerability-management-process) * Potential security vulnerabilities can be responsibly disclosed via our [contact form](https://duendesoftware.com/contact/general). * We guarantee to reply within two US business days. * All licenses include a security notification service. * Whenever a medium severity or higher security vulnerability has been confirmed and fixed, customers will get a private update prior to public release. * We will publish an official advisory ## Dependencies [Section titled “Dependencies”](#dependencies) IdentityServer has two dependencies: * [Microsoft .NET](https://dot.net) * [IdentityModel](https://github.com/IdentityModel) * maintained by Duende Software using the same principles as outlined above ## Certification [Section titled “Certification”](#certification) Duende IdentityServer is a [certified](https://openid.net/certification/) implementation of OpenID Connect. ## Package Signing [Section titled “Package Signing”](#package-signing) NuGet packages published by Duende are cryptographically signed to ensure their authenticity and integrity. Our certificate is signed by DigiCert, which is a widely trusted certificate authority and installed by default in most environments. This means that in many circumstances, the NuGet tools can validate our packages’ signatures automatically. However, some environments (notably the dotnet sdk docker image which is sometimes used in build pipelines) do not trust the certificate. In that case, it might be necessary to add the root certificate to NuGet’s code signing certificate bundle. * Packages released after January 1, 2025 (IdentityServer 7.1+): Use DigiCert’s [root certificate](https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA2562021CA1.crt.pem) ( PEM). * Packages released before January 1, 2025: Use Sectigo’s [root certificate](http://crt.sectigo.com/SectigoPublicCodeSigningRootR46.p7c) (P7C). #### Trusting The DigiCert Certificate [Section titled “Trusting The DigiCert Certificate”](#trusting-the-digicert-certificate) Here is an example of how to configure NuGet to trust the DigiCert root CA on the dotnet sdk docker image. This applies for Duende packages released *`after`* January 1, 2025, such as IdentityServer 7.1 and newer versions. Note the dotnet sdk docker image already includes the tools used in this section. If you are using another container image, make sure the following tools are available in the image: `wget`, `openssl`, `cat`, and the .NET SDK. First, get the DigiCert certificate: Terminal ```bash wget https://cacerts.digicert.com/DigiCertTrustedG4CodeSigningRSA4096SHA2562021CA1.crt.pem ``` Next, you validate that the thumbprint of the certificate is correct. Bootstrapping trust in a certificate chain can be challenging. Fortunately, most desktop environments already trust this certificate, so you can compare the downloaded certificate’s thumbprint to the thumbprint of the certificate on a machine that already trusts it. You should verify this independently, but for your convenience, the thumbprint is `8F:B2:8D:D3:CF:FA:5D:28:6E:7C:71:8A:A9:07:CB:4F:9B:17:67:C2`. You can check the thumbprint of the downloaded certificate with openssl: Terminal ```bash openssl x509 -in DigiCertTrustedG4CodeSigningRSA4096SHA2562021CA1.crt.pem -fingerprint -sha1 -noout ``` Then append that PEM to the certificate bundle at `/usr/share/dotnet/sdk/9.0.102/trustedroots/codesignctl.pem`: Terminal ```bash cat DigiCertTrustedG4CodeSigningRSA4096SHA2562021CA1.crt.pem >> /usr/share/dotnet/sdk/9.0.102/trustedroots/codesignctl.pem ``` After that, NuGet packages signed by Duende can be successfully verified, even if they are not distributed by NuGet.org: Terminal ```bash dotnet nuget verify Duende.IdentityServer.7.1.x.nupkg ``` #### Trusting The Sectigo Certificate [Section titled “Trusting The Sectigo Certificate”](#trusting-the-sectigo-certificate) Here is an example of how to configure NuGet to trust the Sectigo root CA on the dotnet sdk docker image for Duende packages released *`before`* January 1, 2025 Note the dotnet sdk docker image already includes the tools used in this section. If you are using another container image, make sure the following tools are available in the image: `wget`, `openssl`, `cat`, and the .NET SDK. First, get the Sectigo certificate and convert it to PEM format: Terminal ```bash wget http://crt.sectigo.com/SectigoPublicCodeSigningRootR46.p7c openssl pkcs7 -inform DER -outform PEM -in SectigoPublicCodeSigningRootR46.p7c -print_certs -out sectigo.pem ``` Next, you should validate that the thumbprint of the certificate is correct. Bootstrapping trust in a certificate chain can be challenging. Fortunately, most desktop environments already trust this certificate, so you can compare the downloaded certificate’s thumbprint to the thumbprint of the certificate on a machine that already trusts it. You should verify this independently, but for your convenience, the thumbprint is `CC:BB:F9:E1:48:5A:F6:3C:E4:7A:BF:8E:9E:64:8C:25:04:FC:31:9D`. You can check the thumbprint of the downloaded certificate with openssl: Terminal ```bash openssl x509 -in sectigo.pem -fingerprint -sha1 -noout ``` Then append that PEM to the certificate bundle at `/usr/share/dotnet/sdk/8.0.303/trustedroots/codesignctl.pem`: Terminal ```bash cat sectigo.pem >> /usr/share/dotnet/sdk/8.0.303/trustedroots/codesignctl.pem ``` After that, NuGet packages signed by Duende can be successfully verified, even if they are not distributed by NuGet.org: Terminal ```bash dotnet nuget verify Duende.IdentityServer.7.0.x.nupkg ``` ----- # Support & Issues > Comprehensive guide for accessing source code, reporting issues, and obtaining support for Duende products. This document provides information on accessing Duende’s source code, issue tracking, and community forums for support and discussions. It also outlines support policies, including priority support for enterprise users, and the procedure to report security vulnerabilities. ## Source Code [Section titled “Source Code”](#source-code) You can find all source code for Duende Products and its supporting repos in our [organization](https://github.com/duendesoftware). [Source Code](https://github.com/duendesoftware)Learn more about Duende's codebase ## Issue Tracker [Section titled “Issue Tracker”](#issue-tracker) Join our [developer community forum](https://github.com/DuendeSoftware/community/discussions) to ask questions and discuss potential bugs. Follow our [product releases](https://github.com/DuendeSoftware/products/releases) and get notified of new releases as soon as they are published. [Release Notes](https://github.com/DuendeSoftware/products/releases)See what's new in the latest releases ## Support [Section titled “Support”](#support) Duende Software offers [three support tiers](https://duendesoftware.com/support) designed to meet organizations at their stage of growth and operational complexity. Each tier builds on the last, providing a clear path from community-driven help to dedicated, high-touch engagement for mission-critical deployments. ### Community Support [Section titled “Community Support”](#community-support) **License: Free, Community, Lite, Starter (legacy), Business (legacy)** Community Support connects users with a network of fellow developers and Duende team members through our public discussion forums. Whether you’re troubleshooting an integration, exploring a new feature, or looking to share knowledge, the community is an active and knowledgeable resource. To get help, start a discussion [on the developer community forum](https://github.com/DuendeSoftware/community/discussions). | Community Support Overview | | | :------------------------- | :------------------------------------------------------------------------------------------ | | Support Channel | [Public developer community forum](https://github.com/DuendeSoftware/community/discussions) | | SLA | None | [Get support on GitHub from the Duende community](https://github.com/DuendeSoftware/community/discussions)Duende Developer Community Forum ### Priority Support [Section titled “Priority Support”](#priority-support) **License: Standard, Advanced, Enterprise (legacy)** [Priority Support](https://duendesoftware.com/license/PrioritySupportLicense.pdf) provides direct access to the Duende team through a dedicated email channel, with guaranteed response times tied to your license tier. For complex issues requiring deeper investigation, escalation calls are available – a focused video session where our team works alongside yours to diagnose and resolve product issues. | Priority Support Overview | | | | :------------------------ | :--------------------------- | :---------------------------- | | **License** | Standard | Advanced, Enterprise (legacy) | | **Support Channel** | Dedicated email channel | Dedicated email channel | | **Response SLA** | 2 business day response | 2 business day response | | **Escalation Calls** | Up to 2 escalations per year | Up to 4 escalations per year | Note Escalation calls are development-focused pairing sessions, not ongoing operational support or on-call coverage. Response times are measured against standard US business days. Observed holidays and other events are excluded. [Priority Support PDF](https://duendesoftware.com/license/PrioritySupportLicense.pdf)Learn more about Priority Support ### Premium Support [Section titled “Premium Support”](#premium-support) **License: Standard, Advanced, Custom, Enterprise (legacy)** Premium Support is designed for organizations with mission-critical or complex identity infrastructure where downtime presents significant business risk. It provides the benefits of Priority Support alongside a dedicated, proactive, and high-touch engagement model. This tier offers enhanced strategic continuity and the quickest guaranteed response times. **Dedicated Technical Account Manager (TAM):** Your TAM serves as a named point of contact who develops deep familiarity with your implementation, architecture, and business needs. Rather than re-explaining your environment with every interaction, your TAM brings continuity and context to every conversation, whether you’re onboarding, upgrading, or troubleshooting. **1 Business Day SLA Response:** Issues are acknowledged within 1 business day. This ensures a guaranteed response window regardless of when a problem surfaces, which is vital for organizations where swift acknowledgement is critical. **Priority Escalation:** Premium customers receive elevated priority for escalation calls, ensuring video pairing sessions are scheduled with urgency rather than standard queue prioritization. **Proactive Engagement:** Your TAM will periodically check in to review upcoming releases, flag breaking changes relevant to your configuration, and ensure you’re positioned to take advantage of new capabilities before they become blockers. | Premium Support Overview | | | :------------------------------------------ | :---------------------------------------------------------- | | **Support Channel** | Dedicated email channel + Named Technical Account Manager | | **SLA** | 1 business day response | | **Escalation Calls** | Up to 4 per year, priority scheduling | | **Account Manager** | Named TAM with deep familiarity of your environment | | **Proactive Engagement** | Periodic check-ins, release reviews, breaking change alerts | | **Support for Duende open-source packages** | Included | ## Supported Versions [Section titled “Supported Versions”](#supported-versions) Duende differentiates between two categories of NuGet packages: **Product packages** and **Component packages**. **Product packages** are the primary, user-facing products: IdentityServer and BFF. The following support rules apply: * **Major versions** are supported until the end of the associated .NET SDK lifecycle. The “associated” .NET version is determined by the target framework(s) supported at the time of release. A package can target multiple .NET versions. For example, if a release ships with support for both .NET 8 and .NET 10, it is associated with both, and support continues until the last associated .NET version reaches end-of-life. * **Minor versions** receive security fixes until the end of the associated .NET SDK lifecycle. * Product packages do **not** follow Semantic Versioning (SemVer); minor releases may contain breaking API changes. Internal namespaces Types in `Internal` namespaces, even when exposed as `public`, are intended for internal use by Duende. No bugfixes, security fixes or product support is provided for these types, and these types may change between versions. **Component packages** are transitive dependencies and supporting libraries, such as User Management, Storage, Jobs, and similar packages. The following support rules apply: * The **current major and minor** versions are supported for both bug fixes and security fixes. * Component packages follow Semantic Versioning (SemVer). The following sections track release and end of support dates for Duende product packages. ### Duende IdentityServer [Section titled “Duende IdentityServer”](#duende-identityserver) Duende IdentityServer v8 Note v8 and later follow the [Product package support model](#supported-versions) described above. | Version | Release Date | Supported .NET Platforms | Support End-of-Life | | ------- | ------------ | ------------------------ | ------------------- | | 8.0 | June 2, 2026 | .NET 10 | November 14, 2028 | Duende IdentityServer v7 | Version | Release Date | Supported .NET Platforms | Support End-of-Life | | ------- | ---------------- | ------------------------ | ------------------- | | 7.4 | December 2, 2025 | .NET 10 | November 14, 2028 | | | | .NET 9 | November 10, 2026 | | | | .NET 8 | November 10, 2026 | | 7.3 | August 14, 2025 | .NET 9 | November 10, 2026 | | | | .NET 8 | November 10, 2026 | | 7.2 | March 18, 2025 | .NET 9 | November 10, 2026 | | | | .NET 8 | November 10, 2026 | | 7.1 | January 15, 2025 | .NET 9 | November 10, 2026 | | | | .NET 8 | November 10, 2026 | | 7.0 | January 24, 2024 | .NET 8 | November 10, 2026 | Duende IdentityServer v6 Caution Duende IdentityServer v6 is no longer supported. | Version | Release Date | Supported .NET Platforms | Support End-of-Life | | ------- | ----------------- | ------------------------ | ------------------- | | 6.3 | May 16, 2023 | .NET 7 | May 14, 2024 | | | | .NET 6 | November 12, 2024 | | 6.2 | November 22, 2022 | .NET 7 | May 14, 2024 | | | | .NET 6 | November 12, 2024 | | 6.1 | May 20, 2022 | .NET 6 | November 12, 2024 | | 6.0 | January 13, 2022 | .NET 6 | November 12, 2024 | Duende IdentityServer v5 Caution Duende IdentityServer v5 is no longer supported. | Version | Release Date | Supported .NET Platforms | Support End-of-Life | | ------- | ---------------- | ------------------------ | ------------------- | | 5.2 | May 19, 2021 | .NET 5 | May 10, 2022 | | | | .NET Core 3.1 | December 13, 2022 | | 5.1 | March 25, 2021 | .NET 5 | May 10, 2022 | | | | .NET Core 3.1 | December 13, 2022 | | 5.0 | January 14, 2021 | .NET 5 | May 10, 2022 | | | | .NET Core 3.1 | December 13, 2022 | ### Duende Backend For Frontend (BFF) [Section titled “Duende Backend For Frontend (BFF)”](#duende-backend-for-frontend-bff) Duende BFF v4 | Version | Release Date | Supported .NET Platforms | Support End-of-Life | | ------- | ---------------- | ------------------------ | ------------------- | | 4.1 | January 29, 2026 | .NET 10 | November 14, 2028 | | | | .NET 9 | November 10, 2026 | | | | .NET 8 | November 10, 2026 | | 4.0 | December 2, 2025 | .NET 10 | November 14, 2028 | | | | .NET 9 | November 10, 2026 | | | | .NET 8 | November 10, 2026 | Duende BFF v3 | Version | Release Date | Supported .NET Platforms | Support End-of-Life | | ------- | ---------------- | ------------------------ | ------------------- | | 3.1 | December 2, 2025 | .NET 10 | November 14, 2028 | | | | .NET 9 | November 10, 2026 | | | | .NET 8 | November 10, 2026 | | 3.0 | March 17, 2025 | .NET 9 | November 10, 2026 | | | | .NET 8 | November 10, 2026 | Duende BFF v2 | Version | Release Date | Supported .NET Platforms | Support End-of-Life | | ------- | ----------------- | ------------------------ | ------------------- | | 2.3 | December 20, 2024 | .NET 9 | November 10, 2026 | | | | .NET 8 | November 10, 2026 | | 2.2 | April 1, 2024 | .NET 8 | November 10, 2026 | | | | .NET 6 | November 12, 2024 | | 2.1 | June 27, 2022 | .NET 6 | November 12, 2024 | | 2.0 | November 11, 2022 | .NET 6 | November 12, 2024 | Duende BFF v1 Caution Duende BFF v1 is no longer supported. | Version | Release Date | Supported .NET Platforms | Support End-of-Life | | ------- | ----------------- | ------------------------ | ------------------- | | 1.2 | April 1, 2022 | .NET 6 | November 12, 2024 | | | | .NET 5 | May 10, 2022 | | | | .NET Core 3.1 | December 13, 2022 | | 1.1 | December 16, 2021 | .NET 6 | November 12, 2024 | | | | .NET 5 | May 10, 2022 | | | | .NET Core 3.1 | December 13, 2022 | | 1.0 | October 24, 2021 | .NET 5 | May 10, 2022 | | | | .NET Core 3.1 | December 13, 2022 | ## Reporting a security vulnerability [Section titled “Reporting a security vulnerability”](#reporting-a-security-vulnerability) [Security issues and bugs should be reported privately here](https://duendesoftware.com/contact/general). You should receive a response within **two business days**. [Report a security vulnerability](https://duendesoftware.com/contact/general)privately report a security vulnerability ----- # Duende IdentityModel > Duende.IdentityModel for OpenID Connect and OAuth 2.0 related protocol operations, providing object models and utilities for identity-related operations The `Duende.IdentityModel` package is the base library for OpenID Connect and OAuth 2.0 related protocol operations. It provides an object model to interact with the endpoints defined in the various OAuth and OpenId Connect specifications. The types included represent the requests and responses, and constants defined in the specifications, such as standard scope, claim, and parameter names. The library also contains extension methods to invoke requests and other convenience methods for performing common identity related operations. [GitHub Repository](https://github.com/DuendeSoftware/foss/tree/main/identity-model)View the source code for this library on GitHub. [NuGet Package](https://www.nuget.org/packages/Duende.IdentityModel/)View the package on NuGet.org. ----- # Duende IdentityModel OIDC Client > A certified OpenID Connect relying party library for building native clients with .NET, supporting various UI frameworks and authentication flows Tip **`Duende.IdentityModel.OidcClient` is a [certified](https://openid.net/certification/) OpenID Connect relying party implementation.** The `Duende.IdentityModel.OidcClient` library is a certified OpenID Connect relying party and implements [RFC 8252](https://tools.ietf.org/html/rfc8252/), “OAuth 2.0 for native Applications”. The `Duende.IdentityModel.OidcClient.Extensions` library provides support for [DPoP](https://datatracker.ietf.org/doc/html/rfc9449) extensions to Duende.IdentityModel.OidcClient for sender-constraining tokens. ## Use Cases [Section titled “Use Cases”](#use-cases) OidcClient targets .NET Standard, making it suitable for .NET and .NET Framework. It can be used to build OIDC native clients with a variety of .NET UI tools. * .NET MAUI * WPF with the system browser * WPF with an embedded browser * WinForms with an embedded browser * Cross-platform Console Applications (relies on kestrel for processing the callback) * Windows Console Applications (relies on an HttpListener - a wrapper around the windows HTTP.sys driver) * Windows Console Applications using custom uri schemes ## License and Feedback [Section titled “License and Feedback”](#license-and-feedback) `Duende.IdentityModel.OidcClient` is released as open source under the [Apache 2.0 license](https://github.com/DuendeSoftware/foss/blob/main/LICENSE). Bug reports and contributions are welcome at [the GitHub repository](https://github.com/DuendeSoftware/foss). [GitHub Repository](https://github.com/DuendeSoftware/foss/tree/main/identity-model-oidc-client)View the source code for this library on GitHub. [NuGet Package](https://www.nuget.org/packages/Duende.IdentityModel.OidcClient/)View the package on NuGet.org. ----- # Demonstrating Proof-of-Possession (DPoP) > Learn how to leverage Demonstrating Proof-of-Possession when using OidcClient to build a native OIDC client. [DPoP](https://datatracker.ietf.org/doc/html/rfc9449) specifies how to bind an asymmetric key stored within a JSON Web Key (JWK) to an access token. This will make the access token bound to the key such that if the access token were to leak, it cannot be used without also having access to the private key of the corresponding JWK. The `Duende.IdentityModel.OidcClient.Extensions` library adds supports for DPoP to OidcClient. Note Duende.IdentityModel.OidcClient Version 7.1.0 now supports combining DPoP with [Client Assertions](/identitymodel/endpoints/client-assertions/). ## DPoP Key [Section titled “DPoP Key”](#dpop-key) Before we begin, your application needs to have a DPoP key in the form of a JSON Web Key (or JWK). According to the [DPoP specification](https://datatracker.ietf.org/doc/html/rfc9449), this key needs to use an asymmetric algorithm (“RS”, “ES”, or “PS” style). Note The client application is responsible for creating the DPoP key, rotating it, and managing its lifetime. For as long as there are access tokens (and possibly refresh tokens) bound to a DPoP key, that key needs to remain available to the client application. You can create a JWK in .NET using the `Duende.IdentityModel.OidcClient.Extensions` library. The `JsonWebKeys` class has several static methods to help with creating JWKs using various algorithms. Program.cs ```csharp using Duende.IdentityModel.OidcClient.DPoP; // Creates a JWK using the PS256 algorithm: var jwk = JsonWebKeys.CreateRsaJson(); Console.WriteLine(jwk); ``` Caution In a production scenario, you’ll want to store this JWK in a secure location and use ASP.NET’s [data protection](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/) to further protect the JWK. See [our data protection guide](/identityserver/deployment/#aspnet-core-data-protection) for more information. ## Initializing the OIDC client with DPoP support [Section titled “Initializing the OIDC client with DPoP support”](#initializing-the-oidc-client-with-dpop-support) We will need to extend the `OidcClientOptions` before we can use DPoP. After creating the `OidcClientOptions` to connect our client application with the Identity Provider, we retrieve a JWK to use for DPoP, and add that JWK to our `options` by calling the `ConfigureDPoP` extension method: Program.cs ```csharp using Duende.IdentityModel.OidcClient; using Duende.IdentityModel.OidcClient.DPoP; var options = new OidcClientOptions { Authority = "https://demo.duendesoftware.com", ClientId = "native.dpop", Scope = "openid profile email offline_access", // ... }; // creates a new JWK, or returns an existing one var jwk = GetDPoPJwk(); // Enable DPoP options.ConfigureDPoP(jwk); var oidcClient = new OidcClient(options); ``` ## Proof Tokens for the API [Section titled “Proof Tokens for the API”](#proof-tokens-for-the-api) Now that we’ve configured the `OidcClientOptions` with DPoP support and created an `OidcClient` instance, you can use this instance to create an `HttpMessageHandler` which will: * manage access and refresh tokens * add DPoP proof tokens to HTTP requests The `OidcClient` provides `CreateDPoPHandler` as a convenience method to create such a handler, which can be used with the .NET `HttpClient`. Program.cs ```csharp var sessionRefreshToken = "..."; // read from a previous session, if any var handler = oidcClient.CreateDPoPHandler(jwk, sessionRefreshToken); var apiClient = new HttpClient(handler); ``` For a full example, have a look at our [WPF with the system browser](https://github.com/DuendeSoftware/foss/tree/main/identity-model-oidc-client/samples/Wpf) sample. ----- # OIDC Client Automatic Mode > Learn how to implement automatic OAuth/OIDC authentication by encapsulating browser interactions using OidcClient OpenID Connect (OIDC) is an identity layer on top of the OAuth 2.0 protocol. It allows clients to verify the identity of the end-user based on the authentication performed by an authorization server, as well as obtain basic profile information. An essential part of the OIDC flow is the use of a browser to interact with the end-user and to obtain permissions to access protected resources. In the OidcClient library, you can encapsulate the browser interaction by implementing the [IBrowser](https://github.com/DuendeSoftware/foss/blob/main/identity-model-oidc-client/src/IdentityModel.OidcClient/Browser/IBrowser.cs) interface. Using `IBrowser` helps create a reusable component for all OIDC interaction. ```csharp // Copyright (c) Duende Software. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. namespace Duende.IdentityModel.OidcClient.Browser; /// /// Models a browser /// public interface IBrowser { /// /// Invokes the browser. /// /// The options. /// A token that can be used to cancel the request /// Task InvokeAsync(BrowserOptions options, CancellationToken cancellationToken = default); } ``` The `BrowserResult` represents the result of the browser interaction, including any OIDC payloads that are returned from the authentication server. ```csharp // Copyright (c) Duende Software. All rights reserved. // Licensed under the Apache License, Version 2.0. See LICENSE in the project root for license information. namespace Duende.IdentityModel.OidcClient.Browser; /// /// The result from a browser login. /// /// public class BrowserResult : Result { /// /// Gets or sets the type of the result. /// /// /// The type of the result. /// public BrowserResultType ResultType { get; set; } /// /// Gets or sets the response. /// /// /// The response. /// public string Response { get; set; } } ``` The `BrowserResult` class inherits from `Result`, which provides error handling properties: * `IsError` - Indicates whether the browser interaction resulted in an error * `Error` - The error code if an error occurred * `ErrorDescription` - A human-readable description of the error Browser is platform-specific The `IBrowser` implementation is specific to the platform and environment and must be provided by the host application. For example, a Windows-specific implementation will not work within a macOS, iOS, Android, or Linux environment. For a simple example, the following code shows how to use the [SystemBrowser](https://github.com/DuendeSoftware/foss/blob/main/identity-model-oidc-client/clients/ConsoleClientWithBrowser/SystemBrowser.cs) to invoke a browser on the host desktop platform. The `SystemBrowser` is a naive implementation that uses the [System.Diagnostics.Process](https://docs.microsoft.com/en-us/dotnet/api/system.diagnostics.process) class to start the system default browser. ```csharp var options = new OidcClientOptions { Authority = "https://demo.duendesoftware.com", ClientId = "native", RedirectUri = redirectUri, Scope = "openid profile api", Browser = new SystemBrowser() }; var client = new OidcClient(options); ``` Once the `IBrowser` is configured, the `LoginAsync` method can be invoked to start the authentication flow. ```csharp var result = await client.LoginAsync(); ``` ## Customizing the Login Request [Section titled “Customizing the Login Request”](#customizing-the-login-request) You can customize the login behavior by passing a `LoginRequest` object to `LoginAsync`: ```csharp var result = await client.LoginAsync(new LoginRequest { BrowserDisplayMode = DisplayMode.Hidden, BrowserTimeout = 30, FrontChannelExtraParameters = new Parameters { { "acr_values", "mfa" }, { "login_hint", "user@example.com" } } }); ``` | Property | Type | Description | | ----------------------------- | ------------- | --------------------------------------------------- | | `BrowserDisplayMode` | `DisplayMode` | Controls browser visibility (`Visible` or `Hidden`) | | `BrowserTimeout` | `int` | Timeout in seconds for the browser interaction | | `FrontChannelExtraParameters` | `Parameters` | Extra parameters for the authorization endpoint | | `BackChannelExtraParameters` | `Parameters` | Extra parameters for the token endpoint | Setting the `Browser` property reduces the need to process browser respones and to handle the `BrowserResult` directly. When using this automatic mode, the `LoginAsync` method will return a [`LoginResult`](https://github.com/DuendeSoftware/foss/blob/main/identity-model-oidc-client/src/IdentityModel.OidcClient/LoginResult.cs) which will contain a `ClaimsPrincipal` with the user’s claims along with the `IdentityToken` and `AccessToken`. ## LoginResult Properties [Section titled “LoginResult Properties”](#loginresult-properties) The `LoginResult` class inherits from `Result` (providing `IsError`, `Error`, `ErrorDescription`) and exposes the following properties: | Property | Type | Description | | ----------------------- | ------------------- | ----------------------------------------------------------- | | `User` | `ClaimsPrincipal` | The authenticated user’s claims principal | | `AccessToken` | `string` | The access token for calling protected APIs | | `IdentityToken` | `string` | The identity token containing user claims | | `RefreshToken` | `string` | The refresh token (if requested via `offline_access` scope) | | `AccessTokenExpiration` | `DateTimeOffset` | When the access token expires | | `AuthenticationTime` | `DateTimeOffset?` | When the user authenticated at the IdP | | `RefreshTokenHandler` | `DelegatingHandler` | Pre-configured handler for automatic token refresh | | `TokenResponse` | `TokenResponse` | The raw token endpoint response | ### Example Usage [Section titled “Example Usage”](#example-usage) ```csharp var result = await client.LoginAsync(); if (result.IsError) { Console.WriteLine($"Error: {result.Error} - {result.ErrorDescription}"); return; } // Access user claims var name = result.User.FindFirst("name")?.Value; Console.WriteLine($"Hello, {name}!"); // Use access token for API calls var apiClient = new HttpClient(); apiClient.SetBearerToken(result.AccessToken); // Or use the pre-configured refresh handler for automatic token refresh var apiClientWithRefresh = new HttpClient(result.RefreshTokenHandler); ``` ----- # OIDC Client DPoP Support > Learn how to use Demonstrating Proof of Possession (DPoP) with OidcClient for enhanced token security DPoP (Demonstrating Proof of Possession) is an extension to OAuth 2.0 that provides proof-of-possession for access tokens. It binds tokens to a specific cryptographic key, preventing token theft and replay attacks. [RFC 9449: OAuth 2.0 Demonstrating Proof of Possession](https://datatracker.ietf.org/doc/html/rfc9449)The official specification for DPoP ## Installation [Section titled “Installation”](#installation) DPoP support is provided by the Extensions package: ```bash dotnet add package Duende.IdentityModel.OidcClient.Extensions ``` ## Quick Start [Section titled “Quick Start”](#quick-start) ### 1. Generate a Proof Key [Section titled “1. Generate a Proof Key”](#1-generate-a-proof-key) Use the `JsonWebKeys` helper to create a key pair: ```csharp using Duende.IdentityModel.OidcClient.DPoP; // Create an RSA key (recommended for most scenarios) var proofKey = JsonWebKeys.CreateRsaJson(); // Or create an ECDSA key (smaller, faster) var proofKey = JsonWebKeys.CreateECDsaJson(); ``` ### 2. Configure DPoP on OidcClientOptions [Section titled “2. Configure DPoP on OidcClientOptions”](#2-configure-dpop-on-oidcclientoptions) ```csharp var options = new OidcClientOptions { Authority = "https://demo.duendesoftware.com", ClientId = "native.dpop", RedirectUri = "app://callback", Scope = "openid profile api" }; // Enable DPoP options.ConfigureDPoP(proofKey); var client = new OidcClient(options); ``` ### 3. Login and Make API Calls [Section titled “3. Login and Make API Calls”](#3-login-and-make-api-calls) ```csharp var loginResult = await client.LoginAsync(); if (!loginResult.IsError) { // Create a handler for DPoP-protected API calls var handler = client.CreateDPoPHandler( proofKey, loginResult.RefreshToken ); var apiClient = new HttpClient(handler); var response = await apiClient.GetAsync("https://api.example.com/resource"); } ``` ## API Reference [Section titled “API Reference”](#api-reference) ### JsonWebKeys [Section titled “JsonWebKeys”](#jsonwebkeys) Helper class for generating DPoP proof keys: | Method | Description | | ---------------------------- | ---------------------------------------------- | | `CreateRsa(algorithm)` | Creates an RSA `JsonWebKey` (default: PS256) | | `CreateRsaJson(algorithm)` | Creates an RSA key as JSON string | | `CreateECDsa(algorithm)` | Creates an ECDSA `JsonWebKey` (default: ES256) | | `CreateECDsaJson(algorithm)` | Creates an ECDSA key as JSON string | Key Storage Store the generated proof key securely. The same key must be used for the lifetime of the DPoP-bound tokens. If you lose the key, you’ll need to obtain new tokens. ### OidcClientExtensions.ConfigureDPoP [Section titled “OidcClientExtensions.ConfigureDPoP”](#oidcclientextensionsconfiguredpop) Configures the `OidcClient` to use DPoP for token requests: ```csharp // Using a JSON proof key string options.ConfigureDPoP(proofKey); // Using a custom proof token factory options.ConfigureDPoP( proofTokenFactory, tokenEndpointInnerHandler, // Optional: custom handler for token endpoint apiInnerHandler // Optional: custom handler for API calls ); ``` ### OidcClientExtensions.CreateDPoPHandler [Section titled “OidcClientExtensions.CreateDPoPHandler”](#oidcclientextensionscreatedpophandler) Creates an HTTP message handler for DPoP-protected API calls: ```csharp // Using a JSON proof key string var handler = client.CreateDPoPHandler(proofKey, refreshToken); // Using a custom proof token factory var handler = client.CreateDPoPHandler( proofTokenFactory, refreshToken, apiInnerHandler // Optional: custom inner handler ); ``` ### Custom Proof Token Factory [Section titled “Custom Proof Token Factory”](#custom-proof-token-factory) For advanced scenarios, implement `IDPoPProofTokenFactory`: ```csharp public class CustomProofTokenFactory : IDPoPProofTokenFactory { public DPoPProof CreateProofToken(DPoPProofRequest request) { // request.Url - The HTTP URL // request.Method - The HTTP method (GET, POST, etc.) // request.DPoPNonce - Server-provided nonce (if any) // request.AccessToken - The access token (for ath claim) var proofToken = // ... create JWT proof token return new DPoPProof { ProofToken = proofToken }; } } ``` #### DPoPProofRequest Properties [Section titled “DPoPProofRequest Properties”](#dpopproofrequest-properties) | Property | Type | Description | | ------------- | -------- | ------------------------------------------ | | `Url` | `string` | The HTTP URL of the request | | `Method` | `string` | The HTTP method (GET, POST, etc.) | | `DPoPNonce` | `string` | Server-provided nonce value | | `AccessToken` | `string` | The access token (for `ath` claim binding) | ### ProofTokenMessageHandler [Section titled “ProofTokenMessageHandler”](#prooftokenmessagehandler) Low-level handler that adds DPoP proof tokens to requests: ```csharp var handler = new ProofTokenMessageHandler( proofTokenFactory, innerHandler, logger // Optional: ILogger ); ``` ## DPoP Extensions for HttpRequestMessage [Section titled “DPoP Extensions for HttpRequestMessage”](#dpop-extensions-for-httprequestmessage) The `DPoPExtensions` class provides helper methods: | Method | Description | | ---------------------------------------- | --------------------------------------- | | `SetDPoPProofToken(request, proofToken)` | Adds the DPoP header to a request | | `GetDPoPNonce(response)` | Extracts the DPoP-Nonce from a response | | `GetDPoPUrl(request)` | Gets the URL for DPoP proof creation | ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Generate keys on device** - Create proof keys locally and store them securely 2. **Use appropriate key type** - RSA (PS256) for broad compatibility, ECDSA (ES256) for performance 3. **Handle nonce requirements** - The handler automatically handles server-provided nonces 4. **Persist keys securely** - Use platform-specific secure storage ----- # OIDC Client Logging > Learn how to configure and customize logging in OidcClient using Microsoft.Extensions.Logging.ILogger `OidcClient` logs errors, warnings, and diagnostic information using `Microsoft.Extensions.Logging.ILogger`, the standard .NET logging library. For log level definitions, environment guidance, and actionable next steps for each level, see the [Logging Fundamentals](/general/logging) guide. [Logging Fundamentals](/general/logging)Log level definitions, environment configuration table, and the log level anxiety spectrum. ## Configuration [Section titled “Configuration”](#configuration) ### Wiring Up a Logger Factory [Section titled “Wiring Up a Logger Factory”](#wiring-up-a-logger-factory) `OidcClient` does not use the ASP.NET Core dependency injection container directly — instead, you configure it by setting the `LoggerFactory` property on `OidcClientOptions`. This gives you full control over logging in native app, mobile, and console scenarios. Program.cs ```csharp using Duende.IdentityModel; using Duende.IdentityModel.OidcClient; var builder = Host.CreateApplicationBuilder(args); builder.Services.AddSingleton(svc => { var loggerFactory = svc.GetRequiredService(); var options = new OidcClientOptions { Authority = "https://demo.duendesoftware.com", ClientId = "interactive.public", Scope = "openid profile email offline_access", RedirectUri = "app://localhost/", PostLogoutRedirectUri = "app://localhost/", LoggerFactory = loggerFactory }; return new OidcClient(options); }); var app = builder.Build(); var client = app.Services.GetService(); ``` You can use any logging framework that integrates with `ILoggerFactory`, such as [Serilog](https://github.com/serilog/serilog-extensions-hosting). ### Log Level Configuration [Section titled “Log Level Configuration”](#log-level-configuration) `OidcClient` emits logs at `Trace`, `Debug`, `Information`, and `Error` levels. To control log output, set the `Duende.IdentityModel.OidcClient` namespace in your `appsettings.json`: appsettings.json ```json { "Logging": { "LogLevel": { "Default": "Information", "Microsoft": "Warning", "Microsoft.Hosting.Lifetime": "Information", "Duende.IdentityModel.OidcClient": "Error" } } } ``` ## What Gets Logged [Section titled “What Gets Logged”](#what-gets-logged) OidcClient emits structured log messages across several functional areas. Each message includes contextual parameters for effective filtering and troubleshooting. ### Login Flow [Section titled “Login Flow”](#login-flow) | Level | Message | Description | | ----------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- | --------------------------------------- | | Trace | LoginAsync | Beginning the login method | | Information | Starting authentication request. | Login flow initiated | | Information | Authentication request success. | Login completed successfully | | Trace | PrepareLoginAsync | Preparing login state for manual flow | | Trace | CreateAuthorizeStateAsync | Creating PKCE and state parameters | | Debug | The IdentityProvider contains a pushed authorization request endpoint. Automatically pushing authorization parameters. Use DisablePushedAuthorization to opt out. | PAR endpoint detected and being used | | Error | Failed to push authorization parameters | PAR request failed | | Trace | CreateAuthorizeUrl | Building the authorization URL | | Trace | CreateAuthorizeParameters | Assembling authorize request parameters | | Trace | AuthorizeAsync | Starting browser-based authorization | ### Response Processing [Section titled “Response Processing”](#response-processing) | Level | Message | Description | | ----------- | -------------------------------- | ------------------------------------- | | Trace | ProcessResponseAsync | Beginning response processing | | Information | Processing response. | Handling authorization response | | Debug | Authorize response: `{Response}` | Raw authorization response data | | Trace | ProcessCodeFlowResponseAsync | Processing authorization code flow | | Trace | RedeemCodeAsync | Exchanging code for tokens | | Trace | ValidateTokenResponse | Validating token endpoint response | | Error | `{Error}` | Authorization or token response error | ### Discovery Document [Section titled “Discovery Document”](#discovery-document) | Level | Message | Description | | ----- | ----------------------------------------------------------- | ---------------------------------------------- | | Trace | EnsureProviderInformation | Checking provider configuration | | Debug | Skipping refresh of discovery document. | Reusing cached discovery document | | Debug | Successfully loaded discovery document | Discovery document retrieved | | Debug | Loaded keyset from `{JwksUri}` | JSON Web Key Set location | | Debug | Keyset contains the following kids: `{Kids}` | Available signing key IDs | | Error | Error loading discovery document: `{ErrorType}` - `{Error}` | Discovery document retrieval failed | | Error | Issuer name is missing in provider information | Invalid provider configuration | | Error | Authorize endpoint is missing in provider information | Invalid provider configuration | | Error | Token endpoint is missing in provider information | Invalid provider configuration | | Error | Key set is missing in provider information | Invalid provider configuration (when required) | ### UserInfo [Section titled “UserInfo”](#userinfo) | Level | Message | Description | | ----- | --------------------------------------------------------------------------------- | ---------------------------------------- | | Trace | GetUserInfoAsync | Starting userinfo request | | Error | Error contacting userinfo endpoint: `{Error}` | Userinfo request failed | | Error | sub claim is missing from userinfo endpoint | Userinfo response missing required claim | | Error | sub claim from userinfo endpoint is different than sub claim from identity token. | Subject mismatch between tokens | ### Token Refresh [Section titled “Token Refresh”](#token-refresh) | Level | Message | Description | | ----- | --------------------------------------------------------- | -------------------------------------------------------------- | | Trace | RefreshTokenAsync | Starting token refresh | | Error | Failed on RefreshTokensAsync: `{Error}` - `{Description}` | Automatic token refresh failed (RefreshTokenDelegatingHandler) | ### Logout [Section titled “Logout”](#logout) | Level | Message | Description | | ----- | ------------------- | ---------------------------- | | Trace | CreateEndSessionUrl | Building the end session URL | ### Claims Processing [Section titled “Claims Processing”](#claims-processing) | Level | Message | Description | | ----- | -------------------------- | --------------------------------------- | | Trace | ProcessClaims | Merging claims from tokens and userinfo | | Debug | Claim: `{Type}`: `{Value}` | Individual claim being processed | ### Configuration [Section titled “Configuration”](#configuration-1) | Level | Message | Description | | ----- | ------------------ | ---------------------------------------- | | Trace | Effective options: | Followed by serialized OidcClientOptions | Sensitive Data At `Trace` level, the full `OidcClientOptions` configuration is serialized to logs, which can help diagnose configuration issues. Be cautious with `Trace` logging in production as it may expose sensitive configuration details. ----- # OIDC Client Logout > Learn how to implement logout flows with OidcClient including automatic and manual modes The `OidcClient` library supports OpenID Connect logout, allowing you to end the user’s session at the identity provider. Like login, logout can be performed in automatic or manual mode. ## Automatic Mode Logout [Section titled “Automatic Mode Logout”](#automatic-mode-logout) If you’ve configured an `IBrowser` implementation, you can use `LogoutAsync` for automatic logout: ```csharp var result = await client.LogoutAsync(); if (result.IsError) { Console.WriteLine($"Logout error: {result.Error}"); } ``` ### Customizing the Logout Request [Section titled “Customizing the Logout Request”](#customizing-the-logout-request) You can pass a `LogoutRequest` to customize the logout behavior: ```csharp var result = await client.LogoutAsync(new LogoutRequest { IdTokenHint = loginResult.IdentityToken, BrowserDisplayMode = DisplayMode.Hidden, BrowserTimeout = 30 }); ``` #### LogoutRequest Properties [Section titled “LogoutRequest Properties”](#logoutrequest-properties) | Property | Type | Description | | -------------------- | ------------- | ---------------------------------------------------------- | | `IdTokenHint` | `string` | The identity token to hint to the IdP which session to end | | `State` | `string` | Optional state parameter for the logout request | | `BrowserDisplayMode` | `DisplayMode` | Controls browser visibility (`Visible` or `Hidden`) | | `BrowserTimeout` | `int` | Timeout in seconds for the browser interaction | Include the Identity Token Always pass the `IdTokenHint` when possible. This allows the identity provider to identify which session to end without prompting the user for confirmation. ## Manual Mode Logout [Section titled “Manual Mode Logout”](#manual-mode-logout) For manual mode, use `PrepareLogoutAsync` to generate the logout URL: ```csharp var logoutUrl = await client.PrepareLogoutAsync(new LogoutRequest { IdTokenHint = loginResult.IdentityToken }); // Navigate the browser to logoutUrl manually // Handle the callback at PostLogoutRedirectUri ``` The method returns the fully-formed end session endpoint URL. After navigating the browser to this URL, the identity provider will end the session and redirect back to your configured `PostLogoutRedirectUri`. ## LogoutResult [Section titled “LogoutResult”](#logoutresult) The `LogoutResult` class inherits from `Result` and provides: | Property | Type | Description | | ------------------ | -------- | ----------------------------------------- | | `IsError` | `bool` | Whether the logout resulted in an error | | `Error` | `string` | The error code if an error occurred | | `ErrorDescription` | `string` | Human-readable error description | | `Response` | `string` | The raw response from the logout endpoint | ## Configuration [Section titled “Configuration”](#configuration) Ensure your `OidcClientOptions` includes the post-logout redirect URI: ```csharp var options = new OidcClientOptions { Authority = "https://demo.duendesoftware.com", ClientId = "native", RedirectUri = "app://callback", PostLogoutRedirectUri = "app://logout-callback", Scope = "openid profile", Browser = new SystemBrowser() }; ``` ----- # OIDC Client Manual Mode > Guide for implementing manual mode in OidcClient to handle browser interactions and token processing OpenID Connect is a protocol that allows you to authenticate users using a browser and involves browser-based interactions. When using this library you can choose between two modes: [automatic](/identitymodel-oidcclient/automatic/) and manual. We recommend using automatic mode when possible, but sometimes you need to use manual mode when you want to handle browser interactions yourself. With manual mode, `OidcClient` is still useful, as it helps with creating the necessary start URL and state parameters needed to complete an OIDC flow. You’ll need to handle all browser interactions yourself with custom code. This is beneficial for scenarios where you want to customize the browser experience or when you want to integrate with other platform-specific browser libraries. ```csharp var options = new OidcClientOptions { Authority = "https://demo.duendesoftware.com", ClientId = "native", RedirectUri = redirectUri, Scope = "openid profile api" }; var client = new OidcClient(options); // generate start URL, state, nonce, code challenge var state = await client.PrepareLoginAsync(); ``` The `state` object is of type `AuthorizeState` and contains everything you need to perform the browser interaction: | Property | Description | | -------------- | ---------------------------------------------------------------- | | `StartUrl` | The fully-formed authorization URL to navigate the browser to | | `State` | The state parameter for CSRF protection (must match on callback) | | `CodeVerifier` | The PKCE code verifier (needed for token exchange) | | `RedirectUri` | The redirect URI where the browser will return | When the browser work is done, `OidcClient` can take over to process the response, get the access/refresh tokens, contact userinfo endpoint etc.: ```csharp var result = await client.ProcessResponseAsync(data, state); ``` When using this manual mode, and processing the response, the `ProcessResponseAsync` method will return a [`LoginResult`](https://github.com/DuendeSoftware/foss/blob/main/identity-model-oidc-client/src/IdentityModel.OidcClient/LoginResult.cs) which will contain a `ClaimsPrincipal` with the user’s claims along with the `IdentityToken` and `AccessToken`. ----- # OidcClientOptions Reference > Complete reference for all OidcClientOptions configuration properties This page provides a complete reference for all `OidcClientOptions` properties. For a quick start, see [Automatic Mode](/identitymodel-oidcclient/automatic/) or [Manual Mode](/identitymodel-oidcclient/manual/). ## Required Properties [Section titled “Required Properties”](#required-properties) These properties must be configured for basic operation: | Property | Type | Description | | ------------- | -------- | ------------------------------------------------------------------------- | | `Authority` | `string` | The OpenID Connect provider URL (e.g., `https://demo.duendesoftware.com`) | | `ClientId` | `string` | The OAuth client identifier registered with the provider | | `RedirectUri` | `string` | The URI where the browser redirects after authentication | | `Scope` | `string` | Space-separated list of scopes to request (must include `openid`) | ```csharp var options = new OidcClientOptions { Authority = "https://demo.duendesoftware.com", ClientId = "native", RedirectUri = "app://callback", Scope = "openid profile email offline_access" }; ``` ## Browser Configuration [Section titled “Browser Configuration”](#browser-configuration) | Property | Type | Default | Description | | ---------------- | ---------- | ------- | ------------------------------------------- | | `Browser` | `IBrowser` | `null` | Browser implementation for user interaction | | `BrowserTimeout` | `TimeSpan` | 5 min | Timeout for browser-based operations | ## Client Authentication [Section titled “Client Authentication”](#client-authentication) | Property | Type | Description | | ---------------------------- | ----------------------------- | ---------------------------------------------- | | `ClientSecret` | `string` | Client secret for confidential clients | | `ClientAssertion` | `ClientAssertion` | Client assertion for JWT client authentication | | `GetClientAssertionAsync` | `Func>` | Callback for dynamic client assertion | | `TokenClientCredentialStyle` | `ClientCredentialStyle` | How credentials are sent (default: POST body) | ```csharp // Confidential client with secret options.ClientSecret = "secret"; // Or with client assertion (e.g., private_key_jwt) options.ClientAssertion = new ClientAssertion { Type = OidcConstants.ClientAssertionTypes.JwtBearer, Value = GenerateClientAssertion() }; ``` ## Token Handling [Section titled “Token Handling”](#token-handling) | Property | Type | Default | Description | | ---------------- | --------------------- | --------- | ----------------------------------------- | | `LoadProfile` | `bool` | `true` | Load claims from userinfo endpoint | | `FilterClaims` | `bool` | `true` | Filter protocol claims from user claims | | `FilteredClaims` | `ICollection` | (various) | Claim types to filter out | | `ClockSkew` | `TimeSpan` | 5 min | Clock skew tolerance for token validation | ## Discovery [Section titled “Discovery”](#discovery) | Property | Type | Default | Description | | ------------------------------------ | --------------------- | ------- | -------------------------------------------------- | | `ProviderInformation` | `ProviderInformation` | `null` | Manual provider configuration (skips discovery) | | `RefreshDiscoveryDocumentForLogin` | `bool` | `false` | Re-fetch discovery on each login | | `RefreshDiscoveryOnSignatureFailure` | `bool` | `true` | Re-fetch discovery when signature validation fails | ### Manual Provider Configuration [Section titled “Manual Provider Configuration”](#manual-provider-configuration) For offline scenarios or providers without discovery: ```csharp options.ProviderInformation = new ProviderInformation { IssuerName = "https://idp.example.com", AuthorizeEndpoint = "https://idp.example.com/authorize", TokenEndpoint = "https://idp.example.com/token", UserInfoEndpoint = "https://idp.example.com/userinfo", EndSessionEndpoint = "https://idp.example.com/endsession", KeySet = loadedKeySet }; ``` ## Logout [Section titled “Logout”](#logout) | Property | Type | Description | | ----------------------- | -------- | ----------------------------- | | `PostLogoutRedirectUri` | `string` | URI for redirect after logout | ## HTTP Configuration [Section titled “HTTP Configuration”](#http-configuration) | Property | Type | Description | | ------------------------------ | ------------------------------------- | ----------------------------------------- | | `BackchannelHandler` | `HttpMessageHandler` | Custom handler for back-channel requests | | `BackchannelTimeout` | `TimeSpan` | Timeout for token/userinfo requests | | `RefreshTokenInnerHttpHandler` | `HttpMessageHandler` | Inner handler for refresh token handler | | `HttpClientFactory` | `Func` | Factory for creating HttpClient instances | ```csharp // Custom handler for debugging or proxying options.BackchannelHandler = new HttpClientHandler { Proxy = new WebProxy("http://localhost:8888") }; options.BackchannelTimeout = TimeSpan.FromSeconds(30); ``` ## Pushed Authorization Requests (PAR) [Section titled “Pushed Authorization Requests (PAR)”](#pushed-authorization-requests-par) | Property | Type | Default | Description | | ---------------------------- | ------ | ------- | ----------------------------- | | `DisablePushedAuthorization` | `bool` | `false` | Disable PAR even if supported | PAR is automatically used when the provider supports it unless disabled. ## Resource Indicators [Section titled “Resource Indicators”](#resource-indicators) | Property | Type | Description | | ---------- | --------------------- | -------------------------------------- | | `Resource` | `ICollection` | Resource indicators for token requests | ```csharp options.Resource = new[] { "urn:api:resource1", "urn:api:resource2" }; ``` ## State Management [Section titled “State Management”](#state-management) | Property | Type | Default | Description | | ------------- | ----- | ------- | ----------------------------------- | | `StateLength` | `int` | 64 | Length of generated state parameter | ## Logging [Section titled “Logging”](#logging) | Property | Type | Description | | --------------- | ---------------- | ------------------------------------- | | `LoggerFactory` | `ILoggerFactory` | Logger factory for diagnostic logging | See [Logging](/identitymodel-oidcclient/logging/) for details. ## Validation Policy [Section titled “Validation Policy”](#validation-policy) | Property | Type | Description | | ------------------------ | ------------------------- | -------------------------- | | `Policy` | `Policy` | Validation policy settings | | `IdentityTokenValidator` | `IIdentityTokenValidator` | Custom token validator | ### Policy Properties [Section titled “Policy Properties”](#policy-properties) | Property | Type | Default | Description | | -------------------------------------------- | --------------------- | ------------ | -------------------------------------- | | `Discovery` | `DiscoveryPolicy` | (default) | Discovery document validation settings | | `RequireAccessTokenHash` | `bool` | `false` | Require `at_hash` claim in ID token | | `RequireIdentityTokenOnRefreshTokenResponse` | `bool` | `false` | Require ID token on refresh | | `RequireIdentityTokenSignature` | `bool` | `true` | Require signed ID tokens | | `ValidateTokenIssuerName` | `bool` | `true` | Validate issuer matches | | `ValidSignatureAlgorithms` | `ICollection` | (asymmetric) | Allowed signing algorithms | ```csharp options.Policy = new Policy { RequireAccessTokenHash = true, ValidSignatureAlgorithms = new[] { "RS256", "ES256" } }; ``` ## Complete Example [Section titled “Complete Example”](#complete-example) ```csharp var options = new OidcClientOptions { // Required Authority = "https://demo.duendesoftware.com", ClientId = "native", RedirectUri = "app://callback", Scope = "openid profile email offline_access api", // Logout PostLogoutRedirectUri = "app://logout", // Browser Browser = new SystemBrowser(), BrowserTimeout = TimeSpan.FromMinutes(2), // Claims LoadProfile = true, FilterClaims = true, // Validation ClockSkew = TimeSpan.FromMinutes(5), // Logging LoggerFactory = loggerFactory, // HTTP BackchannelTimeout = TimeSpan.FromSeconds(30) }; var client = new OidcClient(options); ``` ----- # Duende IdentityModel OIDC Client Samples > A collection of sample applications demonstrating how to use IdentityModel.OidcClient with various platforms and UI frameworks. Samples of IdentityModel.OidcClient are available [on GitHub](https://github.com/DuendeSoftware/foss/tree/main/identity-model-oidc-client/samples). Our samples show how to use an OidcClient with a variety of platforms and UI tools. [.NET MAUI](https://github.com/DuendeSoftware/foss/tree/main/identity-model-oidc-client/samples/Maui)Mobile and desktop app using .NET MAUI. [WPF with System Browser](https://github.com/DuendeSoftware/foss/tree/main/identity-model-oidc-client/samples/Wpf)WPF app using the system browser for login. [WPF with Embedded Browser](https://github.com/DuendeSoftware/foss/tree/main/identity-model-oidc-client/samples/WpfWebView2)WPF app using an embedded WebView2 browser. [WinForms with Embedded Browser](https://github.com/DuendeSoftware/foss/tree/main/identity-model-oidc-client/samples/WinFormsWebView2)WinForms app using an embedded WebView2 browser. [Cross-Platform Console](https://github.com/DuendeSoftware/foss/tree/main/identity-model-oidc-client/samples/NetCoreConsoleClient)Console app using Kestrel for processing the callback. [Windows Console (HttpListener)](https://github.com/DuendeSoftware/foss/tree/main/identity-model-oidc-client/samples/HttpSysConsoleClient)Console app using HttpListener (HTTP.sys wrapper). [Windows Console (Custom URI Schemes)](https://github.com/DuendeSoftware/foss/tree/main/identity-model-oidc-client/samples/WindowsConsoleSystemBrowser)Console app using custom URI schemes. All samples use a [demo instance of Duende IdentityServer](https://demo.duendesoftware.com) as their OIDC Provider. You can see its [source code on GitHub](https://github.com/DuendeSoftware/demo.duendesoftware.com). You can log in with *alice/alice* or *bob/bob* ## Additional Samples [Section titled “Additional Samples”](#additional-samples) [Unity3D](https://github.com/peterhorsley/Unity3D.Authentication.Example)Community sample for Unity3D authentication. ## No Longer Maintained [Section titled “No Longer Maintained”](#no-longer-maintained) These samples are no longer maintained because their underlying technology is no longer supported. * [UWP](https://github.com/IdentityModel/IdentityModel.OidcClient.Samples/tree/archived/uwp/Uwp) * [Xamarin](https://github.com/IdentityModel/IdentityModel.OidcClient.Samples/tree/archived/xamarin/XamarinAndroidClient) * [Xamarin Forms](https://github.com/IdentityModel/IdentityModel.OidcClient.Samples/tree/archived/xamarin/XamarinForms) * [Xamarin iOS - AuthenticationServices](https://github.com/IdentityModel/IdentityModel.OidcClient.Samples/tree/archived/xamarin/iOS_AuthenticationServices) * [Xamarin iOS - SafariServices](https://github.com/IdentityModel/IdentityModel.OidcClient.Samples/tree/archived/xamarin/iOS_SafariServices) ----- # OIDC Client Token Refresh > Learn how to refresh access tokens using OidcClient, including manual refresh and automatic refresh handlers Access tokens have limited lifetimes for security. When using refresh tokens (obtained by requesting the `offline_access` scope), you can obtain new access tokens without requiring user interaction. ## Manual Token Refresh [Section titled “Manual Token Refresh”](#manual-token-refresh) Use `RefreshTokenAsync` to manually refresh tokens: ```csharp var result = await client.RefreshTokenAsync(refreshToken); if (result.IsError) { Console.WriteLine($"Refresh error: {result.Error}"); // Handle refresh failure - may need to re-authenticate return; } // Use the new tokens var newAccessToken = result.AccessToken; var newRefreshToken = result.RefreshToken; // May be rotated ``` ### RefreshTokenResult Properties [Section titled “RefreshTokenResult Properties”](#refreshtokenresult-properties) | Property | Type | Description | | ----------------------- | ---------------- | ------------------------------ | | `AccessToken` | `string` | The new access token | | `IdentityToken` | `string` | New identity token (if issued) | | `RefreshToken` | `string` | New refresh token (if rotated) | | `ExpiresIn` | `int` | Token lifetime in seconds | | `AccessTokenExpiration` | `DateTimeOffset` | When the access token expires | | `IsError` | `bool` | Whether the refresh failed | | `Error` | `string` | Error code if failed | | `ErrorDescription` | `string` | Error description if failed | Refresh Token Rotation Many identity providers rotate refresh tokens. Always store the latest `RefreshToken` from the result, as the previous one may be invalidated. ## Automatic Token Refresh with RefreshTokenDelegatingHandler [Section titled “Automatic Token Refresh with RefreshTokenDelegatingHandler”](#automatic-token-refresh-with-refreshtokendelegatinghandler) For seamless API calls, use the `RefreshTokenDelegatingHandler` which automatically refreshes tokens before they expire: ```csharp // After login, create an HttpClient with automatic refresh var loginResult = await client.LoginAsync(); var handler = new RefreshTokenDelegatingHandler( client, loginResult.AccessToken, loginResult.RefreshToken ); var apiClient = new HttpClient(handler) { BaseAddress = new Uri("https://api.example.com") }; // Tokens are refreshed automatically when needed var response = await apiClient.GetAsync("/protected-resource"); ``` ### Using the Handler from LoginResult [Section titled “Using the Handler from LoginResult”](#using-the-handler-from-loginresult) The `LoginResult` includes a pre-configured handler: ```csharp var loginResult = await client.LoginAsync(); if (!result.IsError && loginResult.RefreshTokenHandler != null) { var apiClient = new HttpClient(loginResult.RefreshTokenHandler); // Use apiClient for API calls with automatic refresh } ``` ### Handling Token Refresh Events [Section titled “Handling Token Refresh Events”](#handling-token-refresh-events) Subscribe to the `TokenRefreshed` event to be notified when tokens are refreshed: ```csharp var handler = new RefreshTokenDelegatingHandler( client, loginResult.AccessToken, loginResult.RefreshToken ); handler.TokenRefreshed += (sender, args) => { // Persist the new tokens SaveTokens(args.AccessToken, args.RefreshToken); Console.WriteLine($"Tokens refreshed, new expiry in {args.ExpiresIn} seconds"); }; ``` #### TokenRefreshedEventArgs Properties [Section titled “TokenRefreshedEventArgs Properties”](#tokenrefreshedeventargs-properties) | Property | Type | Description | | --------------- | -------- | ------------------------------ | | `AccessToken` | `string` | The new access token | | `RefreshToken` | `string` | The new refresh token | | `IdentityToken` | `string` | New identity token (if issued) | | `ExpiresIn` | `int` | Token lifetime in seconds | ### Handler Configuration [Section titled “Handler Configuration”](#handler-configuration) The handler exposes configuration properties: ```csharp var handler = new RefreshTokenDelegatingHandler( client, accessToken, refreshToken, tokenType: "Bearer", // Token type (default: Bearer) innerHandler: new HttpClientHandler() // Custom inner handler ); handler.Timeout = TimeSpan.FromSeconds(30); // Request timeout ``` | Property | Type | Description | | -------------- | ---------- | --------------------------------- | | `AccessToken` | `string` | Current access token (read-only) | | `RefreshToken` | `string` | Current refresh token (read-only) | | `Timeout` | `TimeSpan` | HTTP request timeout | ## Best Practices [Section titled “Best Practices”](#best-practices) 1. **Store tokens securely** - Use platform-specific secure storage (Keychain, Credential Manager, etc.) 2. **Handle refresh failures** - Prompt for re-authentication when refresh fails 3. **Use automatic refresh** - The `RefreshTokenDelegatingHandler` simplifies token management 4. **Persist rotated tokens** - Subscribe to `TokenRefreshed` to save new tokens immediately ----- # Client Assertions > How to use client assertions (private_key_jwt / client_secret_jwt) for client authentication in protocol requests. Client assertions are an alternative to client secrets for authenticating confidential clients at token endpoints. Instead of sending a shared secret, the client creates a signed JWT (or SAML assertion) and includes it in the request. This is defined in [RFC 7523 — JSON Web Token (JWT) Profile for OAuth 2.0 Client Authentication](https://datatracker.ietf.org/doc/html/rfc7523) and is commonly known as the `private_key_jwt` or `client_secret_jwt` authentication methods defined in [OpenID Connect Core §9](https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication). All protocol request types that derive from `ProtocolRequest` expose two properties for setting client assertions: `ClientAssertion` and `ClientAssertionFactory`. ## ClientAssertion [Section titled “ClientAssertion”](#clientassertion) The `ClientAssertion` property lets you attach a pre-built assertion to any protocol request. Set its `Type` and `Value` and they will be included as the `client_assertion_type` and `client_assertion` parameters: ```csharp var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientAssertion = { Type = OidcConstants.ClientAssertionTypes.JwtBearer, Value = mySignedJwt }, ClientCredentialStyle = ClientCredentialStyle.PostBody }); ``` Note When using a client assertion, set `ClientCredentialStyle` to `ClientCredentialStyle.PostBody`. Client assertions are not compatible with `AuthorizationHeader` style and an `InvalidOperationException` will be thrown if both are combined with a `ClientId`. ## ClientAssertionFactory [Section titled “ClientAssertionFactory”](#clientassertionfactory) *Added in `Duende.IdentityModel` 7.2.0* The `ClientAssertionFactory` property accepts a `Func>` — a factory function that creates a **fresh** `ClientAssertion` on demand. This was introduced to support scenarios where a protocol request may need to be **retried**, and each attempt requires a new assertion with unique `jti` and `iat` claims. The primary motivating scenario is **DPoP** (Demonstrating Proof of Possession). When a DPoP token request receives a `use_dpop_nonce` error, the HTTP handler retries the request with an updated DPoP proof. If the client assertion were static, the server could reject the retry because it has already seen that assertion’s `jti`. The factory solves this by generating a new assertion for each attempt. ```csharp var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientAssertionFactory = () => Task.FromResult(new ClientAssertion { Type = OidcConstants.ClientAssertionTypes.JwtBearer, Value = CreateSignedJwt() // generates a fresh JWT each time }), ClientCredentialStyle = ClientCredentialStyle.PostBody }); ``` When `ClientAssertionFactory` is set, the factory is stored on the `HttpRequestMessage.Options` so that DPoP retry handlers (and other delegating handlers in the pipeline) can invoke it to obtain a new assertion on each attempt. Note If both `ClientAssertion` and `ClientAssertionFactory` are set, the factory takes precedence during request preparation. ### Usage with Duende.IdentityModel.OidcClient [Section titled “Usage with Duende.IdentityModel.OidcClient”](#usage-with-duendeidentitymodeloidcclient) Both the `ClientAssertion` and `ClientAssertionFactory` properties exist on `ProtocolRequest` to support [`Duende.IdentityModel.OidcClient`](/identitymodel-oidcclient/). The OidcClient library builds on IdentityModel’s protocol requests internally, and when configured with client assertion-based authentication, it sets these properties on the underlying requests it creates. When `ClientAssertionFactory` is set, it is used during both: * **Pushed Authorization Requests (PAR)** — the factory is invoked to produce a fresh assertion for the PAR endpoint request. * **Token requests** — the factory is invoked again to produce a fresh assertion for the token endpoint request. This ensures each request carries its own unique assertion, which is essential when the authorization server enforces `jti` uniqueness across requests. ----- # Device Authorization Endpoint > Documentation for OAuth 2.0 device flow authorization endpoint using HttpClient extension methods The client library for the [OAuth 2.0 device flow](https://tools.ietf.org/html/rfc8628) device authorization is provided as an extension method for `HttpClient`. The following code sends a device authorization request: ```csharp var client = new HttpClient(); var response = await client.RequestDeviceAuthorizationAsync(new DeviceAuthorizationRequest { Address = "https://demo.duendesoftware.com/connect/device_authorize", ClientId = "device" }); ``` The response is of type `DeviceAuthorizationResponse` and has properties for the standard response parameters. You also have access to the raw response and to a parsed JSON document (via the `Raw` and `Json` properties). Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (response.IsError) throw new Exception(response.Error); var userCode = response.UserCode; var deviceCode = response.DeviceCode; var verificationUrl = response.VerificationUri; var verificationUrlComplete = response.VerificationUriComplete; ``` ----- # Discovery Endpoint > Documentation for using the OpenID Connect discovery endpoint client library, including configuration, validation, and caching features The client library for the [OpenID Connect discovery endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html) is provided as an extension method for `HttpClient`. The `GetDiscoveryDocumentAsync` method returns a `DiscoveryDocumentResponse` object that has both strong and weak typed accessors for the various elements of the discovery document. You should always check the `IsError` and `Error` properties before accessing the contents of the document: ```csharp var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync("https://demo.duendesoftware.com"); if (disco.IsError) throw new Exception(disco.Error); ``` [Standard elements](#discoverydocumentresponse-properties-reference) can be accessed by using properties: ```csharp var tokenEndpoint = disco.TokenEndpoint; var keys = disco.KeySet.Keys; ``` Custom elements (or elements not covered by the standard properties) can be accessed like this: ```csharp // returns string or null var stringValue = disco.TryGetString("some_string_element"); // return a nullable boolean var boolValue = disco.TryGetBoolean("some_boolean_element"); // return array (maybe empty) var arrayValue = disco.TryGetStringArray("some_array_element"); // returns JToken var rawJson = disco.TryGetValue("some_element"); ``` ### Discovery Policy [Section titled “Discovery Policy”](#discovery-policy) By default, the discovery response is validated before it is returned to the client, validation includes: * enforce that HTTPS is used (except for localhost addresses) * enforce that the issuer matches the authority * enforce that the protocol endpoints are on the same DNS name as the `authority` * enforce the existence of a keyset Policy violation errors will set the `ErrorType` property on the `DiscoveryDocumentResponse` to `PolicyViolation`. All the standard validation rules can be modified using the `DiscoveryPolicy` class, e.g. disabling the issuer name check: ```csharp var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest { Address = "https://demo.duendesoftware.com", Policy = { ValidateIssuerName = false } }); ``` #### Cross-Host Endpoints [Section titled “Cross-Host Endpoints”](#cross-host-endpoints) When the URIs in the discovery document are on a different base address than the issuer URI (for example, a [Dynamic Client Registration endpoint](/identityserver/configuration/dcr/#adding-the-registration-endpoint-to-the-discovery-document) hosted on a separate service), the discovery policy will reject those endpoints by default with: ```text Endpoint is on a different host than authority ``` You can resolve this by adding the additional host to `AdditionalEndpointBaseAddresses` (recommended), or by setting `ValidateEndpoints = false` to disable endpoint validation entirely. The same applies to any component that has its own `DiscoveryPolicy`, such as `OAuth2IntrospectionOptions.DiscoveryPolicy`. Each instance needs to be configured independently. ```csharp // Using AdditionalEndpointBaseAddresses (recommended) var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest { Address = "https://authority.example.com", Policy = { AdditionalEndpointBaseAddresses = [ "https://config-api.example.com" ] } }); // Or when using DiscoveryCache var cache = new DiscoveryCache( "https://authority.example.com", () => factory.CreateClient(), new DiscoveryPolicy { AdditionalEndpointBaseAddresses = [ "https://config-api.example.com" ] }); ``` You can also customize validation strategy based on the authority with your own implementation of `IAuthorityValidationStrategy`. By default, comparison uses ordinal string comparison. To switch to `Uri` comparison: ```csharp var disco = await client.GetDiscoveryDocumentAsync(new DiscoveryDocumentRequest { Address = "https://demo.duendesoftware.com", Policy = { AuthorityValidationStrategy = new AuthorityUrlValidationStrategy() } }); ``` ### Caching The Discovery Document [Section titled “Caching The Discovery Document”](#caching-the-discovery-document) You should periodically update your local copy of the discovery document, to be able to react to configuration changes on the server. This is especially important for playing nice with automatic key rotation. The `DiscoveryCache` class can help you with that. The following code will set up the cache, retrieve the document the first time it is needed, and then cache it for 24 hours: ```csharp var cache = new DiscoveryCache("https://demo.duendesoftware.com"); ``` You can then access the document like this: ```csharp var disco = await cache.GetAsync(); if (disco.IsError) throw new Exception(disco.Error); ``` You can specify the cache duration using the `CacheDuration` property and also specify a custom discovery policy by passing in a `DiscoveryPolicy` to the constructor. ### Caching And HttpClient Instances [Section titled “Caching And HttpClient Instances”](#caching-and-httpclient-instances) By default, the discovery cache will create a new instance of `HttpClient` every time it needs to access the discovery endpoint. You can modify this behavior in two ways, either by passing in a pre-created instance into the constructor, or by providing a function that will return an `HttpClient` when needed. The following code will set up the discovery cache in the ASP.NET Core service provider and will use the `HttpClientFactory` to create clients: ```csharp services.AddSingleton(r => { var factory = r.GetRequiredService(); return new DiscoveryCache(Constants.Authority, () => factory.CreateClient()); }); ``` ### DiscoveryDocumentResponse Properties Reference [Section titled “DiscoveryDocumentResponse Properties Reference”](#discoverydocumentresponse-properties-reference) The following table lists the standard properties on the `DiscoveryDocumentResponse` class: | Property | Description | | ------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- | | `Policy` | Gets or sets the discovery policy used to configure how the discovery document is processed | | `KeySet` | Gets or sets the JSON Web Key Set (JWKS) associated with the discovery document | | `MtlsEndpointAliases` | Gets the mutual TLS (mTLS) endpoint aliases | | `Issuer` | Gets the issuer identifier for the authorization server | | `AuthorizeEndpoint` | Gets the authorization endpoint URL | | `TokenEndpoint` | Gets token endpoint URL | | `UserInfoEndpoint` | Gets user info endpoint URL | | `IntrospectionEndpoint` | Gets the introspection endpoint URL | | `RevocationEndpoint` | Gets the revocation endpoint URL | | `DeviceAuthorizationEndpoint` | Gets the device authorization endpoint URL | | `BackchannelAuthenticationEndpoint` | Gets the backchannel authentication endpoint URL | | `JwksUri` | Gets the URI of the JSON Web Key Set (JWKS) | | `EndSessionEndpoint` | Gets the end session endpoint URL | | `CheckSessionIframe` | Gets the check session iframe URL | | `RegistrationEndpoint` | Gets the dynamic client registration (DCR) endpoint URL | | `PushedAuthorizationRequestEndpoint` | Gets the pushed authorization request (PAR) endpoint URL | | `FrontChannelLogoutSupported` | Gets a flag indicating whether front-channel logout is supported | | `FrontChannelLogoutSessionSupported` | Gets a flag indicating whether a session ID (sid) parameter is supported at the front-channel logout endpoint | | `GrantTypesSupported` | Gets the supported grant types | | `CodeChallengeMethodsSupported` | Gets the supported code challenge methods | | `ScopesSupported` | Gets the supported scopes | | `SubjectTypesSupported` | Gets the supported subject types | | `ResponseModesSupported` | Gets the supported response modes | | `ResponseTypesSupported` | Gets the supported response types | | `ClaimsSupported` | Gets the supported claims | | `TokenEndpointAuthenticationMethodsSupported` | Gets the authentication methods supported by the token endpoint | | `TokenEndpointAuthenticationSigningAlgorithmsSupported` | Gets the signing algorithms supported by the token endpoint for client authentication | | `BackchannelTokenDeliveryModesSupported` | Gets the supported backchannel token delivery modes | | `BackchannelUserCodeParameterSupported` | Gets a flag indicating whether the backchannel user code parameter is supported | | `RequirePushedAuthorizationRequests` | Gets a flag indicating whether the use of pushed authorization requests (PAR) is required | | `IntrospectionSigningAlgorithmsSupported` | Gets the signing algorithms supported for introspection responses | | `IntrospectionEncryptionAlgorithmsSupported` | Gets the encryption “alg” values supported for encrypted JWT introspection responses | | `IntrospectionEncryptionEncValuesSupported` | Gets the encryption “enc” values supported for encrypted JWT introspection responses | ----- # Dynamic Client Registration > Documentation for OpenID Connect Dynamic Client Registration library extension method for HttpClient that enables client registration and response handling The client library for [OpenID Connect Dynamic Client Registration](https://openid.net/specs/openid-connect-registration-1_0.html) is provided as an extension method for [`System.Net.Http.HttpClient`](https://learn.microsoft.com/en-us/dotnet/api/system.net.http.httpclient). The following code sends a registration request: ```csharp var client = new HttpClient(); var response = await client.RegisterClientAsync(new DynamicClientRegistrationRequest { Address = Endpoint, Document = new DynamicClientRegistrationDocument { RedirectUris = { redirectUri }, ApplicationType = "native" } }); ``` Note The `DynamicClientRegistrationDocument` class has strongly typed properties for all standard registration parameters as defines by the specification. If you want to add custom parameters, it is recommended to derive from this class and add your own properties. The response is of type `DynamicClientRegistrationResponse` and has properties for the standard response parameters. You also have access to the raw response and to a parsed JSON document (via the `Raw` and `Json` properties). Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (response.IsError) throw new Exception(response.Error); var clientId = response.ClientId; var secret = response.ClientSecret; ``` ----- # General Usage > Overview of IdentityModel client libraries common design patterns and usage for OpenID Connect and OAuth 2.0 endpoint interactions. IdentityModel contains client libraries for many interactions with endpoints defined in OpenID Connect and OAuth 2.0. All of these libraries have a common design, let’s examine the various layers using the client for the token endpoint. ## Request and response objects [Section titled “Request and response objects”](#request-and-response-objects) All protocol request are modeled as request objects and have a common base class called `ProtocolRequest` which has properties to set the endpoint address, client ID, client secret, client assertion, and the details of how client secrets are transmitted (e.g. authorization header vs POST body). `ProtocolRequest` derives from `HttpRequestMessage` and thus also allows setting custom headers etc. The following code snippet creates a request for a client credentials grant type: ```csharp var request = new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret" }; ``` While in theory you could now call `Prepare` (which internally sets the headers, body and address) and send the request via a plain `HttpClient`, typically there are more parameters with special semantics and encoding required. That’s why we provide extension methods to do the low level work. Equally, a protocol response has a corresponding `ProtocolResponse` implementation that parses the status codes and response content. The following code snippet would parse the raw HTTP response from a token endpoint and turn it into a `TokenResponse` object: ```csharp var tokenResponse = await ProtocolResponse .FromHttpResponseAsync(httpResponse); ``` Again these steps are automated using the extension methods. So let’s have a look at an example next. ## Extension methods [Section titled “Extension methods”](#extension-methods) For each protocol interaction, an extension method for `HttpMessageInvoker` (that’s the base class of `HttpClient`) exists. The extension methods expect a request object and return a response object. It is your responsibility to set up and manage the lifetime of the `HttpClient`, e.g. manually: ```csharp var client = new HttpClient(); var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret" }); ``` You might want to use other techniques to obtain an `HttpClient`, e.g. via the HTTP client factory: ```csharp var client = HttpClientFactory.CreateClient("my_named_token_client"); var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret" }); ``` All other endpoint client follow the same design. Note Some client libraries also include a stateful client object (e.g. `TokenClient` and `IntrospectionClient`). See the corresponding section to find out more. ## Client Credential Style [Section titled “Client Credential Style”](#client-credential-style) Note We recommend only changing the Client Credential Style if you’re experiencing HTTP Basic authentication encoding issues. Any request type implementing `ProtocolRequest` has the ability to configure the client credential style, which specifies how the client will transmit the client ID and secret. `ClientCredentialStyle` options include `PostBody` and the default value of `AuthorizationHeader`. ```csharp var client = HttpClientFactory.CreateClient("my_named_token_client"); var response = await client.RequestClientCredentialsTokenAsync( new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret", // set the client credential style ClientCredentialStyle = ClientCredentialStyle.AuthorizationHeader }); ``` For interoperability between OAuth implementations, we allow you to choose either approach, depending on which specification version you are targeting. When using IdentityServer, both header and body approaches are supported and *“it just works”*. [RFC 6749](https://datatracker.ietf.org/doc/rfc6749/), the original OAuth spec, says that support for the basic auth header is mandatory, and that the POST body is optional. OAuth 2.1 reverses this: now the body is mandatory and the header is optional. In the previous OAuth specification version, the header caused bugs and interoperability problems. To follow both RFC 6749 and RFC 2617 (which is where basic auth headers are specified), you have to form url encode the client id and client secret, concatenate them both with a colon in between, and then base64 encode the final value. To try to avoid that complex process, OAuth 2.1 now prefers the POST body mechanism. References: * [RFC 6749](https://datatracker.ietf.org/doc/rfc6749/) section 2.3.1 * [RFC 2617 section 2](https://www.rfc-editor.org/rfc/rfc2617#section-2) * [OAuth 2.1 Draft](https://datatracker.ietf.org/doc/draft-ietf-oauth-v2-1/) Here is a complete list of `ProtocolRequest` implementors that expose the `ClientCredentialStyle` option: * `Duende.IdentityModel.Client.AuthorizationCodeTokenRequest` * `Duende.IdentityModel.Client.BackchannelAuthenticationRequest` * `Duende.IdentityModel.Client.BackchannelAuthenticationTokenRequest` * `Duende.IdentityModel.Client.ClientCredentialsTokenRequest` * `Duende.IdentityModel.Client.DeviceAuthorizationRequest` * `Duende.IdentityModel.Client.DeviceTokenRequest` * `Duende.IdentityModel.Client.DiscoveryDocumentRequest` * `Duende.IdentityModel.Client.DynamicClientRegistrationRequest` * `Duende.IdentityModel.Client.JsonWebKeySetRequest` * `Duende.IdentityModel.Client.PasswordTokenRequest` * `Duende.IdentityModel.Client.PushedAuthorizationRequest` * `Duende.IdentityModel.Client.RefreshTokenRequest` * `Duende.IdentityModel.Client.TokenExchangeTokenRequest` * `Duende.IdentityModel.Client.TokenIntrospectionRequest` * `Duende.IdentityModel.Client.TokenRequest` * `Duende.IdentityModel.Client.TokenRevocationRequest` * `Duende.IdentityModel.Client.UserInfoRequest` ----- # Token Introspection Endpoint > Learn how to use the OAuth 2.0 token introspection endpoint to validate and inspect access tokens using HttpClient extensions. The client library for [OAuth 2.0 token introspection (RFC 7662)](https://tools.ietf.org/html/rfc7662) is provided by the `IntrospectionClient` class, and as an extension method for `HttpClient`. ## Token Introspection Request [Section titled “Token Introspection Request”](#token-introspection-request) The following code sends a reference token to an introspection endpoint: * Using IntrospectionClient ```csharp var clientOptions = new IntrospectionClientOptions { Address = Endpoint, ClientId = "client", ClientSecret = "secret", ResponseFormat = ResponseFormat.Json }; var httpClient = new HttpClient(); var introspectionClient = new IntrospectionClient(httpClient, clientOptions); var introspectionResponse = await introspectionClient.Introspect("token"); ``` * Using HttpClient extension ```csharp var client = new HttpClient(); var introspectionResponse = await client.IntrospectTokenAsync(new TokenIntrospectionRequest { Address = Endpoint, Token = "token", ResponseFormat = ResponseFormat.Json }); ``` ## Token Introspection Response [Section titled “Token Introspection Response”](#token-introspection-response) The response of a token introspection request is an object of type `TokenIntrospectionResponse`. Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (introspectionResponse.IsError) throw new Exception(introspectionResponse.Error); var isActive = introspectionResponse.IsActive; var claims = introspectionResponse.Claims; ``` The `TokenIntrospectionResponse` class exposes the raw response through its `Raw` property, and to the parsed JSON document through its `Json` property. In addition, it provides access to the following standard response parameters: | Property | Value | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Scopes` | The list of scopes associated to the token or an empty array if no `scope` claim is present. | | `ClientId` | The client identifier for the OAuth 2.0 client that requested the token or `null` if the `client_id` claim is missing. | | `UserName` | The human-readable identifier for the resource owner who authorized the token or `null` if the `username` claim is missing. | | `TokenType` | The type of the token as defined in [section 5.1 of OAuth 2.0 (RFC6749)](https://datatracker.ietf.org/doc/html/rfc6749#section-5.1) or `null` if the `token_type` claim is missing. | | `Expiration` | The expiration time of the token or `null` if the `exp` claim is missing. | | `IssuedAt` | The issuance time of the token or `null` if the `iat` claim is missing. | | `NotBefore` | The validity start time of the token or `null` if the `nbf` claim is missing. | | `Subject` | The subject of the token or `null` if the `sub` claim is missing. | | `Audiences` | The service-specific list of string identifiers representing the intended audience for the token or an empty array if no `aud` claim is present. | | `Issuer` | The string representing the issuer of the token or `null` if the `iss` claim is missing. | | `JwtId` | The string identifier for the token or `null` if the `jti` claim is missing. | ## JWT Response Format v7.1 [Section titled “JWT Response Format ”v7.1](#jwt-response-format) Introspection requests can optionally pass a parameter to indicate that a signed JWT rather than JSON payload is desired. Such a JWT response is most often useful for non-repudiation. For example, an API might rely on the claims from introspection to produce digitally signed documents or issue certificates, with the Authorization Server assuming legal liability for the introspected data. A JWT introspection response can be stored and its signature independently verified as part of an audit. ### Requesting JWT Response Format [Section titled “Requesting JWT Response Format”](#requesting-jwt-response-format) To request the JWT response format, set the `ResponseFormat` option to `ResponseFormat.Jwt`. ```csharp var client = new HttpClient(); var introspectionResponse = await client.IntrospectTokenAsync( new TokenIntrospectionRequest { Address = Endpoint, Token = "token", ResponseFormat = ResponseFormat.Jwt }); ``` ### Validating JWT Signature [Section titled “Validating JWT Signature”](#validating-jwt-signature) By default, when the introspection endpoint returns a JWT, the system performs only a basic format check on the response. Full cryptographic validation of the JWT’s signature and claims is not performed. This approach is generally appropriate because the introspection request is made over a direct back-channel connection from the application to the introspection endpoint. This connection is secured by TLS, which guarantees the authenticity and integrity of the response in transit. The introspected claims can safely be used immediately without an additional cryptographic validation. An extensibility point is available to provide your own implementation of `ITokenIntrospectionJwtResponseValidator`. ITokenIntrospectionJwtResponseValidator.cs ```csharp public interface ITokenIntrospectionJwtResponseValidator { void Validate(string rawJwtResponse); } ``` A custom validator can be applied using the `TokenIntrospectionRequest.JwtResponseValidator` property or using `IntrospectionClientOptions`: ```csharp var client = new HttpClient(); var introspectionResponse = await client.IntrospectTokenAsync( new TokenIntrospectionRequest { Address = Endpoint, Token = "token", ResponseFormat = ResponseFormat.Jwt, JwtResponseValidator = new CustomIntrospectionJwtResponseValidator() }); ``` ----- # Token Revocation Endpoint > Client library implementation for OAuth 2.0 token revocation endpoint using HttpClient extension methods The client library for [OAuth 2.0 token revocation](https://tools.ietf.org/html/rfc7009) is provided as an extension method for `HttpClient`. The following code revokes an access token at a revocation endpoint: ```csharp var client = new HttpClient(); var result = await client.RevokeTokenAsync(new TokenRevocationRequest { Address = "https://demo.duendesoftware.com/connect/revocation", ClientId = "client", ClientSecret = "secret", Token = accessToken }); ``` The response is of type `TokenRevocationResponse` gives you access to the raw response and to a parsed JSON document (via the `Raw` and `Json` properties). Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (response.IsError) throw new Exception(response.Error); ``` ----- # Token Endpoint > Documentation for the OAuth 2.0 and OpenID Connect token endpoint client library, providing extension methods for HttpClient to handle various token request flows The client library for the token endpoint ([OAuth 2.0](https://tools.ietf.org/html/rfc6749#section-3.2) and [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html#tokenendpoint)) is provided as a set of extension methods for `HttpClient`. This allows creating and managing the lifetime of the `HttpClient` the way you prefer: statically or via a factory like the Microsoft `HttpClientFactory`. ## Requesting a token [Section titled “Requesting a token”](#requesting-a-token) The main extension method is called `RequestTokenAsync`. It has direct support for standard parameters like client ID/secret (or assertion) and grant type, but it also allows setting arbitrary other parameters via a dictionary. All other extensions methods ultimately call this method internally: ```csharp var client = new HttpClient(); var response = await client.RequestTokenAsync(new TokenRequest { Address = "https://demo.duendesoftware.com/connect/token", GrantType = "custom", ClientId = "client", ClientSecret = "secret", Parameters = { { "custom_parameter", "custom value"}, { "scope", "api1" } } }); ``` The response is of type `TokenResponse` and has properties for the standard token response parameters like `access_token`, `expires_in` etc. You also have access to the raw response and to a parsed JSON document (via the `Raw` and `Json` properties). Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (response.IsError) throw new Exception(response.Error); var token = response.AccessToken; var custom = response.Json.TryGetString("custom_parameter"); ``` ## Requesting a token using the `client_credentials` Grant Type [Section titled “Requesting a token using the client\_credentials Grant Type”](#requesting-a-token-using-the-client_credentials-grant-type) The `RequestClientCredentialsToken` extension method has convenience properties for the `client_credentials` grant type: ```csharp var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret", Scope = "api1" }); ``` ## Requesting a token using the `password` Grant Type [Section titled “Requesting a token using the password Grant Type”](#requesting-a-token-using-the-password-grant-type) The `RequestPasswordToken` extension method has convenience properties for the `password` grant type: ```csharp var response = await client.RequestPasswordTokenAsync(new PasswordTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "client", ClientSecret = "secret", Scope = "api1", UserName = "bob", Password = "bob" }); ``` ## Requesting a token using the `authorization_code` Grant Type [Section titled “Requesting a token using the authorization\_code Grant Type”](#requesting-a-token-using-the-authorization_code-grant-type) The `RequestAuthorizationCodeToken` extension method has convenience properties for the `authorization_code` grant type and PKCE: ```csharp var response = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest { Address = IdentityServerPipeline.TokenEndpoint, ClientId = "client", ClientSecret = "secret", Code = code, RedirectUri = "https://app.com/callback", // optional PKCE parameter CodeVerifier = "xyz" }); ``` ## Requesting a token using the `refresh_token` Grant Type [Section titled “Requesting a token using the refresh\_token Grant Type”](#requesting-a-token-using-the-refresh_token-grant-type) The `RequestRefreshToken` extension method has convenience properties for the `refresh_token` grant type: ```csharp var response = await _client.RequestRefreshTokenAsync(new RefreshTokenRequest { Address = TokenEndpoint, ClientId = "client", ClientSecret = "secret", RefreshToken = "xyz" }); ``` ## Requesting a Device Token [Section titled “Requesting a Device Token”](#requesting-a-device-token) The `RequestDeviceToken` extension method has convenience properties for the `urn:ietf:params:oauth:grant-type:device_code` grant type: ```csharp var response = await client.RequestDeviceTokenAsync(new DeviceTokenRequest { Address = disco.TokenEndpoint, ClientId = "device", DeviceCode = authorizeResponse.DeviceCode }); ``` ----- # UserInfo Endpoint The client library for the [OpenID Connect UserInfo](https://openid.net/specs/openid-connect-core-1_0.html#userinfo) endpoint is provided as an extension method for `HttpClient`. The following code sends an access token to the UserInfo endpoint: ```csharp var client = new HttpClient(); var response = await client.GetUserInfoAsync(new UserInfoRequest { Address = disco.UserInfoEndpoint, Token = token }); ``` The response is of type `UserInfoResponse` and has properties for the standard response parameters. You also have access to the raw response and to a parsed JSON document (via the `Raw` and `Json` properties). Before using the response, you should always check the `IsError` property to make sure the request was successful: ```csharp if (response.IsError) throw new Exception(response.Error); var claims = response.Claims; ``` ----- # Base64 URL Encoding > Documentation for Base64 URL encoding and decoding utilities in Duende IdentityModel, used for JWT token serialization JWT serialization involves transforming the three core components of a JWT (Header, Payload, Signature) into a single, compact, URL-safe string. [Base64 URL encoding](https://tools.ietf.org/html/rfc4648#section-5) is used instead of standard Base64 because it doesn’t include characters like `+`, `/`, or `=`, making it safe to use directly in URLs and HTTP headers without requiring further encoding. In newer .NET versions, you can use the `Base64Url` class found in the `System.Buffers.Text` namespace to decode Base64 payloads using the `DecodeFromChars` method: ```csharp using System.Buffers.Text; var jsonString = Base64Url.DecodeFromChars(payload); ``` Encoding can be done using the `EncodeToString` method: ```csharp using System.Buffers.Text; var bytes = Encoding.UTF8.GetBytes("some string"); var encodedString = Base64Url.EncodeToString(bytes); ``` Alternatively, ASP.NET Core has built-in support for Base64 encoding and decoding via [WebEncoders.Base64UrlEncode](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.webutilities.webencoders.base64urlencode) and [WebEncoders.Base64UrlDecode](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.webutilities.webencoders.base64urldecode). To use these methods, ensure you have the following package installed: ```bash dotnet add package Microsoft.AspNetCore.WebUtilities ``` Then use the following code: ```csharp using System.Text; using Microsoft.AspNetCore.WebUtilities; var bytes = "hello"u8.ToArray(); var b64url = WebEncoders.Base64UrlEncode(bytes); bytes = WebEncoders.Base64UrlDecode(b64url); var text = Encoding.UTF8.GetString(bytes); Console.WriteLine(text); ``` ----- # Protocol and Claim Type Constants > Explore constant string classes provided by IdentityModel for OAuth 2.0, OpenID Connect protocol values, and JWT claim types When working with OAuth 2.0, OpenID Connect and claims, there are a lot of **✨magic strings** for claim types and protocol values. IdentityModel provides a couple of constant strings classes to help with that. ## OAuth 2.0 And OpenID Connect Protocol Values [Section titled “OAuth 2.0 And OpenID Connect Protocol Values”](#oauth-20-and-openid-connect-protocol-values) The `OidcConstants` class provides a set of constants for OAuth 2.0 and OpenID Connect protocol values. #### AuthorizeRequest [Section titled “AuthorizeRequest”](#authorizerequest) | Name | Value | | ------------------- | ----------------------- | | Scope | `scope` | | ResponseType | `response_type` | | ClientId | `client_id` | | RedirectUri | `redirect_uri` | | State | `state` | | ResponseMode | `response_mode` | | Nonce | `nonce` | | Display | `display` | | Prompt | `prompt` | | MaxAge | `max_age` | | UiLocales | `ui_locales` | | IdTokenHint | `id_token_hint` | | LoginHint | `login_hint` | | AcrValues | `acr_values` | | CodeChallenge | `code_challenge` | | CodeChallengeMethod | `code_challenge_method` | | Request | `request` | | RequestUri | `request_uri` | | Resource | `resource` | | DPoPKeyThumbprint | `dpop_jkt` | ### AuthorizeErrors [Section titled “AuthorizeErrors”](#authorizeerrors) | Name | Value | | ------------------------------- | ----------------------------------- | | InvalidRequest | `invalid_request` | | UnauthorizedClient | `unauthorized_client` | | AccessDenied | `access_denied` | | UnsupportedResponseType | `unsupported_response_type` | | InvalidScope | `invalid_scope` | | ServerError | `server_error` | | TemporarilyUnavailable | `temporarily_unavailable` | | UnmetAuthenticationRequirements | `unmet_authentication_requirements` | | InteractionRequired | `interaction_required` | | LoginRequired | `login_required` | | AccountSelectionRequired | `account_selection_required` | | ConsentRequired | `consent_required` | | InvalidRequestUri | `invalid_request_uri` | | InvalidRequestObject | `invalid_request_object` | | RequestNotSupported | `request_not_supported` | | RequestUriNotSupported | `request_uri_not_supported` | | RegistrationNotSupported | `registration_not_supported` | | InvalidTarget | `invalid_target` | ### AuthorizeResponse [Section titled “AuthorizeResponse”](#authorizeresponse) | Name | Value | | ---------------- | ------------------- | | Scope | `scope` | | Code | `code` | | AccessToken | `access_token` | | ExpiresIn | `expires_in` | | TokenType | `token_type` | | RefreshToken | `refresh_token` | | IdentityToken | `id_token` | | State | `state` | | SessionState | `session_state` | | Issuer | `iss` | | Error | `error` | | ErrorDescription | `error_description` | ### DeviceAuthorizationResponse [Section titled “DeviceAuthorizationResponse”](#deviceauthorizationresponse) | Name | Value | | ----------------------- | --------------------------- | | DeviceCode | `device_code` | | UserCode | `user_code` | | VerificationUri | `verification_uri` | | VerificationUriComplete | `verification_uri_complete` | | ExpiresIn | `expires_in` | | Interval | `interval` | ### EndSessionRequest [Section titled “EndSessionRequest”](#endsessionrequest) | Name | Value | | --------------------- | -------------------------- | | IdTokenHint | `id_token_hint` | | PostLogoutRedirectUri | `post_logout_redirect_uri` | | State | `state` | | Sid | `sid` | | Issuer | `iss` | | UiLocales | `ui_locales` | ### TokenRequest [Section titled “TokenRequest”](#tokenrequest) | Name | Value | | ----------------------- | ----------------------- | | GrantType | `grant_type` | | RedirectUri | `redirect_uri` | | ClientId | `client_id` | | ClientSecret | `client_secret` | | ClientAssertion | `client_assertion` | | ClientAssertionType | `client_assertion_type` | | Assertion | `assertion` | | Code | `code` | | RefreshToken | `refresh_token` | | Scope | `scope` | | UserName | `username` | | Password | `password` | | CodeVerifier | `code_verifier` | | TokenType | `token_type` | | Algorithm | `alg` | | Key | `key` | | DeviceCode | `device_code` | | Resource | `resource` | | Audience | `audience` | | RequestedTokenType | `requested_token_type` | | SubjectToken | `subject_token` | | SubjectTokenType | `subject_token_type` | | ActorToken | `actor_token` | | ActorTokenType | `actor_token_type` | | AuthenticationRequestId | `auth_req_id` | ### BackchannelAuthenticationRequest [Section titled “BackchannelAuthenticationRequest”](#backchannelauthenticationrequest) | Name | Value | | ----------------------- | --------------------------- | | Scope | `scope` | | ClientNotificationToken | `client_notification_token` | | AcrValues | `acr_values` | | LoginHintToken | `login_hint_token` | | IdTokenHint | `id_token_hint` | | LoginHint | `login_hint` | | BindingMessage | `binding_message` | | UserCode | `user_code` | | RequestedExpiry | `requested_expiry` | | Request | `request` | | Resource | `resource` | | DPoPKeyThumbprint | `dpop_jkt` | ### BackchannelAuthenticationRequestErrors [Section titled “BackchannelAuthenticationRequestErrors”](#backchannelauthenticationrequesterrors) | Name | Value | | --------------------- | -------------------------- | | InvalidRequestObject | `invalid_request_object` | | InvalidRequest | `invalid_request` | | InvalidScope | `invalid_scope` | | ExpiredLoginHintToken | `expired_login_hint_token` | | UnknownUserId | `unknown_user_id` | | UnauthorizedClient | `unauthorized_client` | | MissingUserCode | `missing_user_code` | | InvalidUserCode | `invalid_user_code` | | InvalidBindingMessage | `invalid_binding_message` | | InvalidClient | `invalid_client` | | AccessDenied | `access_denied` | | InvalidTarget | `invalid_target` | ### TokenRequestTypes [Section titled “TokenRequestTypes”](#tokenrequesttypes) | Name | Value | | ------ | -------- | | Bearer | `bearer` | | Pop | `pop` | ### TokenErrors [Section titled “TokenErrors”](#tokenerrors) | Name | Value | | ----------------------- | --------------------------- | | InvalidRequest | `invalid_request` | | InvalidClient | `invalid_client` | | InvalidGrant | `invalid_grant` | | UnauthorizedClient | `unauthorized_client` | | UnsupportedGrantType | `unsupported_grant_type` | | UnsupportedResponseType | `unsupported_response_type` | | InvalidScope | `invalid_scope` | | AuthorizationPending | `authorization_pending` | | AccessDenied | `access_denied` | | SlowDown | `slow_down` | | ExpiredToken | `expired_token` | | InvalidTarget | `invalid_target` | | InvalidDPoPProof | `invalid_dpop_proof` | | UseDPoPNonce | `use_dpop_nonce` | ### TokenResponse [Section titled “TokenResponse”](#tokenresponse) | Name | Value | | ---------------- | ------------------- | | AccessToken | `access_token` | | ExpiresIn | `expires_in` | | TokenType | `token_type` | | RefreshToken | `refresh_token` | | IdentityToken | `id_token` | | Error | `error` | | ErrorDescription | `error_description` | | BearerTokenType | `Bearer` | | DPoPTokenType | `DPoP` | | IssuedTokenType | `issued_token_type` | | Scope | `scope` | ### BackchannelAuthenticationResponse [Section titled “BackchannelAuthenticationResponse”](#backchannelauthenticationresponse) | Name | Value | | ----------------------- | ------------- | | AuthenticationRequestId | `auth_req_id` | | ExpiresIn | `expires_in` | | Interval | `interval` | ### PushedAuthorizationRequestResponse [Section titled “PushedAuthorizationRequestResponse”](#pushedauthorizationrequestresponse) | Name | Value | | ---------- | ------------- | | ExpiresIn | `expires_in` | | RequestUri | `request_uri` | ### TokenIntrospectionRequest [Section titled “TokenIntrospectionRequest”](#tokenintrospectionrequest) | Name | Value | | ------------- | ----------------- | | Token | `token` | | TokenTypeHint | `token_type_hint` | ### RegistrationResponse [Section titled “RegistrationResponse”](#registrationresponse) | Name | Value | | ----------------------- | --------------------------- | | Error | `error` | | ErrorDescription | `error_description` | | ClientId | `client_id` | | ClientSecret | `client_secret` | | RegistrationAccessToken | `registration_access_token` | | RegistrationClientUri | `registration_client_uri` | | ClientIdIssuedAt | `client_id_issued_at` | | ClientSecretExpiresAt | `client_secret_expires_at` | | SoftwareStatement | `software_statement` | ### ClientMetadata [Section titled “ClientMetadata”](#clientmetadata) | Name | Value | | ------------------------------------------- | -------------------------------------- | | RedirectUris | `redirect_uris` | | ResponseTypes | `response_types` | | GrantTypes | `grant_types` | | ApplicationType | `application_type` | | Contacts | `contacts` | | ClientName | `client_name` | | LogoUri | `logo_uri` | | ClientUri | `client_uri` | | PolicyUri | `policy_uri` | | TosUri | `tos_uri` | | JwksUri | `jwks_uri` | | Jwks | `jwks` | | SectorIdentifierUri | `sector_identifier_uri` | | Scope | `scope` | | PostLogoutRedirectUris | `post_logout_redirect_uris` | | FrontChannelLogoutUri | `frontchannel_logout_uri` | | FrontChannelLogoutSessionRequired | `frontchannel_logout_session_required` | | BackchannelLogoutUri | `backchannel_logout_uri` | | BackchannelLogoutSessionRequired | `backchannel_logout_session_required` | | SoftwareId | `software_id` | | SoftwareStatement | `software_statement` | | SoftwareVersion | `software_version` | | SubjectType | `subject_type` | | TokenEndpointAuthenticationMethod | `token_endpoint_auth_method` | | TokenEndpointAuthenticationSigningAlgorithm | `token_endpoint_auth_signing_alg` | | DefaultMaxAge | `default_max_age` | | RequireAuthenticationTime | `require_auth_time` | | DefaultAcrValues | `default_acr_values` | | InitiateLoginUri | `initiate_login_uri` | | RequestUris | `request_uris` | | IdentityTokenSignedResponseAlgorithm | `id_token_signed_response_alg` | | IdentityTokenEncryptedResponseAlgorithm | `id_token_encrypted_response_alg` | | IdentityTokenEncryptedResponseEncryption | `id_token_encrypted_response_enc` | | UserinfoSignedResponseAlgorithm | `userinfo_signed_response_alg` | | UserInfoEncryptedResponseAlgorithm | `userinfo_encrypted_response_alg` | | UserinfoEncryptedResponseEncryption | `userinfo_encrypted_response_enc` | | RequestObjectSigningAlgorithm | `request_object_signing_alg` | | RequestObjectEncryptionAlgorithm | `request_object_encryption_alg` | | RequestObjectEncryptionEncryption | `request_object_encryption_enc` | | RequireSignedRequestObject | `require_signed_request_object` | | AlwaysUseDPoPBoundAccessTokens | `dpop_bound_access_tokens` | | IntrospectionSignedResponseAlgorithm | `introspection_signed_response_alg` | | IntrospectionEncryptedResponseAlgorithm | `introspection_encrypted_response_alg` | | IntrospectionEncryptedResponseEncryption | `introspection_encrypted_response_enc` | ### TokenTypes [Section titled “TokenTypes”](#tokentypes) | Name | Value | | ------------- | --------------- | | AccessToken | `access_token` | | IdentityToken | `id_token` | | RefreshToken | `refresh_token` | ### TokenTypeIdentifiers [Section titled “TokenTypeIdentifiers”](#tokentypeidentifiers) | Name | Value | | ------------- | ------------------------------------------------ | | AccessToken | `urn:ietf:params:oauth:token-type:access_token` | | IdentityToken | `urn:ietf:params:oauth:token-type:id_token` | | RefreshToken | `urn:ietf:params:oauth:token-type:refresh_token` | | Saml11 | `urn:ietf:params:oauth:token-type:saml1` | | Saml2 | `urn:ietf:params:oauth:token-type:saml2` | | Jwt | `urn:ietf:params:oauth:token-type:jwt` | ### AuthenticationSchemes [Section titled “AuthenticationSchemes”](#authenticationschemes) | Name | Value | | ------------------------- | ------------------ | | AuthorizationHeaderBearer | `Bearer` | | AuthorizationHeaderDPoP | `DPoP` | | FormPostBearer | `access_token` | | QueryStringBearer | `access_token` | | AuthorizationHeaderPop | `PoP` | | FormPostPop | `pop_access_token` | | QueryStringPop | `pop_access_token` | ### GrantTypes [Section titled “GrantTypes”](#granttypes) | Name | Value | | ----------------- | ------------------------------------------------- | | Password | `password` | | AuthorizationCode | `authorization_code` | | ClientCredentials | `client_credentials` | | RefreshToken | `refresh_token` | | Implicit | `implicit` | | Saml2Bearer | `urn:ietf:params:oauth:grant-type:saml2-bearer` | | JwtBearer | `urn:ietf:params:oauth:grant-type:jwt-bearer` | | DeviceCode | `urn:ietf:params:oauth:grant-type:device_code` | | TokenExchange | `urn:ietf:params:oauth:grant-type:token-exchange` | | Ciba | `urn:openid:params:grant-type:ciba` | ### ClientAssertionTypes [Section titled “ClientAssertionTypes”](#clientassertiontypes) | Name | Value | | ---------- | ---------------------------------------------------------- | | JwtBearer | `urn:ietf:params:oauth:client-assertion-type:jwt-bearer` | | SamlBearer | `urn:ietf:params:oauth:client-assertion-type:saml2-bearer` | ### ResponseTypes [Section titled “ResponseTypes”](#responsetypes) | Name | Value | | ---------------- | --------------------- | | Code | `code` | | Token | `token` | | IdToken | `id_token` | | IdTokenToken | `id_token token` | | CodeIdToken | `code id_token` | | CodeToken | `code token` | | CodeIdTokenToken | `code id_token token` | ### ResponseModes [Section titled “ResponseModes”](#responsemodes) | Name | Value | | -------- | ----------- | | FormPost | `form_post` | | Query | `query` | | Fragment | `fragment` | ### DisplayModes [Section titled “DisplayModes”](#displaymodes) | Name | Value | | ----- | ------- | | Page | `page` | | Popup | `popup` | | Touch | `touch` | | Wap | `wap` | ### PromptModes [Section titled “PromptModes”](#promptmodes) | Name | Value | | ------------- | ---------------- | | None | `none` | | Login | `login` | | Consent | `consent` | | SelectAccount | `select_account` | | Create | `create` | ### CodeChallengeMethods [Section titled “CodeChallengeMethods”](#codechallengemethods) | Name | Value | | ------ | ------- | | Plain | `plain` | | Sha256 | `S256` | ### ProtectedResourceErrors [Section titled “ProtectedResourceErrors”](#protectedresourceerrors) | Name | Value | | ----------------- | -------------------- | | InvalidToken | `invalid_token` | | ExpiredToken | `expired_token` | | InvalidRequest | `invalid_request` | | InsufficientScope | `insufficient_scope` | ### EndpointAuthenticationMethods [Section titled “EndpointAuthenticationMethods”](#endpointauthenticationmethods) | Name | Value | | ----------------------- | ----------------------------- | | PostBody | `client_secret_post` | | BasicAuthentication | `client_secret_basic` | | PrivateKeyJwt | `private_key_jwt` | | TlsClientAuth | `tls_client_auth` | | SelfSignedTlsClientAuth | `self_signed_tls_client_auth` | ### AuthenticationMethods [Section titled “AuthenticationMethods”](#authenticationmethods) | Name | Value | | ----------------------------------- | -------- | | FacialRecognition | `face` | | FingerprintBiometric | `fpt` | | Geolocation | `geo` | | ProofOfPossessionHardwareSecuredKey | `hwk` | | IrisScanBiometric | `iris` | | KnowledgeBasedAuthentication | `kba` | | MultipleChannelAuthentication | `mca` | | MultiFactorAuthentication | `mfa` | | OneTimePassword | `otp` | | PersonalIdentificationOrPattern | `pin` | | ProofOfPossessionKey | `pop` | | Password | `pwd` | | RiskBasedAuthentication | `rba` | | RetinaScanBiometric | `retina` | | SmartCard | `sc` | | ConfirmationBySms | `sms` | | ProofOfPossessionSoftwareSecuredKey | `swk` | | ConfirmationByTelephone | `tel` | | UserPresenceTest | `user` | | VoiceBiometric | `vbm` | | WindowsIntegratedAuthentication | `wia` | ### Algorithms [Section titled “Algorithms”](#algorithms) #### Symmetric [Section titled “Symmetric”](#symmetric) | Name | Value | | ----- | ------- | | HS256 | `HS256` | | HS384 | `HS384` | | HS512 | `HS512` | #### Asymmetric [Section titled “Asymmetric”](#asymmetric) | Name | Value | | ----- | ------- | | RS256 | `RS256` | | RS384 | `RS384` | | RS512 | `RS512` | | ES256 | `ES256` | | ES384 | `ES384` | | ES512 | `ES512` | | PS256 | `PS256` | | PS384 | `PS384` | | PS512 | `PS512` | ### Discovery [Section titled “Discovery”](#discovery) | Name | Value | | ------------------------------------------- | -------------------------------------------------- | | Issuer | `issuer` | | AuthorizationEndpoint | `authorization_endpoint` | | DeviceAuthorizationEndpoint | `device_authorization_endpoint` | | TokenEndpoint | `token_endpoint` | | UserInfoEndpoint | `userinfo_endpoint` | | IntrospectionEndpoint | `introspection_endpoint` | | RevocationEndpoint | `revocation_endpoint` | | DiscoveryEndpoint | `.well-known/openid-configuration` | | JwksUri | `jwks_uri` | | EndSessionEndpoint | `end_session_endpoint` | | CheckSessionIframe | `check_session_iframe` | | RegistrationEndpoint | `registration_endpoint` | | MtlsEndpointAliases | `mtls_endpoint_aliases` | | PushedAuthorizationRequestEndpoint | `pushed_authorization_request_endpoint` | | FrontChannelLogoutSupported | `frontchannel_logout_supported` | | FrontChannelLogoutSessionSupported | `frontchannel_logout_session_supported` | | BackChannelLogoutSupported | `backchannel_logout_supported` | | BackChannelLogoutSessionSupported | `backchannel_logout_session_supported` | | GrantTypesSupported | `grant_types_supported` | | CodeChallengeMethodsSupported | `code_challenge_methods_supported` | | ScopesSupported | `scopes_supported` | | SubjectTypesSupported | `subject_types_supported` | | ResponseModesSupported | `response_modes_supported` | | ResponseTypesSupported | `response_types_supported` | | ClaimsSupported | `claims_supported` | | TokenEndpointAuthenticationMethodsSupported | `token_endpoint_auth_methods_supported` | | ClaimsLocalesSupported | `claims_locales_supported` | | ClaimsParameterSupported | `claims_parameter_supported` | | ClaimTypesSupported | `claim_types_supported` | | DisplayValuesSupported | `display_values_supported` | | AcrValuesSupported | `acr_values_supported` | | IdTokenEncryptionAlgorithmsSupported | `id_token_encryption_alg_values_supported` | | IdTokenEncryptionEncValuesSupported | `id_token_encryption_enc_values_supported` | | IdTokenSigningAlgorithmsSupported | `id_token_signing_alg_values_supported` | | OpPolicyUri | `op_policy_uri` | | OpTosUri | `op_tos_uri` | | RequestObjectEncryptionAlgorithmsSupported | `request_object_encryption_alg_values_supported` | | RequestObjectEncryptionEncValuesSupported | `request_object_encryption_enc_values_supported` | | RequestObjectSigningAlgorithmsSupported | `request_object_signing_alg_values_supported` | | RequestParameterSupported | `request_parameter_supported` | | RequestUriParameterSupported | `request_uri_parameter_supported` | | RequireRequestUriRegistration | `require_request_uri_registration` | | ServiceDocumentation | `service_documentation` | | TokenEndpointAuthSigningAlgorithmsSupported | `token_endpoint_auth_signing_alg_values_supported` | | UILocalesSupported | `ui_locales_supported` | | UserInfoEncryptionAlgorithmsSupported | `userinfo_encryption_alg_values_supported` | | UserInfoEncryptionEncValuesSupported | `userinfo_encryption_enc_values_supported` | | UserInfoSigningAlgorithmsSupported | `userinfo_signing_alg_values_supported` | | TlsClientCertificateBoundAccessTokens | `tls_client_certificate_bound_access_tokens` | | AuthorizationResponseIssParameterSupported | `authorization_response_iss_parameter_supported` | | PromptValuesSupported | `prompt_values_supported` | | IntrospectionSigningAlgorithmsSupported | `introspection_signing_alg_values_supported` | | IntrospectionEncryptionAlgorithmsSupported | `introspection_encryption_alg_values_supported` | | IntrospectionEncryptionEncValuesSupported | `introspection_encryption_enc_values_supported` | ### BackchannelTokenDeliveryModes [Section titled “BackchannelTokenDeliveryModes”](#backchanneltokendeliverymodes) | Name | Value | | ---- | ------ | | Poll | `poll` | | Ping | `ping` | | Push | `push` | ### Events [Section titled “Events”](#events) | Name | Value | | ----------------- | ---------------------------------------------------- | | BackChannelLogout | `http://schemas.openid.net/event/backchannel-logout` | ### BackChannelLogoutRequest [Section titled “BackChannelLogoutRequest”](#backchannellogoutrequest) | Name | Value | | ----------- | -------------- | | LogoutToken | `logout_token` | ### StandardScopes [Section titled “StandardScopes”](#standardscopes) | Name | Value | Description | | ------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | OpenId | `openid` | REQUIRED. Indicates the Client is making an OpenID Connect request. The behavior is unspecified if this is not included. | | Profile | `profile` | OPTIONAL. Requests access to End-User’s default profile Claims such as `name`, `family_name`, `given_name`, etc. | | Email | `email` | OPTIONAL. Requests access to the `email` and `email_verified` Claims. | | Address | `address` | OPTIONAL. Requests access to the `address` Claim. | | Phone | `phone` | OPTIONAL. Requests access to `phone_number` and `phone_number_verified` Claims. | | OfflineAccess | `offline_access` | MUST NOT be used with the OpenID Connect Implicit Client Implementer’s Guide. Used in accordance with the OpenID Connect Basic Client Implementer’s Guide. | ### HttpHeaders [Section titled “HttpHeaders”](#httpheaders) | Name | Value | | --------- | ------------ | | DPoP | `DPoP` | | DPoPNonce | `DPoP-Nonce` | ## JWT Claim Types [Section titled “JWT Claim Types”](#jwt-claim-types) The `JwtClaimTypes` class has all standard claim types found in the OpenID Connect, JWT and OAuth 2.0 specs -many of them are also aggregated at [IANA](https://www.iana.org/assignments/jwt/jwt.xhtml). | Claim Type | Value | Description/Remarks | | :---------------------------------- | :---------------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | Subject | `sub` | Unique Identifier for the End-User at the Issuer. | | Name | `name` | End-User’s full name in displayable form including all name parts, possibly including titles and suffixes, ordered according to the End-User’s locale and preferences. | | GivenName | `given_name` | Given name(s) or first name(s) of the End-User. Note that in some cultures, people can have multiple given names; all can be present, with the names being separated by space characters. | | FamilyName | `family_name` | Surname(s) or last name(s) of the End-User. Note that in some cultures, people can have multiple family names or no family name; all can be present, with the names being separated by space characters. | | MiddleName | `middle_name` | Middle name(s) of the End-User. Note that in some cultures, people can have multiple middle names; all can be present, with the names being separated by space characters. Also note that in some cultures, middle names are not used. | | NickName | `nickname` | Casual name of the End-User that may or may not be the same as the given\_name. For instance, a nickname value of Mike might be returned alongside a given\_name value of Michael. | | PreferredUserName | `preferred_username` | Shorthand name by which the End-User wishes to be referred to at the RP, such as janedoe or j.doe. This value MAY be any valid JSON string including special characters. **Remarks:** The relying party MUST NOT rely upon this value being unique, as discussed in the OpenID Connect specification. | | Profile | `profile` | URL of the End-User’s profile page. The contents of this Web page SHOULD be about the End-User. | | Picture | `picture` | URL of the End-User’s profile picture. This URL MUST refer to an image file (e.g., PNG, JPEG, or GIF image file). **Remarks:** This URL SHOULD specifically reference a profile photo of the End-User rather than an arbitrary photo. | | WebSite | `website` | URL of the End-User’s Web page or blog. This Web page SHOULD contain information published by the End-User or an organization related to the End-User. | | Email | `email` | End-User’s preferred e-mail address. Its value MUST conform to the RFC 5322 syntax. The relying party MUST NOT rely upon this value being unique. | | EmailVerified | `email_verified` | `"true"` if the End-User’s e-mail address has been verified; otherwise `"false"`. **Remarks:** Verification methods vary depending on trust frameworks or agreements. | | Gender | `gender` | End-User’s gender. Allowed values include `"female"` and `"male"`, with additional values permissible when the predefined ones are not applicable. | | BirthDate | `birthdate` | End-User’s birthday in ISO 8601 format (e.g., YYYY-MM-DD). The year MAY be `0000`, indicating it is omitted. | | ZoneInfo | `zoneinfo` | String representing the End-User’s time zone, e.g., `Europe/Paris` or `America/Los_Angeles`. | | Locale | `locale` | End-User’s locale represented as a BCP47 language tag (e.g., `en-US`, `fr-CA`). Compatibility notes suggest some implementations may use underscores instead of dashes. | | PhoneNumber | `phone_number` | End-User’s preferred telephone number. E.164 format is recommended, including extensions. | | PhoneNumberVerified | `phone_number_verified` | `"true"` if the End-User’s phone number has been verified; otherwise `"false"`. **Remarks:** Applies to numbers in E.164 format. | | Address | `address` | End-User’s preferred postal address. Contains a JSON structure with predefined fields from the OpenID Connect specification. | | Audience | `aud` | Audience(s) that this ID Token is intended for. It MUST contain the OAuth 2.0 client\_id of the Relying Party. | | Issuer | `iss` | Issuer Identifier for the Issuer of the response in the form of a URL. | | NotBefore | `nbf` | The time before which the JWT MUST NOT be accepted, specified in seconds since 1970-01-01T00:00:00Z. | | Expiration | `exp` | The token’s expiration time in seconds since 1970-01-01T00:00:00Z. | | UpdatedAt | `updated_at` | Time of last update for the End-User’s information, measured in seconds since 1970-01-01T00:00:00Z. | | IssuedAt | `iat` | Time at which the JWT was issued, specified in seconds since 1970-01-01T00:00:00Z. | | AuthenticationMethod | `amr` | JSON array of strings identifying the authentication method(s) used. | | SessionId | `sid` | Session identifier representing an OP session at an RP for a logged-in End-User. | | AuthenticationContextClassReference | `acr` | Specifies the Authentication Context Class Reference value satisfied during authentication. **Remarks:** Example: `"level 0"` indicates authentication did not meet ISO/IEC 29115 level 1. | | AuthenticationTime | `auth_time` | Time of the End-User’s authentication, measured in seconds since 1970-01-01T00:00:00Z. | | AuthorizedParty | `azp` | Authorized party to which the ID Token was issued. | | AccessTokenHash | `at_hash` | Access token hash value derived using a specific hash algorithm. | | AuthorizationCodeHash | `c_hash` | Authorization code hash value derived using a specific hash algorithm. | | StateHash | `s_hash` | State hash value derived using a specific hash algorithm. | | Nonce | `nonce` | Value used to mitigate replay attacks between a Client session and an ID Token. | | JwtId | `jti` | A unique identifier for the token to prevent reuse. | | Events | `events` | Defines a set of event statements to describe a logical event that has occurred. | | ClientId | `client_id` | OAuth 2.0 Client Identifier valid at the Authorization Server. | | Scope | `scope` | OpenID Connect “openid” scope value. Additional scope values can be included. | | Actor | `act` | Identifies the acting party to whom authority has been delegated. | | MayAct | `may_act` | Statement asserting that a party is authorized to act on behalf of another party. | | Id | `id` | An identifier. | | IdentityProvider | `idp` | The identity provider. | | Role | `role` | The role. | | Roles | `roles` | The roles. | | ReferenceTokenId | `reference_token_id` | Reference token identifier. | | Confirmation | `cnf` | The confirmation. | | Algorithm | `alg` | The algorithm. | | JsonWebKey | `jwk` | JSON web key. | | TokenType | `typ` | The token type. | | DPoPHttpMethod | `htm` | DPoP HTTP method. | | DPoPHttpUrl | `htu` | DPoP HTTP URL. | | DPoPAccessTokenHash | `ath` | DPoP access token hash. | ### JwtTypes [Section titled “JwtTypes”](#jwttypes) `JwtTypes` is a nested class that provides a set of constants for confirmation methods. It can be found under the `JwtConstants` class. | Type | Value | Description | | :----------------------- | :-------------------------- | :---------------------------------------------------------- | | AccessToken | `at+jwt` | OAuth 2.0 access token. | | AuthorizationRequest | `oauth-authz-req+jwt` | JWT secured authorization request. | | DPoPProofToken | `dpop+jwt` | DPoP proof token. | | IntrospectionJwtResponse | `token-introspection+jwt` | Token introspection JWT response. | | ClientAuthentication | `client-authentication+jwt` | Client authentication JWT (for use with private\_key\_jwt). | ### ConfirmationMethods [Section titled “ConfirmationMethods”](#confirmationmethods) `ConfirmationMethods` is a nested class that provides a set of constants for confirmation methods. It can be found under the `JwtConstants` class. | Method | Value | Description | | :------------------- | :--------- | :----------------------------------------- | | JsonWebKey | `jwk` | JSON web key. | | JwkThumbprint | `jkt` | JSON web key thumbprint. | | X509ThumbprintSha256 | `x5t#S256` | X.509 certificate thumbprint using SHA256. | ----- # Epoch Time Conversion > Learn about converting between DateTime and Unix/Epoch time formats in Duende IdentityModel for JWT tokens JSON Web Token (JWT) tokens use so-called [Epoch or Unix time](https://en.wikipedia.org/wiki/Unix_time) to represent date/times, which is the number of seconds that have elapsed since January 1, 1970 (midnight UTC/GMT). In .NET, you can convert `DateTimeOffset` to Unix/Epoch time via the two methods of `ToUnixTimeSeconds` and `ToUnixTimeMilliseconds`: EpochTimeExamples.cs ```csharp var seconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); var milliseconds = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds(); ``` ----- # Creating Authorize and EndSession URLs > Helper utilities for creating OAuth 2.0/OpenID Connect authorization and end session URLs with query parameters The *RequestUrl* class is a helper for creating URLs with query string parameters, e.g.: ```csharp var ru = new RequestUrl("https://server/endpoint"); // produces https://server/endpoint?foo=foo&bar=bar var url = ru.Create(new { foo: "foo", bar: "bar" }); ``` As a parameter to the *Create* method you can either pass in an object, or a string dictionary. In both cases the properties/values will be serialized to key/value pairs. Note All values will be URL encoded. ## Authorization Endpoint [Section titled “Authorization Endpoint”](#authorization-endpoint) For most cases, the [OAuth 2.0](https://tools.ietf.org/html/rfc6749#section-3.1) and [OpenID Connect](https://openid.net/specs/openid-connect-core-1_0.html#authorizationendpoint) authorization endpoint expects a GET request with a number of query string parameters. The *CreateAuthorizeUrl* extension method creates URLs for the authorize endpoint - it has support the most common parameters: ```csharp /// /// Creates an authorize URL. /// /// The request. /// The client identifier. /// The response type. /// The scope. /// The redirect URI. /// The state. /// The nonce. /// The login hint. /// The acr values. /// The prompt. /// The response mode. /// The code challenge. /// The code challenge method. /// The display option. /// The max age. /// The ui locales. /// The id_token hint. /// The request_uri for PAR. /// Extra parameters. /// public static string CreateAuthorizeUrl(this RequestUrl request, string clientId, string responseType, string scope = null, string redirectUri = null, string state = null, string nonce = null, string loginHint = null, string acrValues = null, string prompt = null, string responseMode = null, string codeChallenge = null, string codeChallengeMethod = null, string display = null, int? maxAge = null, string uiLocales = null, string idTokenHint = null, string requestUri = null, Parameters extra = null) { ... } ``` Example: ```csharp var ru = new RequestUrl("https://demo.duendesoftware.com/connect/authorize"); var url = ru.CreateAuthorizeUrl( clientId: "client", responseType: "implicit", redirectUri: "https://app.com/callback", nonce: "xyz", scope: "openid"); ``` Note The *extra* parameter is of type `Parameters`, which can be constructed from a dictionary or populated with key-value pairs. ## EndSession Endpoint [Section titled “EndSession Endpoint”](#endsession-endpoint) The *CreateEndSessionUrl* extensions methods supports the most common parameters: ```csharp /// /// Creates a end_session URL. /// /// The request. /// The id_token hint. /// The post logout redirect URI. /// The state. /// The extra parameters. /// public static string CreateEndSessionUrl(this RequestUrl request, string idTokenHint = null, string postLogoutRedirectUri = null, string state = null, Parameters extra = null) { ... } ``` Note The *extra* parameter is of type `Parameters`, which can be constructed from a dictionary or populated with key-value pairs. ----- # Time-Constant String Comparison > Learn about implementing secure string comparison to prevent timing attacks in security-sensitive contexts using TimeConstantComparer Note Starting with .NET Core 2.1 this functionality is built in via [CryptographicOperations.FixedTimeEquals](https://docs.microsoft.com/en-us/dotnet/api/system.security.cryptography.cryptographicoperations.fixedtimeequals?view=netcore-2.1) When comparing strings in a security context (e.g. comparing keys), you should avoid leaking timing information. Standard string comparison algorithms are optimized to stop comparing characters as soon as a difference is found. An attacker can exploit this by making many requests with strings that all differ in the first character. The strings that begin with an incorrect first character will make a single character comparison and stop. However, the strings that begin with a correct first character will need to make additional string comparisons, and thus take more time before they stop. Sophisticated attackers can measure this difference and use it to deduce the characters that their input is being compared to. ## Time-Constant String Comparison [Section titled “Time-Constant String Comparison”](#time-constant-string-comparison) ```csharp using System.Security.Cryptography; // Simulated sensitive data (e.g., a secure token or password hash) var storedHash = Convert.FromBase64String("HJG3+eXAIoQsNI1ASD2i+If7xhQAEjZLefBWo5pcuDE="); // Incoming hash to validate (e.g., provided by the user) var providedHash = Convert.FromBase64String("HJG3+eXAIoQsNI1ASD2i+If7xhQAEjZLefBWo5pcuDE="); // Compare the two byte sequences using FixedTimeEquals var isEqual = CryptographicOperations.FixedTimeEquals(storedHash, providedHash); var result = isEqual ? "the hashes match!" : "the hashes do not match!"; Console.WriteLine(result); ``` ## TimeConstantComparer [Section titled “TimeConstantComparer”](#timeconstantcomparer) The *TimeConstantComparer* class defends against these timing attacks by implementing a constant-time string comparison. The string comparison is a constant-time operation in the sense that comparing strings of equal length always performs the same amount of work. Usage example: ```csharp using Duende.IdentityModel; // Simulated sensitive data (e.g., a secure token or password hash) var storedHash = "HJG3+eXAIoQsNI1ASD2i+If7xhQAEjZLefBWo5pcuDE="; // Incoming hash to validate (e.g., provided by the user) var providedHash = "HJG3+eXAIoQsNI1ASD2i+If7xhQAEjZLefBWo5pcuDE="; // Compare the two byte sequences using FixedTimeEquals var isEqual = TimeConstantComparer.IsEqual(storedHash, providedHash); var result = isEqual ? "the hashes match!" : "the hashes do not match!"; Console.WriteLine(result); ``` ----- # Fluent X.509 Certificate Store API > Provides a simplified, fluent API for accessing and managing X.509 certificates in a certificate store. A common place to store X.509 certificates is within a host’s X.509 certificate store. With .NET APIs, this is done using the `X509Store` class. ```csharp using System.Security.Cryptography.X509Certificates; // with .NET APIs using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadOnly); using var certificate = store.Certificates .Find(X509FindType.FindBySubjectDistinguishedName, "CN=localhost", false)[0]; if (certificate == null) throw new InvalidOperationException("Certificate not found"); Console.WriteLine(certificate); ``` The *X509* class in the IdentityModel library is a simplified API to load certificates from a certificate store. The following code loads a certificate by name from the personal machine store: ```csharp using Duende.IdentityModel; using var certificate = X509.CurrentUser .My .SubjectDistinguishedName .Find("CN=localhost", false) .FirstOrDefault(); if (certificate == null) throw new InvalidOperationException("Certificate not found"); Console.WriteLine(certificate); ``` ### Certificate Store Locations [Section titled “Certificate Store Locations”](#certificate-store-locations) You can load certificates from the following machine or user stores: * *My* * *AddressBook* * *TrustedPeople* * *CertificateAuthority* * *TrustedPublisher* ### Certificate Search Options [Section titled “Certificate Search Options”](#certificate-search-options) You can search for a certificate by the following attributes: * Subject name, * Thumbprint * Issuer name * Serial number. ### Debugging Certificates in a Store [Section titled “Debugging Certificates in a Store”](#debugging-certificates-in-a-store) When finding it difficult to find a certificate by name, you can use the following code to list all certificates in a store for debugging purposes: ```csharp using System.Security.Cryptography.X509Certificates; using var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadOnly); var certificates = store.Certificates; foreach (var certificate in certificates) { Console.WriteLine($"{certificate.Subject} ({certificate.Thumbprint})"); } ``` ----- # Duende IdentityServer > Overview of Duende IdentityServer framework for OpenID Connect and OAuth 2.x protocols, covering extensibility, security scenarios, licensing, and support. Duende IdentityServer is a highly extensible, standards-compliant framework for implementing the OpenID Connect, OAuth 2.x and SAML protocols in .NET and ASP.NET Core. It offers deep flexibility for handling authentication, authorization, and token issuance and can be adapted to fit complex custom security scenarios. [GitHub Repository](https://github.com/DuendeSoftware/products/tree/main/identity-server/)View the source code for this library on GitHub. [NuGet Package](https://www.nuget.org/packages/Duende.IdentityServer/)View the package on NuGet.org. ## Key Capabilities [Section titled “Key Capabilities”](#key-capabilities) [Fundamentals](/identityserver/fundamentals/)Learn what you need to bring to IdentityServer: UI, data stores, and identity/profile storage — plus ready-made options for each. [User Management](/identityserver/identity/user-management/)Full user lifecycle management including authentication methods, profiles, roles, and groups. [SAML Support](/identityserver/saml/)Act as both a SAML Identity Provider and Service Provider with full protocol support. [FAPI 2.0 & Conformance](/identityserver/tokens/fapi-2-0-specification/)Financial-grade API security profile with certified conformance results and reporting. [Dynamic Providers](/identityserver/ui/login/dynamicproviders/)Configure external identity providers from a store at runtime without recompilation. [Multi-Issuer](/identityserver/tokens/issuer/#multi-issuer)Run a single IdentityServer instance with multiple issuer identities. [Automatic Key Management](/identityserver/fundamentals/key-management/)Automatic rotation and management of signing keys with zero downtime. ## Extensibility Points [Section titled “Extensibility Points”](#extensibility-points) * **Customizable User Experience**: Go beyond simple branding to fully customizable user interfaces. * **Core Engine Customization**: The engine itself is modular and built from services that can be extended or overridden. ## Advanced Security Scenarios [Section titled “Advanced Security Scenarios”](#advanced-security-scenarios) Duende IdentityServer supports a wide range of security scenarios for modern applications: * **Federation**: Easily integrate with external identity providers or other authentication services using [federation](/identityserver/ui/federation/). * **Token Exchange**: Enable secure token exchange between clients and services with [Token Exchange](/identityserver/tokens/extension-grants/#token-exchange). * **Audience Constrained Tokens**: Restrict tokens to specific audiences, increasing security in multi-service architectures. Learn more about [audience-constrained tokens](/identityserver/fundamentals/resources/isolation/). * **Sender Constrained Tokens**: Implement Proof of Possession (PoP) tokens with [DPoP or mTLS](/identityserver/tokens/pop/), which bind tokens to the client, adding another layer of protection. * **Pushed Authorization Requests (PAR)**: Support [Pushed Authorization Requests](/identityserver/tokens/par/) to enhance the security of the authorization flow. * **FAPI 2.0**: Protect APIs in high-value scenarios with the [FAPI 2.0 Security profile](/identityserver/tokens/fapi-2-0-specification/). ## Licensing [Section titled “Licensing”](#licensing) Duende IdentityServer is source-available, but **requires a paid [license](/general/licensing/) for production use.** * **Development and Testing**: You are free to use and explore the code for development, testing, or personal projects without a license. * **Production**: A license is required for production environments. * **Free Community Edition**: A free Community Edition license is available for qualifying companies and non-profit organizations. Learn more [here](https://duendesoftware.com/products/communityedition). ## Reporting Issues and Getting Support [Section titled “Reporting Issues and Getting Support”](#reporting-issues-and-getting-support) * For bug reports or feature requests, [use our developer community forum](https://github.com/DuendeSoftware/community). * For security-related concerns, please contact us privately at: ****. ----- # Protecting APIs > Learn how to secure and protect your APIs using Duende IdentityServer's token-based authentication and authorization Duende IdentityServer issues tokens for accessing resources. These resources are very often HTTP-based APIs, but could be also other “invocable” functionality like messaging endpoints, gRPC services or even good old XML Web Services. See the [issuing tokens](/identityserver/tokens/) section on more information on access tokens and how to request them. ## Adding API Endpoints to IdentityServer [Section titled “Adding API Endpoints to IdentityServer”](#adding-api-endpoints-to-identityserver) It’s a common scenario to add additional API endpoints to the application hosting IdentityServer. These endpoints are typically protected by IdentityServer itself. For simple scenarios, we give you some helpers. See the advanced section to understand more of the internal plumbing. Note You could achieve the same by using either Microsoft’s `JwtBearer` handler. But this requires more configuration and creates dependencies on external libraries that might lead to conflicts in future updates. Start by registering your API as an `ApiScope`, (or resource) e.g.: ```csharp var scopes = new List { // local API new ApiScope(IdentityServerConstants.LocalApi.ScopeName), }; ``` …and give your clients access to this API, e.g.: ```csharp new Client { // rest omitted AllowedScopes = { IdentityServerConstants.LocalApi.ScopeName }, } ``` Note The value of `IdentityServerConstants.LocalApi.ScopeName` is `IdentityServerApi`. To enable token validation for local APIs, add the following to your IdentityServer startup: Program.cs ```csharp builder.Services.AddLocalApiAuthentication(); ``` To protect an API endpoint, call `RequireAuthorization` with the `LocalApi.PolicyName` policy: ```csharp app.MapGet("/localApi", () => { // omitted }).RequireAuthorization(LocalApi.PolicyName); ``` To protect an API controller, decorate it with an `Authorize` attribute using the `LocalApi.PolicyName` policy: ```csharp [Route("localApi")] [Authorize(LocalApi.PolicyName)] public class LocalApiController : ControllerBase { public IActionResult Get() { // omitted } } ``` Authorized clients can then request a token for the `IdentityServerApi` scope and use it to call the API. ## Discovery [Section titled “Discovery”](#discovery) You can also add your endpoints to the discovery document if you want, e.g.like this:: Program.cs ```csharp builder.Services.AddIdentityServer(options => { options.Discovery.CustomEntries.Add("local_api", "~/localapi"); }) ``` ## Advanced [Section titled “Advanced”](#advanced) Under the hood, the `AddLocalApiAuthentication` helper does a couple of things: * adds an authentication handler that validates incoming tokens using IdentityServer’s built-in token validation engine (the name of this handler is `IdentityServerAccessToken` or `IdentityServerConstants.LocalApi.AuthenticationScheme` * configures the authentication handler to require a scope claim inside the access token of value `IdentityServerApi` * sets up an authorization policy that checks for a scope claim of value `IdentityServerApi` This covers the most common scenarios. You can customize this behavior in the following ways: * Add the authentication handler yourself by calling `services.AddAuthentication().AddLocalApi(...)`. This way you can specify the required scope name yourself, or (by specifying no scope at all) accept any token from the current IdentityServer instance * Do your own scope validation/authorization in your controllers using custom policies or code, e.g.: Program.cs ```csharp builder.Services.AddAuthorization(options => { options.AddPolicy(IdentityServerConstants.LocalApi.PolicyName, policy => { policy.AddAuthenticationSchemes(IdentityServerConstants.LocalApi.AuthenticationScheme); policy.RequireAuthenticatedUser(); // custom requirements }); }); ``` ## Claims Transformation [Section titled “Claims Transformation”](#claims-transformation) You can provide a callback to transform the claims of the incoming token after validation. Either use the helper method, e.g.: Program.cs ```csharp builder.Services.AddLocalApiAuthentication(principal => { principal.Identities.First().AddClaim(new Claim("additional_claim", "additional_value")); return Task.FromResult(principal); }); ``` …or implement the event on the options if you add the authentication handler manually. ## DPoP Support v8.0 [Section titled “DPoP Support ”v8.0](#dpop-support) The local API authentication handler supports [DPoP](/identityserver/tokens/pop/) proof validation. When a client sends a DPoP-bound access token to a local API, the handler validates the accompanying proof token automatically. Requests must include exactly one `DPoP` header. If multiple `DPoP` headers are present, the handler rejects the request with an `invalid_dpop_proof` error. ----- # Authorization based on Scopes and Claims > Guide for implementing authorization using scope claims and ASP.NET Core authorization policies with IdentityServer access tokens The access token will include additional claims that can be used for authorization, e.g. the `scope` claim will reflect the scope the client requested (and was granted) during the token request. In ASP.NET core, the contents of the JWT payload get transformed into claims and packaged up in a `ClaimsPrincipal`. So you can always write custom validation or authorization logic in C#: ```csharp public IActionResult Get() { var isAllowed = User.HasClaim("scope", "read"); // rest omitted } ``` For better encapsulation and re-use, consider using the ASP.NET Core [authorization policy](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies) feature. With this approach, you would first turn the claim requirement(s) into a named policy: ```csharp builder.Services.AddAuthorization(options => { options.AddPolicy("read_access", policy => policy.RequireClaim("scope", "read")); }); ``` …and then enforce it, e.g. using the routing table: ```csharp app.MapControllers().RequireAuthorization("read_access"); ``` …or imperatively inside the endpoint handler: ```csharp app.MapGet("/", async (IAuthorizationService authz, ClaimsPrincipal user) => { var allowed = await authz.AuthorizeAsync(user, "read_access"); if (!allowed.Succeeded) { return Results.Forbid(); } // rest omitted }); ``` … or declaratively: ```csharp app.MapGet("/", () => { // rest omitted }).RequireAuthorization("read_access"); ``` #### Scope Claim Format [Section titled “Scope Claim Format”](#scope-claim-format) Historically, Duende IdentityServer emitted the `scope` claims as an array in the JWT. This works very well with the .NET deserialization logic, which turns every array item into a separate claim of type `scope`. The newer *JWT Profile for OAuth* [spec](/identityserver/overview/specs/) mandates that the scope claim is a single space delimited string. You can switch the format by setting the `EmitScopesAsSpaceDelimitedStringInJwt` on the [options](/identityserver/reference/v8/options/). But this means that the code consuming access tokens might need to be adjusted. The following code can do a conversion to the *multiple claims* format that .NET prefers: ```csharp namespace IdentityModel.AspNetCore.AccessTokenValidation; /// /// Logic for normalizing scope claims to separate claim types /// public static class ScopeConverter { /// /// Logic for normalizing scope claims to separate claim types /// /// /// public static ClaimsPrincipal NormalizeScopeClaims(this ClaimsPrincipal principal) { var identities = new List(); foreach (var id in principal.Identities) { var identity = new ClaimsIdentity(id.AuthenticationType, id.NameClaimType, id.RoleClaimType); foreach (var claim in id.Claims) { if (claim.Type == "scope") { if (claim.Value.Contains(' ')) { var scopes = claim.Value.Split(' ', StringSplitOptions.RemoveEmptyEntries); foreach (var scope in scopes) { identity.AddClaim(new Claim("scope", scope, claim.ValueType, claim.Issuer)); } } else { identity.AddClaim(claim); } } else { identity.AddClaim(claim); } } identities.Add(identity); } return new ClaimsPrincipal(identities); } } ``` The above code could then be called as an extension method or as part of [claims transformation](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.iclaimstransformation). ----- # Validating Proof-of-Possession > Guide for validating Proof-of-Possession (PoP) access tokens in ASP.NET Core using mTLS or DPoP mechanisms IdentityServer can [bind tokens to clients](/identityserver/tokens/pop/#proof-of-possession-styles) using either mTLS or DPoP, creating a `Proof-of-Possession` (PoP) access token. When one of these mechanisms is used, APIs that use those access tokens for authorization need to validate the binding between the client and token. This document describes how to perform such validation, depending on which mechanism was used to produce a PoP token. ### Validating mTLS [Section titled “Validating mTLS”](#validating-mtls) If you are using a [mutual TLS connection](/identityserver/tokens/pop/#mutual-tls) to establish proof-of-possession, the resulting access token will contain a `cnf` claim containing the client’s certificate thumbprint. APIs validate such tokens by comparing this thumbprint to the thumbprint of the client certificate in the mTLS connection. This validation should be performed early in the pipeline, ideally immediately after the standard validation of the access token. You can do so with custom middleware like this: ```csharp // normal token validation happens here app.UseAuthentication(); // This adds custom middleware to validate cnf claim app.UseConfirmationValidation(); app.UseAuthorization(); ``` Here, `UseConfirmationValidation` is an extension method that registers the middleware that performs the necessary validation: ```csharp public static class ConfirmationValidationExtensions { public static IApplicationBuilder UseConfirmationValidation(this IApplicationBuilder app, ConfirmationValidationMiddlewareOptions options = default) { return app.UseMiddleware(options ?? new ConfirmationValidationMiddlewareOptions()); } } ``` And this is the actual middleware that validates the `cnf` claim: ```csharp // this middleware validates the cnf claim (if present) against the thumbprint of the X.509 client certificate for the current client public class ConfirmationValidationMiddleware { private readonly RequestDelegate _next; private readonly ILogger _logger; private readonly ConfirmationValidationMiddlewareOptions _options; public ConfirmationValidationMiddleware( RequestDelegate next, ILogger logger, ConfirmationValidationMiddlewareOptions options = null) { _next = next; _logger = logger; _options ??= new ConfirmationValidationMiddlewareOptions(); } public async Task Invoke(HttpContext ctx) { if (ctx.User.Identity.IsAuthenticated) { // read the cnf claim from the validated token var cnfJson = ctx.User.FindFirst("cnf")?.Value; if (!String.IsNullOrWhiteSpace(cnfJson)) { // if present, make sure a valid certificate was presented as well var certResult = await ctx.AuthenticateAsync(_options.CertificateSchemeName); if (!certResult.Succeeded) { await ctx.ChallengeAsync(_options.CertificateSchemeName); return; } // get access to certificate from transport var certificate = await ctx.Connection.GetClientCertificateAsync(); var thumbprint = Base64UrlTextEncoder.Encode(certificate.GetCertHash(HashAlgorithmName.SHA256)); // retrieve value of the thumbprint from cnf claim var cnf = JObject.Parse(cnfJson); var sha256 = cnf.Value("x5t#S256"); // compare thumbprint claim with thumbprint of current TLS client certificate if (String.IsNullOrWhiteSpace(sha256) || !thumbprint.Equals(sha256, StringComparison.OrdinalIgnoreCase)) { _logger.LogError("certificate thumbprint does not match cnf claim."); await ctx.ChallengeAsync(_options.JwtBearerSchemeName); return; } _logger.LogDebug("certificate thumbprint matches cnf claim."); } } await _next(ctx); } } public class ConfirmationValidationMiddlewareOptions { public string CertificateSchemeName { get; set; } = CertificateAuthenticationDefaults.AuthenticationScheme; public string JwtBearerSchemeName { get; set; } = JwtBearerDefaults.AuthenticationScheme; } ``` ### Validating DPoP [Section titled “Validating DPoP”](#validating-dpop) When using [DPoP](/identityserver/tokens/pop/#enabling-dpop-in-identityserver) for proof-of-possession, validating the `cnf` claim requires several steps: 1. Validating the access token as normal 2. Validating the DPoP proof token from the `DPoP` HTTP request header 3. Ensuring the authorization header uses the DPoP scheme 4. Validating the JWT format of the proof token 5. Verifying the `cnf` claim matches between tokens 6. Validating the HTTP method and URL match the request 7. Detecting replay attacks using storage 8. Managing nonce generation and validation 9. Handling clock skew between systems 10. Returning appropriate error response headers when validation fails This comprehensive validation process requires careful implementation to ensure security. Luckily for developers, we’ve implemented these steps into an easy-to-use library. You can use the `Duende.AspNetCore.Authentication.JwtBearer` NuGet package to implement this validation. ```bash dotnet add package Duende.AspnetCore.Authentication.JwtBearer ``` With this package, the configuration necessary in your startup can be as simple as this: ```csharp // adds the normal JWT bearer validation builder.Services.AddAuthentication("token") .AddJwtBearer("token", options => { options.Authority = Constants.Authority; options.TokenValidationParameters.ValidateAudience = false; options.MapInboundClaims = false; options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" }; }); // extends the "token" scheme above with DPoP processing and validation builder.Services.ConfigureDPoPTokensForScheme("token"); ``` You will also typically need a distributed cache, used to perform replay detection of DPoP proofs. `Duende.AspNetCore.Authentication.JwtBearer` relies on `IDistributedCache` for this, so you can supply the cache implementation of your choice. See the [Microsoft documentation](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/distributed?view=aspnetcore-8.0) for more details on setting up distributed caches, along with many examples, including Redis, CosmosDB, and Sql Server. A full sample [using the default in memory caching](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/DPoP) is available on GitHub. ----- # Using JSON Web Tokens (JWTs) > Guide for validating JWT bearer tokens in ASP.NET Core applications using the JWT authentication handler On ASP.NET Core, you typically use the [JWT authentication handler](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.JwtBearer) for validating JWT bearer tokens. ## Validating A JWT [Section titled “Validating A JWT”](#validating-a-jwt) First you need to add a reference to the authentication handler in your API project: ```xml ``` If all you care about is making sure that an access token comes from your trusted IdentityServer, the following snippet shows the typical JWT validation configuration for ASP.NET Core: ```csharp builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { // base-address of your identityserver options.Authority = "https://demo.duendesoftware.com"; // audience is optional, make sure you read the following paragraphs // to understand your options options.TokenValidationParameters.ValidateAudience = false; // it's recommended to check the type header to avoid "JWT confusion" attacks options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" }; }); ``` ## Adding Audience Validation [Section titled “Adding Audience Validation”](#adding-audience-validation) Simply making sure that the token is coming from a trusted issuer is not good enough for most cases. In more complex systems, you will have multiple resources and multiple clients. Not every client might be authorized to access every resource. In OAuth there are two complementary mechanisms to embed more information about the “functionality” that the token is for - `audience` and `scope` (see [defining resources](/identityserver/fundamentals/resources/api-resources/) for more information). If you designed your APIs around the concept of [API resources](/identityserver/fundamentals/resources/api-resources/), your IdentityServer will emit the `aud` claim by default (`api1` in this example): ```text { "typ": "at+jwt", "kid": "123" }. { "aud": "api1", "client_id": "mobile_app", "sub": "123", "scope": "read write delete" } ``` If you want to express in your API, that only access tokens for the `api1` audience (aka API resource name) are accepted, change the above code snippet to: ```csharp builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(options => { options.Authority = "https://demo.duendesoftware.com"; options.Audience = "api1"; options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" }; }); ``` Dynamic Proof-of-Possession (DPoP) validation You can make use of the [JwtBearer Extensions](/identityserver/apis/aspnetcore/confirmation/#validating-dpop) to validate Dynamic Proof-of-Possession (DPoP) access tokens in ASP.NET Core. ----- # Reference Tokens > Guide for implementing reference token validation in ASP.NET Core APIs using OAuth 2.0 token introspection If you are using [reference tokens](/identityserver/tokens/reference/), you need an authentication handler that implements the back-channel validation via the [OAuth 2.0 token introspection](https://tools.ietf.org/html/rfc7662) protocol, e.g. [Duende.AspNetCore.Authentication.OAuth2Introspection](/introspection/): Program.cs ```csharp builder.Services.AddAuthentication("token") .AddOAuth2Introspection("token", options => { options.Authority = Constants.Authority; // this maps to the API resource name and secret options.ClientId = "resource1"; options.ClientSecret = "secret"; }); ``` ## Supporting Both JWTs And Reference Tokens [Section titled “Supporting Both JWTs And Reference Tokens”](#supporting-both-jwts-and-reference-tokens) It is not uncommon to use the same API with both JWTs and reference tokens. In this case you set up two authentication handlers, make one the default handler and provide some forwarding logic, e.g.: Program.cs ```csharp builder.Services.AddAuthentication("token") // JWT tokens .AddJwtBearer("token", options => { options.Authority = Constants.Authority; options.Audience = "resource1"; options.TokenValidationParameters.ValidTypes = new[] { "at+jwt" }; // if token does not contain a dot, it is a reference token options.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection"); }) // reference tokens .AddOAuth2Introspection("introspection", options => { options.Authority = Constants.Authority; options.ClientId = "resource1"; options.ClientSecret = "secret"; }); ``` The logic of the forward selector looks like this: IntrospectionUtilities.cs ```csharp /// /// Provides a forwarding func for JWT vs reference tokens (based on existence of dot in token) /// /// Scheme name of the introspection handler /// public static Func ForwardReferenceToken(string introspectionScheme = "introspection") { string Select(HttpContext context) { var (scheme, credential) = GetSchemeAndCredential(context); if (scheme.Equals("Bearer", StringComparison.OrdinalIgnoreCase) && !credential.Contains(".")) { return introspectionScheme; } return null; } return Select; } /// /// Extracts scheme and credential from Authorization header (if present) /// /// /// public static (string, string) GetSchemeAndCredential(HttpContext context) { var header = context.Request.Headers["Authorization"].FirstOrDefault(); if (string.IsNullOrEmpty(header)) { return ("", ""); } var parts = header.Split(' ', StringSplitOptions.RemoveEmptyEntries); if (parts.Length != 2) { return ("", ""); } return (parts[0], parts[1]); } ``` ----- # Configuration API > Documentation for the Configuration API endpoints that enable management and configuration of IdentityServer implementations Tip Added in Duende IdentityServer 6.3 The Configuration API is a collection of endpoints that allow for management and configuration of an IdentityServer implementation. The Configuration API can be hosted either separately or within the IdentityServer implementation, and is distributed through the separate [Duende.IdentityServer.Configuration NuGet package](https://www.nuget.org/packages/Duende.IdentityServer.Configuration). Currently, the Configuration API supports the [Dynamic Client Registration](/identityserver/configuration/dcr/) protocol. Note This feature is part of the [Duende IdentityServer Business (legacy), Enterprise (legacy), Standard, Advanced, and Custom Edition](https://duendesoftware.com/products/identityserver). The Configuration API source code is available [on GitHub](https://github.com/DuendeSoftware/products/tree/main/identity-server/src/Configuration). Samples of the Configuration API are available [here](/identityserver/samples/configuration/). ----- # Dynamic Client Registration (DCR) > Learn how to configure and use Dynamic Client Registration (DCR) to automatically register OAuth clients with IdentityServer Dynamic Client Registration (DCR) is the process of registering OAuth clients dynamically. It allows OAuth client applications to programmatically register themselves with an authorization server at runtime, rather than requiring manual configuration. The client provides information about itself and specifies its desired configuration in an HTTP request to the configuration endpoint. If the request is authorized and valid, the endpoint will then create the necessary client configuration and return an HTTP response describing the new client. DCR eliminates the need for a manual registration process, making it more efficient and less time-consuming to register new clients. It can help automate the onboarding of new applications in large-scale OAuth ecosystems, such as microservices, mobile apps, and partner APIs. ## Installation And Hosting [Section titled “Installation And Hosting”](#installation-and-hosting) DCR in Duende IdentityServer is provided as a separate NuGet package, [`Duende.IdentityServer.Configuration`](https://www.nuget.org/packages/Duende.IdentityServer.Configuration), which contains the Configuration API and endpoints required to support DCR. The Configuration API can be installed in a separate host from IdentityServer, or in the same host. In many cases, it is desirable to host the configuration API and IdentityServer separately. This facilitates the ability to restrict access to the configuration API at the network level separately from IdentityServer and keeps IdentityServer’s access to the configuration data read-only. In other cases, you may find that hosting the two systems together better fits your needs. ### Separate Host For Configuration API [Section titled “Separate Host For Configuration API”](#separate-host-for-configuration-api) To host the Configuration API separately from IdentityServer, you will need to create a new ASP.NET Core Web application which will host the Configuration API. 1. **Create a new project of type “Empty Web Application”** Terminal ```bash dotnet new web -n Configuration ``` 2. **Add the `Duende.IdentityServer.Configuration` package** Terminal ```bash cd Configuration dotnet add package Duende.IdentityServer.Configuration ``` 3. **Configure services to include the Configuration API** Program.cs ```csharp builder.Services.AddIdentityServerConfiguration(opt => opt.LicenseKey = ""; ); ``` Note This feature is part of the [Duende IdentityServer Business, Enterprise, Standard, Advanced Edition](https://duendesoftware.com/products/identityserver). You don’t need to acquire an additional license, use the same license key for Duende IdentityServer and the DCR Configuration API. 4. **Add and configure the client configuration store** The Configuration API uses the `IClientConfigurationStore` abstraction to persist new clients to the configuration store. Your Configuration API host needs an implementation of this interface. You can either use the Entity Framework Core-based implementation, or implement the interface yourself. See [the IClientConfigurationStore reference](/identityserver/reference/v8/stores/) for more details. If you wish to use the built-in implementation, install its NuGet package and add it to the ASP.NET Core service provider. Terminal ```bash dotnet add package Duende.IdentityServer.Configuration.EntityFramework ``` The `AddClientConfigurationStore()` extension method registers the built-in implementation of the `IClientConfigurationStore` interface with the service provider. Make sure to also configure the connection string to the [configuration store](/identityserver/data/providers/entityframework-core/#configuration-store-support): Program.cs ```csharp builder.Services.AddIdentityServerConfiguration(opt => opt.LicenseKey = "" ).AddClientConfigurationStore(); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddConfigurationDbContext(options => { options.ConfigureDbContext = builder => builder.UseSqlite(connectionString); }); ``` 5. **Map the Configuration API endpoints** Program.cs ```csharp app.MapDynamicClientRegistration() .RequireAuthorization("DCR"); ``` The `MapDynamicClientRegistration` extension method registers the DCR endpoints and returns an `IEndpointConventionBuilder` which you can use to define authorization requirements for your DCR endpoint. See [Authorization](#authorization) for more details about implementing authorization for the DCR endpoint. ### Shared Host For Configuration API and IdentityServer [Section titled “Shared Host For Configuration API and IdentityServer”](#shared-host-for-configuration-api-and-identityserver) The Configuration API can be hosted by your Duende IdentityServer host. You’ll need to add the Configuration API’s services to the service collection, and configure the store implementation. 1. **Add the `Duende.IdentityServer.Configuration` package** Terminal ```bash cd Configuration dotnet add package Duende.IdentityServer.Configuration ``` 2. **Configure services to include the Configuration API** Program.cs ```csharp builder.Services.AddIdentityServerConfiguration(opt => opt.LicenseKey = ""; ); ``` Note This feature is part of the [Duende IdentityServer Business, Enterprise, Standard, Advanced Edition](https://duendesoftware.com/products/identityserver). You don’t need to acquire an additional license, use the same license key for Duende IdentityServer and the DCR Configuration API. 3. **Add and configure the client configuration store** The Configuration API uses the `IClientConfigurationStore` abstraction to persist new clients to the configuration store. Your Configuration API host needs an implementation of this interface. You can either use the Entity Framework Core-based implementation, or implement the interface yourself. See [the IClientConfigurationStore reference](/identityserver/reference/v8/stores/) for more details. If you wish to use the built-in implementation, install its NuGet package and add it to the ASP.NET Core service provider. Terminal ```bash dotnet add package Duende.IdentityServer.Configuration.EntityFramework ``` The `AddClientConfigurationStore()` extension method registers the built-in implementation of the `IClientConfigurationStore` interface with the service provider. Make sure to also configure the connection string to the [configuration store](/identityserver/data/providers/entityframework-core/#configuration-store-support) if you haven’t already as part of your IdentityServer host: Program.cs ```csharp builder.Services.AddIdentityServerConfiguration(opt => opt.LicenseKey = "" ).AddClientConfigurationStore(); var connectionString = builder.Configuration.GetConnectionString("DefaultConnection"); builder.Services.AddConfigurationDbContext(options => { options.ConfigureDbContext = builder => builder.UseSqlite(connectionString); }); ``` 4. **Map the Configuration API endpoints** Program.cs ```csharp app.MapDynamicClientRegistration() .RequireAuthorization("DCR"); ``` The `MapDynamicClientRegistration` extension method registers the DCR endpoints and returns an `IEndpointConventionBuilder` which you can use to define authorization requirements for your DCR endpoint. See [Authorization](#authorization) for more details about implementing authorization for the DCR endpoint. ### Adding the Registration Endpoint to the Discovery Document [Section titled “Adding the Registration Endpoint to the Discovery Document”](#adding-the-registration-endpoint-to-the-discovery-document) By default, the Dynamic Client Registration (DCR) endpoint is not included in the [discovery document](/identityserver/reference/v8/endpoints/discovery/) of Duende IdentityServer. To include it, change the Discovery Document options when registering IdentityServer in the service collection: Program.cs ```csharp builder.Services.AddIdentityServer(options => { // Either use a static URL for the registration endpoint, when hosted outside of IdentityServer: options.Discovery.DynamicClientRegistration.RegistrationEndpointMode = RegistrationEndpointMode.Static; options.Discovery.DynamicClientRegistration.StaticRegistrationEndpoint = new Uri("https://my-configuration-api/connect/dcr"); // Or use inferred when the registration endpoint is hosted within IdentityServer: options.Discovery.DynamicClientRegistration.RegistrationEndpointMode = RegistrationEndpointMode.Inferred; }); ``` Cross-host Registration Endpoint When using `RegistrationEndpointMode.Static` with a registration endpoint on a different host than the authority, clients that consume the discovery document may reject the cross-host endpoint by default due to endpoint validation in their discovery policy. This applies to [Duende IdentityModel](/identitymodel/endpoints/discovery/#cross-host-endpoints), and may also apply to other client libraries that validate discovery document endpoints. You will need to configure the discovery policy on any downstream service that fetches the discovery document. Note DCR support was added to Duende IdentityServer v7.4. If you cannot upgrade your IdentityServer solution yet, you’ll have to add custom entries to the Discovery Document instead: Program.cs ```csharp using Duende.IdentityModel; builder.Services.AddIdentityServer(options => { options.Discovery.CustomEntries.Add OidcConstants.Discovery.RegistrationEndpoint, "https://my-configuration-api/connect/dcr"); }); ``` ## Authorization [Section titled “Authorization”](#authorization) When implementing Dynamic Client Registration (DCR), it is important to consider authentication and authorization for the Configuration API endpoint. While not strictly required, it is recommended that you implement some form of authentication and authorization for the DCR endpoint. You don’t want anyone with access to the DCR endpoint to be able to register clients! The specifications that define DCR allow both open registration, where authentication and authorization are absent and all client software can register with the authorization server, and protected registration, where an initial access token is required to register. The Configuration API creates standard ASP.NET endpoints that can be protected through traditional ASP.NET authorization. Alternatively, the Dynamic Client Registration `software_statement` parameter can be used to authenticate requests. ### Traditional ASP.NET Authorization [Section titled “Traditional ASP.NET Authorization”](#traditional-aspnet-authorization) You can authorize access to the Configuration API Endpoints using [authorization policies](https://learn.microsoft.com/en-us/aspnet/core/security/authorization/policies), just like any other endpoint created in an ASP.NET Web application. That authorization policy can use any criteria that an authorization policy might enforce, such as checking for particular claims or scopes. One possibility is to authenticate the provisioning system, that is, the system making the DCR call, using OAuth. The resulting access token could include a scope that grants access to the Configuration API. For example, you might protect the Configuration APIs with a JWT-bearer authentication scheme and an authorization policy that requires a particular scope to be present in the JWTs. You could choose any name for the scope that gives access to the Configuration APIs. Let’s use the name `IdentityServer.Configuration` for this example. You would then define the `IdentityServer.Configuration` scope as an [ApiScope](/identityserver/reference/v8/models/api-scope/) in your IdentityServer and allow the appropriate clients to access it. An automated process running in a CI pipeline could be configured as an OAuth client that uses the client credentials flow and is allowed to request the `IdentityServer.Configuration` scope. It could obtain a token using its client id and secret and then present that token when it calls the Configuration API. You might also have an interactive web application with a user interface that makes calls to the Configuration API. Again, you would define the application as an OAuth client allowed to request the appropriate scope, but this time, you’d use the authorization code flow. ### Software Statement [Section titled “Software Statement”](#software-statement) The metadata within requests to the Configuration API can be bundled together into a JWT and sent in the [`software_statement` parameter](https://datatracker.ietf.org/doc/html/rfc7591#section-2.3). If you can establish a trust relationship between the Configuration API and the issuer of the software statement, then that can be used to decide if you want to accept registration requests. To use a software statement in this way, you would need to design the specific semantics of your software statements. How you will issue them, how you will create the necessary trust relationship between the issuer and your Configuration API, and how the Configuration API will validate the software statements are all aspects to consider. The configuration API doesn’t make any assumptions about the software statement design. By default, it does nothing with the `software_statement` parameter. To make use of software statements, customize the `DynamicClientRegistrationValidator.ValidateSoftwareStatementAsync` extension point and add your validation logic. ## Calling The Registration Endpoint [Section titled “Calling The Registration Endpoint”](#calling-the-registration-endpoint) The registration endpoint is invoked by making an HTTP POST request to the `/connect/dcr` endpoint with a JSON payload containing metadata describing the desired client as described in [RFC 7591](https://datatracker.ietf.org/doc/rfc7591/) and [OpenID Connect Dynamic Client Registration 1.0](https://openid.net/specs/openid-connect-registration-1_0.html). The supported metadata properties are listed in the reference section on the [`DynamicClientRegistrationRequest` model](/identityserver/reference/v8/dcr/models/#dynamicclientregistrationrequest). A mixture of standardized and IdentityServer-specific properties are supported. Most standardized properties that are applicable to the client credentials or code flow grants are supported. Where IdentityServer’s configuration model includes important properties that are not standardized, we have included those properties as extensions. For example, there are no standardized properties describing token lifetimes, so the dynamic client registration endpoint adds `absolute_refresh_token_lifetime`, `access_token_lifetime`, `identity_token_lifetime`, etc. ## Customization [Section titled “Customization”](#customization) The behavior of the Configuration API can be customized through the use of several extension points that control the steps that occur when a dynamic client registration request arrives. First, the incoming request is validated to ensure that it is syntactically valid and semantically correct. The result of the validation process is a model which will either contain error details or a validated `Client` model. When validation succeeds, the validated request is passed on to the request processor. The request processor is responsible for generating properties of the `Client` that are not specified in the request. For example, the `client_id` is not normally specified in the request and is instead generated by the processor. When the processor is finished generating values, it passes the final client object to the store and returns an `IDynamicClientRegistrationResponse` indicating success or failure. This response object is finally used by the response generator to generate an HTTP response. Each of the validation and processing steps might also encounter an error. When that occurs, errors are conveyed using the `DynamicClientRegistrationError` class. ### Validation [Section titled “Validation”](#validation) To customize the validation process, you can implement the `IDynamicClientRegistrationValidator` interface, or extend the default implementation, `DynamicClientRegistrationValidator`. The default implementation includes many virtual methods, allowing you to use most of the base functionality and add your customization in a targeted manner. Each virtual method is responsible for validating a small number of parameters in the request and setting corresponding values on the client. A context object is passed to each virtual method. It contains the client object that is being built up, the original request, the claims principal that made the request, and a dictionary of additional items that can be used to pass state between customized steps. Each step should update the client in the context and return an `IStepResult` to indicate success or failure. For more details, see the [reference section on DCR validation](/identityserver/reference/v8/dcr/validation/). ### Processing [Section titled “Processing”](#processing) The request processor can be customized by implementing the `IDynamicClientRegistrationRequestProcessor` interface, or by extending the default `DynamicClientRegistrationRequestProcessor`. The default request processor contains virtual methods that allow you to override (part of) its functionality. For more details, see the [reference section on DCR request processing](/identityserver/reference/v8/dcr/processing/). ### Response Generation [Section titled “Response Generation”](#response-generation) To customize the HTTP responses of the Configuration API, you can implement the `IDynamicClientRegistrationResponseGenerator` interface, or extend the default `DynamicClientRegistrationResponseGenerator`. For more details, see the [reference section on DCR response generation](/identityserver/reference/v8/dcr/response/). ----- # Data Stores and Persistence > Overview of IdentityServer data stores types, including configuration and operational data, and their implementation options Duende IdentityServer is backed by two kinds of data: * [Configuration Data](/identityserver/data/configuration/): clients, resources, and identity providers. * [Operational Data](/identityserver/data/operational/): tokens, authorization codes, grants, and sessions. Data access is abstracted by store interfaces registered in the ASP.NET Core service provider. These interfaces allow IdentityServer to access the data it needs at runtime when processing requests. You can implement these interfaces yourself to use any database, or choose one of the built-in providers. Note Given that data stores abstract the details of the data stored, strictly speaking, IdentityServer does not know or understand where the data is actually being stored. As such, there is no built-in administrative tool to populate or manage this data. There are third-party options (both commercial and FOSS) that provide an administrative UI for managing the data when using the EntityFramework Core implementations. See [Admin UI](/identityserver/ui/admin/) for details. [Configuration Data](/identityserver/data/configuration/)Clients, resources, and identity providers: the static configuration that defines what your IdentityServer supports. [Operational Data](/identityserver/data/operational/)Tokens, authorization codes, grants, and sessions: the runtime data IdentityServer generates during authentication flows. [Storage Providers](/identityserver/data/providers/)Choose how and where your data is stored: Entity Framework Core, in-memory, or a custom implementation. ----- # Configuration Data > Documentation about configuration data models and stores in Duende IdentityServer, including client, resource, and identity provider stores Configuration data models the information for [Clients](/identityserver/fundamentals/clients/) and [Resources](/identityserver/fundamentals/resources). ## Stores [Section titled “Stores”](#stores) Store interfaces are designed to abstract accessing the configuration data. The stores used in Duende IdentityServer are: * [Client store](/identityserver/reference/v8/stores/client-store/) for `Client` data. * [CORS policy service](/identityserver/reference/v8/stores/cors-policy-service/) for [CORS support](/identityserver/tokens/cors/). Given that this is so closely tied to the `Client` configuration data, the CORS policy service is considered one of the configuration stores. * [Resource store](/identityserver/reference/v8/stores/resource-store/) for `IdentityResource`, `ApiResource`, and `ApiScope` data. * [Identity Provider store](/identityserver/reference/v8/stores/idp-store/) for `IdentityProvider` data. ## Registering Custom Stores [Section titled “Registering Custom Stores”](#registering-custom-stores) Custom implementations of the stores must be registered in the ASP.NET Core service provider. There are [convenience methods](/identityserver/reference/v8/di/#configuration-stores) for registering these. For example: Program.cs ```csharp builder.Services.AddIdentityServer() .AddClientStore() .AddCorsPolicyService() .AddResourceStore() .AddIdentityProviderStore(); ``` ## Caching Configuration Data [Section titled “Caching Configuration Data”](#caching-configuration-data) Configuration data is used frequently during request processing. If this data is loaded from a database or other external store, then it might be expensive to frequently re-load the same data. * v8.0+ Duende IdentityServer provides [convenience methods](/identityserver/reference/v8/di#caching-configuration-data) to enable caching data from the various stores. The caching implementation is built on Microsoft’s [`HybridCache`](https://learn.microsoft.com/en-us/aspnet/core/performance/caching/hybrid) from the `Microsoft.Extensions.Caching.Hybrid` package, registered as a [keyed service](https://learn.microsoft.com/en-us/dotnet/core/extensions/dependency-injection#keyed-services) under `ServiceProviderKeys.ConfigurationStoreCache`. For example: Program.cs ```csharp builder.Services.AddIdentityServer() .AddClientStore() .AddCorsPolicyService() .AddResourceStore() .AddInMemoryCaching() .AddClientStoreCache() .AddCorsPolicyCache() .AddResourceStoreCache() .AddIdentityProviderStoreCache(); ``` For Entity Framework users, there is a convenience method `AddConfigurationStoreCache()` that enables caching for all configuration stores at once: Program.cs ```csharp builder.Services.AddIdentityServer() .AddConfigurationStore(...) .AddConfigurationStoreCache(); ``` The duration of the data in the default cache is configurable on the [`IdentityServerOptions`](/identityserver/reference/v8/options#caching). For example: Program.cs ```csharp builder.Services.AddIdentityServer(options => { options.Caching.ClientStoreExpiration = TimeSpan.FromMinutes(5); options.Caching.ResourceStoreExpiration = TimeSpan.FromMinutes(5); }) .AddClientStore() .AddCorsPolicyService() .AddResourceStore() .AddInMemoryCaching() .AddClientStoreCache() .AddCorsPolicyCache() .AddResourceStoreCache(); ``` Further customization of the cache is possible: * The caching stores use a keyed `HybridCache` instance registered under `ServiceProviderKeys.ConfigurationStoreCache`. You can customize the `HybridCache` behavior by configuring the keyed service registration (e.g., adding a distributed cache backend via `IDistributedCache`). * By default, only the L1 (in-memory) cache tier is used. To enable L2 (distributed) caching, register an `IDistributedCache` implementation (e.g., Redis via `AddStackExchangeRedisCache`). `HybridCache` will automatically use it as the L2 tier. * v7.0 Duende IdentityServer provides [convenience methods](/identityserver/reference/v8/di/#caching-configuration-data) to enable caching data from the various stores. The caching implementation relies upon an `ICache` service and must also be added to the ASP.NET Core service provider. For example: Program.cs ```csharp builder.Services.AddIdentityServer() .AddClientStore() .AddCorsPolicyService() .AddResourceStore() .AddInMemoryCaching() .AddClientStoreCache() .AddCorsPolicyCache() .AddResourceStoreCache() .AddIdentityProviderStoreCache(); ``` The duration of the data in the default cache is configurable on the [`IdentityServerOptions`](/identityserver/reference/v8/options#caching). For example: Program.cs ```csharp builder.Services.AddIdentityServer(options => { options.Caching.ClientStoreExpiration = TimeSpan.FromMinutes(5); options.Caching.ResourceStoreExpiration = TimeSpan.FromMinutes(5); }) .AddClientStore() .AddCorsPolicyService() .AddResourceStore() .AddInMemoryCaching() .AddClientStoreCache() .AddCorsPolicyCache() .AddResourceStoreCache(); ``` Further customization of the cache is possible: * If you wish to customize the caching behavior for the specific configuration objects, you can replace the `ICache` service implementation in the dependency injection system. * The default implementation of the `ICache` itself relies upon the `IMemoryCache` interface (and `MemoryCache` implementation) provided by .NET. If you wish to customize the in-memory caching behavior, you can replace the `IMemoryCache` implementation in the dependency injection system. ## In-Memory Stores [Section titled “In-Memory Stores”](#in-memory-stores) IdentityServer includes in-memory store implementations for configuration data that are useful during development and testing. For full details, see [In-Memory Stores](/identityserver/data/providers/in-memory/). ----- # Operational Data > Documentation for managing dynamic operational data in IdentityServer including grants, keys, and server-side sessions For certain operations, IdentityServer needs a persistence store to keep dynamically created state. This data is collectively called *operational data*, and includes: * [Grants](#grants) for authorization and device codes, reference and refresh tokens, and remembered user consent * [Keys](#keys) managing dynamically created signing keys * [Server Side Sessions](#server-side-sessions) for storing authentication session data for interactive users server-side ## Grants [Section titled “Grants”](#grants) Many protocol flows produce state that represents a grant of one type or another. These include authorization and device codes, reference and refresh tokens, and remembered user consent. ### Stores [Section titled “Stores”](#stores) The persistence for grants is abstracted behind two interfaces: * The [persisted grant store](/identityserver/reference/v8/stores/persisted-grant-store/) is a common store for most grants. * The [device flow store](/identityserver/reference/v8/stores/device-flow-store/) is a specialized store for device grants. ### Registering Custom Stores [Section titled “Registering Custom Stores”](#registering-custom-stores) Custom implementations of `IPersistedGrantStore`, and/or `IDeviceFlowStore` must be registered in the ASP.NET Core service provider. For example: Program.cs ```csharp builder.Services.AddIdentityServer(); builder.Services.AddTransient(); builder.Services.AddTransient(); ``` ### Grant Expiration and Consumption [Section titled “Grant Expiration and Consumption”](#grant-expiration-and-consumption) The presence of the record in the store without a `ConsumedTime` and while still within the `Expiration` represents the validity of the grant. Setting either of these two values, or removing the record from the store effectively revokes the grant. Some grant types are one-time use only (either by definition or configuration). Once they are “used”, rather than deleting the record, the `ConsumedTime` value is set in the database marking them as having been used. This “soft delete” allows for custom implementations to either have flexibility in allowing a grant to be re-used (typically within a short window of time), or to be used in risk assessment and threat mitigation scenarios (where suspicious activity is detected) to revoke access. For refresh tokens, this sort of custom logic would be performed in the [IRefreshTokenService](/identityserver/reference/v8/services/refresh-token-service/). ### Grant Data [Section titled “Grant Data”](#grant-data) The `Data` property of the model contains the authoritative copy of the values in the store. This data is protected at rest using the ASP.NET Data Protection API. Except for `ConsumedTime`, the other properties of the model should be treated as read-only. ### Persisted Grant Service [Section titled “Persisted Grant Service”](#persisted-grant-service) Working with the grants store directly might be too low level. As such, a higher level service called the [IPersistedGrantService](/identityserver/reference/v8/services/persisted-grant-service/) is provided. It abstracts and aggregates the different grant types into one concept, and allows querying and revoking the persisted grants for a user. ## Keys [Section titled “Keys”](#keys) The [automatic key management](/identityserver/fundamentals/key-management/#automatic-key-management) feature in Duende IdentityServer requires a store to persist keys that are dynamically created. ### Signing Key Store [Section titled “Signing Key Store”](#signing-key-store) By default, the file system is used, but the storage of these keys is abstracted behind an extensible store interface. The [ISigningKeyStore](/identityserver/reference/v8/stores/signing-key-store/) is that storage interface. ### Registering a custom signing key store [Section titled “Registering a custom signing key store”](#registering-a-custom-signing-key-store) To register a custom signing key store in the ASP.NET Core service provider, there is a `AddSigningKeyStore` helper on the `IIdentityServerBuilder`. For example: Program.cs ```csharp builder.Services.AddIdentityServer() .AddSigningKeyStore(); ``` ### Key Lifecycle [Section titled “Key Lifecycle”](#key-lifecycle) When keys are required, `LoadKeysAsync` will be called to load them all from the store. They are then cached automatically for some amount of time based on [configuration](/identityserver/reference/v8/options/#key-management). Periodically a new key will be created, and `StoreKeyAsync` will be used to persist the new key. Once a key is past its retirement, `DeleteKeyAsync` will be used to purge the key from the store. ### Serialized Key [Section titled “Serialized Key”](#serialized-key) The [SerializedKey](/identityserver/reference/v8/stores/signing-key-store/#serializedkey) is the model that contains the key data to persist. It is expected that the `Id` is the unique identifier for the key in the store. The `Data` property is the main payload of the key and contains a copy of all the other values. Some of the properties affect how the `Data` is processed (e.g. `DataProtected`), and the other properties are considered read-only and thus can’t be changed to affect the behavior (e.g. changing the `Created` value will not affect the key lifetime, nor will changing `Algorithm` change which signing algorithm the key is used for). ## Server Side Sessions [Section titled “Server Side Sessions”](#server-side-sessions) Tip Added in Duende IdentityServer 6.1 The [server-side sessions](/identityserver/ui/server-side-sessions/) feature in Duende IdentityServer requires a store to persist a user’s session data. ### Server-Side Session Store [Section titled “Server-Side Session Store”](#server-side-session-store) The [IServerSideSessionStore](/identityserver/reference/v8/stores/server-side-sessions/) abstracts storing the server-side session data. [ServerSideSession](/identityserver/reference/v8/stores/server-side-sessions/#serversidesession) objects act as the storage entity, and provide several properties used as metadata for the session. The `Ticket` property contains the actual serialized data used by the ASP.NET Cookie Authentication handler. By default, this serialized data is stored in an encrypted state using ASP.NET Core Data Protection. The methods on the [IServerSideSessionStore](/identityserver/reference/v8/stores/server-side-sessions/) are used to orchestrate the various management functions needed by the [server-side sessions](/identityserver/ui/server-side-sessions/#session-management) feature. ### Registering a custom store [Section titled “Registering a custom store”](#registering-a-custom-store) To register a custom server-side session store in the ASP.NET Core service provider, there is a `AddServerSideSessionStore` helper on the `IIdentityServerBuilder`. It is still necessary to call `AddServerSideSessions` to enable the server-side session feature. For example: Program.cs ```csharp builder.Services.AddIdentityServer() .AddServerSideSessions() .AddServerSideSessionStore(); ``` There is also an overloaded version of a `AddServerSideSessions` that will perform both registration steps in one call. For example: Program.cs ```csharp builder.Services.AddIdentityServer() .AddServerSideSessions(); ``` ### EntityFramework Store Implementation [Section titled “EntityFramework Store Implementation”](#entityframework-store-implementation) An EntityFramework Core implementation of the server-side session store is included in the [Entity Framework Integration](/identityserver/data/providers/entityframework-core/#operational-store) operational store. When using the EntityFramework Core operational store, it will be necessary to indicate that server-side sessions need to be used with the call to the `AddServerSideSessions` fluent API. For example: Program.cs ```csharp builder.Services.AddIdentityServer() .AddServerSideSessions() .AddOperationalStore(options => { // ... }); ``` ----- # Storage Providers > Overview of the available storage provider options for Duende IdentityServer configuration and operational data. Duende IdentityServer abstracts data access behind store interfaces. You choose which provider backs those interfaces. [Entity Framework Core](/identityserver/data/providers/entityframework-core/)Use any EF Core-supported database (SQL Server, PostgreSQL, SQLite, and more) for durable, production-ready storage. [In-Memory](/identityserver/data/providers/in-memory/)Keep all data in application memory. Ideal for development, testing, and simple scenarios where durability is not required. [Custom](/identityserver/data/providers/custom/)Implement the store interfaces yourself to use any database, storage backend, or data access technology. ----- # Custom Store Implementation > Guide to implementing custom store interfaces in Duende IdentityServer to use any database or storage backend IdentityServer abstracts all data access behind store interfaces. You can implement any of these interfaces yourself to use any database, storage backend, or data access technology, rather than being limited to the built-in [Entity Framework Core](/identityserver/data/providers/entityframework-core/) or [in-memory](/identityserver/data/providers/in-memory/) providers. Register your custom store implementations in `Program.cs` using the standard ASP.NET Core DI methods or the IdentityServer builder extension methods. ## Configuration Stores [Section titled “Configuration Stores”](#configuration-stores) These interfaces back [configuration data](/identityserver/data/configuration/): the clients, resources, and identity providers that define what your IdentityServer instance supports. | Interface | Responsibility | | ---------------------------- | ------------------------------------------------------------------------------------------------------------------ | | `IClientStore` | Retrieve client configuration | | `IResourceStore` | Retrieve identity resources, API resources, and API scopes | | `IIdentityProviderStore` | Retrieve dynamic external identity providers | | `ICorsPolicyService` | Determine allowed CORS origins | | `IConnectedApplicationStore` | Read-only unified access to all registered applications across protocols (OIDC clients and SAML service providers) | | `ISamlServiceProviderStore` | Retrieve [SAML Service Provider](/identityserver/saml/service-providers/) configuration by entity ID | Register custom configuration stores with the IdentityServer builder: ```csharp builder.Services.AddIdentityServer() .AddClientStore() .AddResourceStore() .AddIdentityProviderStore(); ``` See the [stores reference](/identityserver/reference/v8/stores/) for the full interface contracts. ## Operational Stores [Section titled “Operational Stores”](#operational-stores) These interfaces back [operational data](/identityserver/data/operational/): the runtime state that IdentityServer generates and consumes during authentication flows. There are quite a few operational store interfaces in the `Duende.IdentityServer.Stores` namespace. The most commonly implemented ones are listed below, but explore the namespace (or the [stores reference](/identityserver/reference/v8/stores/)) for the full set. Note that several higher-level interfaces (`IAuthorizationCodeStore`, `IRefreshTokenStore`, `IReferenceTokenStore`, `IUserConsentStore`) are backed by `IPersistedGrantStore` by default. Replacing `IPersistedGrantStore` is usually sufficient, but you can also replace the higher-level interfaces individually if you need finer-grained control. | Interface | Responsibility | | ---------------------------------------- | ------------------------------------------------------------------------------------------ | | `IPersistedGrantStore` | Store and retrieve authorization codes, refresh tokens, user consent, and reference tokens | | `ISigningKeyStore` | Persist automatically managed signing keys | | `IServerSideSessionStore` | Store server-side user sessions | | `IDeviceFlowStore` | Store device authorization grant data | | `IBackChannelAuthenticationRequestStore` | Store CIBA authentication requests | | `IPushedAuthorizationRequestStore` | Store Pushed Authorization Requests (PAR) | Register custom operational stores with the IdentityServer builder: ```csharp builder.Services.AddIdentityServer() .AddPersistedGrantStore() .AddSigningKeyStore() .AddServerSideSessionStore(); ``` See the [stores reference](/identityserver/reference/v8/stores/) for the full interface contracts, and the [DI reference](/identityserver/reference/v8/di/) for all available builder extension methods. ----- # Entity Framework Core Integration > Documentation for using Entity Framework with IdentityServer to store configuration and operational data in any EF-supported database An EntityFramework-based implementation is provided for the configuration and operational data extensibility points in IdentityServer. The use of EntityFramework allows any EF-supported database to be used with this library. The features provided by this library are broken down into two main areas: configuration store and operational store support. These two different areas can be used independently or together, based upon the needs of the hosting application. To use this library, ensure that you have the NuGet package for the EntityFramework integration. It is called `Duende.IdentityServer.EntityFramework`. You can install it with: ```plaintext dotnet add package Duende.IdentityServer.EntityFramework ``` ## Configuration Store Support [Section titled “Configuration Store Support”](#configuration-store-support) For storing [configuration data](/identityserver/configuration/), the configuration store can be used. This support provides implementations of the `IClientStore`, `IResourceStore`, `IIdentityProviderStore`, `ISamlServiceProviderStore`, and the `ICorsPolicyService` extensibility points. These implementations use a `DbContext`-derived class called `ConfigurationDbContext` to model the tables in the database. To use the configuration store support, in Program.cs use the `AddConfigurationStore` extension method after the call to `AddIdentityServer`: Program.cs ```csharp const string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;database=YourIdentityServerDatabase;trusted_connection=yes;"; var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name; builder.Services.AddIdentityServer() // this adds the config data from DB (clients, resources, CORS) .AddConfigurationStore(options => { options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); }); ``` To configure the configuration store, use the `ConfigurationStoreOptions` options object passed to the configuration callback. ### ConfigurationStoreOptions [Section titled “ConfigurationStoreOptions”](#configurationstoreoptions) This options class contains properties to control the configuration store and `ConfigurationDbContext`. `ConfigureDbContext` Delegate of type `Action` used as a callback to configure the underlying `ConfigurationDbContext`. The delegate can configure the `ConfigurationDbContext` in the same way if EF were being used directly with `AddDbContext`, which allows any EF-supported database to be used. `DefaultSchema` Allows setting the default database schema name for all the tables in the `ConfigurationDbContext` ```csharp options.DefaultSchema = "myConfigurationSchema"; ``` If you need to change the schema for the Migration History Table, you can chain another action to the `UseSqlServer`: ```csharp options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly) .MigrationsHistoryTable("MyConfigurationMigrationTable", "myConfigurationSchema")); ``` ### Enabling Caching For Configuration Store [Section titled “Enabling Caching For Configuration Store”](#enabling-caching-for-configuration-store) To enable caching for the EF configuration store implementation, use the `AddConfigurationStoreCache` extension method: Program.cs ```csharp builder.Services.AddIdentityServer() .AddConfigurationStore(options => { // ... }) // this is something you will want in production to reduce load on and requests to the DB .AddConfigurationStoreCache(); ``` ## Operational Store [Section titled “Operational Store”](#operational-store) For storing [operational data](/identityserver/data/operational/) then the operational store can be used. This support provides implementations of the `IPersistedGrantStore`, `IDeviceFlowStore`, `IServerSideSessionStore`, and `ISigningKeyStore` extensibility points. The implementation uses a `DbContext`-derived class called `PersistedGrantDbContext` to model the table in the database. To use the operational store support, in Program.cs use the `AddOperationalStore` extension method after the call to `AddIdentityServer`: Program.cs ```csharp const string connectionString = @"Data Source=(LocalDb)\MSSQLLocalDB;database=YourIdentityServerDatabase;trusted_connection=yes;"; var migrationsAssembly = typeof(Program).GetTypeInfo().Assembly.GetName().Name; builder.Services.AddIdentityServer() // this adds the operational data from DB (codes, tokens, consents) .AddOperationalStore(options => { options.ConfigureDbContext = builder => builder.UseSqlServer(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); // this enables automatic token cleanup. this is optional. options.EnableTokenCleanup = true; options.TokenCleanupInterval = 3600; // interval in seconds (default is 3600) }); ``` To configure the operational store, use the `OperationalStoreOptions` options object passed to the configuration callback. ### OperationalStoreOptions [Section titled “OperationalStoreOptions”](#operationalstoreoptions) This options class contains properties to control the operational store and `PersistedGrantDbContext`. `ConfigureDbContext` Delegate of type `Action` used as a callback to configure the underlying `PersistedGrantDbContext`. The delegate can configure the `PersistedGrantDbContext` in the same way if EF were being used directly with `AddDbContext`, which allows any EF-supported database to be used. `DefaultSchema` Allows setting the default database schema name for all the tables in the `PersistedGrantDbContext`. `EnableTokenCleanup` Indicates whether expired grants and pushed authorization requests will be automatically cleaned up from the database. The default is `false`. `RemoveConsumedTokens` added >=5.1 Indicates whether consumed grants will be automatically cleaned up from the database. The default is `false`. `TokenCleanupInterval` The token cleanup interval (in seconds). The default is 3600 (1 hour). `ConsumedTokenCleanupDelay` added >=6.3 The consumed token cleanup delay (in seconds). The default is 0. This delay is the amount of time that must elapse before tokens marked as consumed can be deleted. Note that only refresh tokens with OneTime usage can be marked as consumed. `FuzzTokenCleanupStart` added >=7.0 The background token cleanup job runs at a configured interval. If multiple nodes run the cleanup job at the same time, update conflicts might occur in the store. To reduce the probability of that happening, the startup time can be fuzzed. When enabled, the first run is scheduled at a random time between the host startup and the configured TokenCleanupInterval. Subsequent runs are run on the configured TokenCleanupInterval. Defaults to `true`. Note The token cleanup feature does `not` remove persisted grants that are `consumed` (see [persisted grants](/identityserver/reference/v8/stores/persisted-grant-store/)). It only removes persisted grants that are beyond their `Expiration`. ## Database Creation And Schema Changes Across Different IdentityServer Versions [Section titled “Database Creation And Schema Changes Across Different IdentityServer Versions”](#database-creation-and-schema-changes-across-different-identityserver-versions) It is very likely that across different versions of IdentityServer (and the EF support) that the database schema will change to accommodate new and changing features. We do not provide any support for creating your database or migrating your data from one version to another. You are expected to manage the database creation, schema changes, and data migration in any way your organization sees fit. Using EF migrations is one possible approach to this. If you do wish to use migrations, then see the [EF quickstart](/identityserver/quickstarts/4-entity-framework/) for samples on how to get started, or consult the Microsoft [documentation on EF migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/index). We publish a [sample app](https://github.com/DuendeSoftware/products/tree/main/identity-server/migrations/IdentityServerDb) that we use internally for creating databases to test the latest database schema (this is SQL Server specific). ----- # In-Memory Stores > Documentation for using in-memory stores with IdentityServer for development, testing, and simple production scenarios In-memory stores keep all IdentityServer data in the application’s memory. They require no database setup, making them ideal for development, testing, and simple scenarios. They are not suitable for production deployments with multiple server instances, because data is not shared across instances and is lost when the application restarts. ## Configuration Data [Section titled “Configuration Data”](#configuration-data) The in-memory configuration APIs allow you to configure IdentityServer from in-memory lists of configuration objects. These collections can be hard-coded in the hosting application, or loaded dynamically from a configuration file or a database at startup. Use these APIs when prototyping, developing, or testing where it is not necessary to consult a database at runtime for configuration data. This style of configuration may also be appropriate for production scenarios where configuration rarely changes, or where restarting the application when configuration changes is acceptable. Register in-memory configuration stores using the builder extension methods in `Program.cs`: ```csharp builder.Services.AddIdentityServer() .AddInMemoryClients(Config.Clients) .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryApiResources(Config.ApiResources); ``` If you use [SAML](/identityserver/saml/), `AddInMemorySamlServiceProviders` registers [SAML Service Provider](/identityserver/saml/service-providers/) configuration the same way: ```csharp builder.Services.AddIdentityServer() .AddInMemorySamlServiceProviders(Config.SamlServiceProviders); ``` See [Configuration Data](/identityserver/data/configuration/) for the full details of what configuration data models and what each store is responsible for. ## Operational Data [Section titled “Operational Data”](#operational-data) For operational data (tokens, authorization codes, user consent, refresh tokens), IdentityServer includes `InMemoryPersistedGrantStore`. This implementation persists grants in memory and is intended for demos, tests, and other situations where durable storage is not required. `InMemoryPersistedGrantStore` is registered automatically when no other `IPersistedGrantStore` is configured. You can register it explicitly: ```csharp builder.Services.AddIdentityServer() .AddInMemoryPersistedGrants(); ``` If you use [Pushed Authorization Requests (PAR)](/identityserver/tokens/par/), `AddInMemoryPushedAuthorizationRequests` provides an in-memory `IPushedAuthorizationRequestStore`: ```csharp builder.Services.AddIdentityServer() .AddInMemoryPushedAuthorizationRequests(); ``` See the [Persisted Grant Store reference](/identityserver/reference/v8/stores/persisted-grant-store/) for the full `IPersistedGrantStore` interface documentation and the available implementations. See [Operational Data](/identityserver/data/operational/) for the full details of what operational data models and what each store is responsible for. ## Limitations [Section titled “Limitations”](#limitations) * **Not durable**: All data is lost when the application restarts. * **Not shared**: Each application instance has its own isolated copy of the data; unsuitable for multi-node or load-balanced deployments. * **Not for production operational data**: Tokens and grants in production should be stored durably. Use the [Entity Framework Core](/identityserver/data/providers/entityframework-core/) provider or a [custom implementation](/identityserver/data/providers/custom/) instead. ----- # IdentityServer Deployment > Comprehensive guide covering key aspects of deploying IdentityServer including proxy configuration, data protection, data stores, caching, and health monitoring. Because IdentityServer is made up of middleware and services that you use within an ASP.NET Core application, it can be hosted and deployed with the same diversity of technology as any other ASP.NET Core application. You have the choice about * where to host your IdentityServer (on-prem or in the cloud, and if in the cloud, which one?) * which web server to use (IIS, Kestrel, Nginx, Apache, etc.) * how you’ll scale and load-balance the deployment * what kind of deployment artifacts you’ll publish (files in a folder, containers, etc.) * how you’ll manage the environment (a managed app service in the cloud, a Kubernetes cluster, etc.) While this is a lot of decisions to make, this also means that your IdentityServer implementation can be built, deployed, hosted, and managed with the same technology that you’re using for any other ASP.NET applications that you have. Microsoft publishes extensive [advice and documentation](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/) about deploying ASP.NET Core applications, and it is applicable to IdentityServer implementations. We’re not attempting to replace that documentation - or the documentation for other tools that you might be using in your environment. Rather, this section of our documentation focuses on IdentityServer-specific deployment and hosting considerations. Note Our experience has been that these topics are very important. Some of our most common support requests are related to [Data Protection](/general/data-protection/#data-protection-keys) and [Load Balancing](#proxy-servers-and-load-balancers), so we strongly encourage you to review those pages, along with the rest of this chapter before deploying IdentityServer to production. ## Production Deployment Checklist [Section titled “Production Deployment Checklist”](#production-deployment-checklist) This checklist is a short summary of the detailed deployment guidance on this page. Before deploying IdentityServer to production, confirm that: * **HTTPS and proxy settings are correct.** Configure forwarded headers before IdentityServer and verify that the discovery document publishes the public HTTPS issuer. See [Proxy Servers and Load Balancers](#proxy-servers-and-load-balancers). * **Data Protection keys use durable, shared storage.** Protect the keys at rest and set an explicit application name. See [ASP.NET Core Data Protection](#aspnet-core-data-protection). * **Signing keys are protected and shared by every instance.** Define how keys will be rotated, use [Automatic Key Management](/identityserver/fundamentals/key-management/#automatic-key-management) when it is available for your edition, and choose a [shared key store](/identityserver/fundamentals/key-management/#key-storage) for load-balanced deployments. * **Configuration and operational data use production stores.** Do not rely on in-memory stores for state that must survive restarts or be shared between instances. See [IdentityServer Data Stores](#identityserver-data-stores). * **Database changes are part of the deployment process.** Apply schema changes before the new application version starts, and enable [operational-store cleanup](/identityserver/data/providers/entityframework-core/#operational-store) so expired grants and pushed authorization requests do not accumulate. * **Every instance can access the same shared state.** Configure shared operational data, signing keys, Data Protection keys, and any feature-specific [distributed caches](#distributed-caching). * **CORS allows only the required client origins.** Configure explicit origins for browser-based clients and check the middleware order when combining IdentityServer and ASP.NET Core policies. See [CORS](/identityserver/tokens/cors/). * **Token and session lifetimes match your threat model.** Review [access-token and refresh-token settings](/identityserver/reference/v8/models/client/#token) and keep [server-side session lifetimes](/identityserver/ui/server-side-sessions/session-expiration/) consistent with them. * **Diagnostics are ready before traffic arrives.** Configure appropriate [logging](/identityserver/diagnostics/logging/), collect [OpenTelemetry](/identityserver/diagnostics/otel/) signals, enable the [events](/identityserver/diagnostics/events/) you need, and expose [health checks](#health-checks). * **Traffic controls match the deployment risk.** Most deployments do not need application-level throttling, but public or multi-tenant deployments should assess [rate limiting](#rate-limiting). ## Proxy Servers and Load Balancers [Section titled “Proxy Servers and Load Balancers”](#proxy-servers-and-load-balancers) In typical deployments, your IdentityServer will be hosted behind a load balancer or reverse proxy. These and other network appliances often obscure information about the request before it reaches the host. Some of the behavior of IdentityServer and the ASP.NET authentication handlers depend on that information, most notably the scheme (HTTP vs HTTPS) of the request and the originating client IP address. Requests to your IdentityServer that come through a proxy will appear to come from that proxy instead of its true source on the Internet or corporate network. If the proxy performs TLS termination (that is, HTTPS requests are proxied over HTTP), the original HTTPS scheme will also no longer be present in the proxied request. Then, when the IdentityServer middleware and the ASP.NET authentication middleware process these requests, they will have incorrect values for the scheme and originating IP address. Common symptoms of this problem are * HTTPS requests get downgraded to HTTP * HTTP issuer is being published instead of HTTPS in `.well-known/openid-configuration` * Host names are incorrect in the discovery document or on redirect * Cookies are not sent with the secure attribute, which can especially cause problems with the samesite cookie attribute. In almost all cases, these problems can be solved by adding the ASP.NET `ForwardedHeaders` middleware to your pipeline. Most network infrastructure that proxies requests will set the [`X-Forwarded-For`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-For) and [`X-Forwarded-Proto`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/X-Forwarded-Proto) HTTP headers to describe the original request’s IP address and scheme. The `ForwardedHeaders` middleware reads the information in these headers on incoming requests and makes it available to the rest of the ASP.NET pipeline by updating the [`HttpContext.HttpRequest`](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/use-http-context?view=aspnetcore-7.0#httprequest). This transformation should be done early in the pipeline, certainly before the IdentityServer middleware and ASP.NET authentication middleware process requests, so that the presence of a proxy is abstracted away first. The appropriate configuration for the forwarded headers middleware depends on your environment. In general, you need to configure which headers it should respect, the IP address or IP address range of your proxy, and the number of proxies you expect (when there are multiple proxies, each one is captured in the `X-Forwarded-*` headers). There are two ways to configure this middleware: 1. Enable the environment variable `ASPNETCORE_FORWARDEDHEADERS_ENABLED`. This is the simplest option, but doesn’t give you as much control. It automatically adds the forwarded headers middleware to the pipeline, and configures it to accept forwarded headers from any single proxy, respecting the `X-Forwarded-For` and `X-Forwarded-Proto` headers. This is often the right choice for cloud hosted environments and Kubernetes clusters. 2. Configure the `ForwardedHeadersOptions` in DI, and use the `ForwardedHeaders` middleware explicitly in your pipeline. The advantage of configuring the middleware explicitly is that you can configure it in a way that is appropriate for your environment, if the defaults used by `ASPNETCORE_FORWARDEDHEADERS_ENABLED` are not what you need. Most notably, you can use the `KnownNetworks` or `KnownProxies` options to only accept headers sent by a known proxy, and you can set the `ForwardLimit` to allow for multiple proxies in front of your IdentityServer. This is often the right choice when you have more complex proxying going on, or if your proxy has a stable IP address. By default, `KnownNetworks` and `KnownProxies` support localhost with values of `127.0.0.1/8` and `::1` respectively. This is useful (and secure!) for local development environments and for solutions where the reverse proxy and the .NET web host runs on the same machine. In production environments when operating behind a proxy, you’ll need to configure the `ForwardedHeadersOptions`. Be sure to correctly set values for `KnownNetworks` and `KnownProxies` for your environments, as otherwise requests may be blocked. ```csharp builder.Services.Configure(options => { // you may need to change these ForwardedHeaders // values based on your network architecture options.ForwardedHeaders = ForwardedHeaders.XForwardedHost | ForwardedHeaders.XForwardedProto; // exact Addresses of known proxies to accept forwarded headers from. options.KnownProxies.Add(IPAddress.Parse("203.0.113.42")); // <-- change this value to the IP Address of the proxy // if the proxies could use any address from a block, that can be configured too: // var network = new IPNetwork(IPAddress.Parse("198.51.100.0"), 24); // options.KnownNetworks.Add(network); // default is 1 options.ForwardLimit = 1; }); ``` Please consult the [Microsoft documentation on configuring ASP.NET Core to work with proxy servers and load balancers](https://docs.microsoft.com/en-us/aspnet/core/host-and-deploy/proxy-load-balancer) for more details. ## ASP.NET Core Data Protection [Section titled “ASP.NET Core Data Protection”](#aspnet-core-data-protection) Duende IdentityServer makes extensive use of ASP.NET’s [data protection](https://docs.microsoft.com/en-us/aspnet/core/security/data-protection/) feature. It is crucial that you configure data protection correctly before you start using your IdentityServer in production. The recommended practices for setting up and using ASP.NET Core Data Protection for Duende IdentityServer are the same as for other server-side products, like BFF. See the [general ASP.NET Core Data Protection page](/general/data-protection). ### ASP.NET Data Protection Keys and IdentityServer Signing Keys [Section titled “ASP.NET Data Protection Keys and IdentityServer Signing Keys”](#aspnet-data-protection-keys-and-identityserver-signing-keys) ASP.NET’s data protection keys are sometimes confused with IdentityServer’s signing keys, but the two are completely separate keys with different purposes. IdentityServer implementations need both to function correctly. #### ASP.NET Data Protection Keys [Section titled “ASP.NET Data Protection Keys”](#aspnet-data-protection-keys) Data protection is a cryptographic library that is part of ASP.NET Core. Data protection uses private key cryptography to encrypt and sign sensitive data to ensure that it is only written and read by the application. The framework uses data protection to secure data that is commonly used by IdentityServer implementations, such as authentication cookies and anti-forgery tokens. In addition, IdentityServer itself uses data protection to protect sensitive data at rest, such as persisted grants, and sensitive data passed through the browser, such as the context objects passed to pages in the UI. The data protection keys are critical secrets for an IdentityServer implementation because they encrypt a great deal of sensitive data at rest and prevent sensitive data that is round-tripped through the browser from being tampered with. #### The IdentityServer Signing Key [Section titled “The IdentityServer Signing Key”](#the-identityserver-signing-key) Separately, IdentityServer needs cryptographic keys, called [signing keys](/identityserver/fundamentals/key-management/), to sign tokens such as JWT access tokens and id tokens. The signing keys use public key cryptography to allow client applications and APIs to validate token signatures using the public keys, which are published by IdentityServer through [discovery](/identityserver/reference/v8/endpoints/discovery/). The private key component of the signing keys are also critical secrets for IdentityServer because a valid signature provides integrity and non-repudiation guarantees that allow client applications and APIs to trust those tokens. ### IdentityServer Data Stores [Section titled “IdentityServer Data Stores”](#identityserver-data-stores) IdentityServer itself is stateless and does not require server affinity - but there is data that needs to be shared between in multi-instance deployments. ### Configuration Data [Section titled “Configuration Data”](#configuration-data) This typically includes: * resources * clients * startup configuration, e.g. key material, external provider settings etc… The way you store that data depends on your environment. In situations where configuration data rarely changes we recommend using the in-memory stores and code or configuration files. In highly dynamic environments (e.g. Saas) we recommend using a database or configuration service to load configuration dynamically. ### Operational Data [Section titled “Operational Data”](#operational-data) For certain operations, IdentityServer needs a persistence store to keep state, this includes: * issuing authorization codes * issuing reference and refresh tokens * storing consent * automatic management for signing keys You can either use a traditional database for storing operational data, or use a cache with persistence features like Redis. Duende IdentityServer includes storage implementations for above data using EntityFramework, and you can build your own. See the [data stores](/identityserver/data) section for more information. ### IdentityServer Features Using Data Protection [Section titled “IdentityServer Features Using Data Protection”](#identityserver-features-using-data-protection) Duende IdentityServer’s features that rely on data protection include: * protecting signing keys at rest (if [automatic key management](/identityserver/fundamentals/key-management/#automatic-key-management) is used and enabled) * protecting [persisted grants](/identityserver/data/operational/#persisted-grant-service) at rest (if enabled) * protecting [server-side session](/identityserver/ui/server-side-sessions/) data at rest (if enabled) * protecting [the state parameter](/identityserver/ui/login/external/#state-url-length-and-isecuredataformat) for external OIDC providers (if enabled) * protecting message payloads sent between pages in the UI (e.g. [logout context](/identityserver/ui/logout/logout-context/) and [error context](/identityserver/ui/error/)). * session management (because the ASP.NET Core cookie authentication handler requires it) ## Distributed Caching [Section titled “Distributed Caching”](#distributed-caching) Some optional features rely on ASP.NET Core distributed caching: * [State data formatter for OpenID Connect](/identityserver/ui/login/external/#state-url-length-and-isecuredataformat) * Replay cache (e.g. for [JWT client credentials](/identityserver/tokens/client-authentication/#setting-up-a-private-key-jwt-secret)) * [Device flow](/identityserver/reference/v8/stores/device-flow-store/) throttling service * Authorization parameter store In order to work in a multi-server environment, this needs to be set up correctly. Please consult the Microsoft [documentation](https://docs.microsoft.com/en-us/aspnet/core/performance/caching/distributed) for more details. ## Rate Limiting [Section titled “Rate Limiting”](#rate-limiting) Duende IdentityServer does not include built-in rate limiting, and most deployments do not need it. Excessive requests are usually caused by a client misconfiguration, such as a missing token cache or a retry loop, so fixing the client is the right first step. When you do need to throttle traffic, for example on public-facing or multi-tenant deployments or when you do not have control over client applications, see [Rate Limiting Duende IdentityServer Endpoints](/identityserver/deployment/rate-limiting/) for options at the network layer, in ASP.NET Core middleware, and in a custom token request validator. ## Health Checks [Section titled “Health Checks”](#health-checks) You can use ASP.NET’s [health checks](https://learn.microsoft.com/en-us/aspnet/core/host-and-deploy/health-checks) to monitor the health of your IdentityServer deployment. Health checks can contain arbitrary logic to test various conditions of a system. One common strategy for checking the health of IdentityServer is to make discovery requests. Successful discovery responses indicate not just that the IdentityServer host is running and able to receive requests and generate responses, but also that it was able to communicate with the configuration store. The following example code creates a health check that makes requests to the discovery endpoint. It finds the discovery endpoint’s handler by name, which requires IdentityServer `v6.3`. ```csharp public class DiscoveryHealthCheck : IHealthCheck { private readonly IEnumerable _endpoints; private readonly IHttpContextAccessor _httpContextAccessor; public DiscoveryHealthCheck(IEnumerable endpoints, IHttpContextAccessor httpContextAccessor) { _endpoints = endpoints; _httpContextAccessor = httpContextAccessor; } public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { try { var endpoint = _endpoints.FirstOrDefault(x => x.Name == IdentityServerConstants.EndpointNames.Discovery); if (endpoint != null) { var handler = _httpContextAccessor.HttpContext.RequestServices.GetRequiredService(endpoint.Handler) as IEndpointHandler; if (handler != null) { var result = await handler.ProcessAsync(_httpContextAccessor.HttpContext); if (result is DiscoveryDocumentResult) { return HealthCheckResult.Healthy(); } } } } catch { } return new HealthCheckResult(context.Registration.FailureStatus); } } ``` Another health check that you can perform is to request the public keys that IdentityServer uses to sign tokens - the JWKS (JSON Web Key Set). Doing so demonstrates that IdentityServer is able to communicate with the signing key store, a critical dependency. The following example code creates such a health check. Just as with the previous health check, it finds the endpoint’s handler by name, which requires IdentityServer `v6.3`. ```csharp public class DiscoveryKeysHealthCheck : IHealthCheck { private readonly IEnumerable _endpoints; private readonly IHttpContextAccessor _httpContextAccessor; public DiscoveryKeysHealthCheck(IEnumerable endpoints, IHttpContextAccessor httpContextAccessor) { _endpoints = endpoints; _httpContextAccessor = httpContextAccessor; } public async Task CheckHealthAsync(HealthCheckContext context, CancellationToken cancellationToken = default) { try { var endpoint = _endpoints.FirstOrDefault(x => x.Name == IdentityServerConstants.EndpointNames.Jwks); if (endpoint != null) { var handler = _httpContextAccessor.HttpContext.RequestServices.GetRequiredService(endpoint.Handler) as IEndpointHandler; if (handler != null) { var result = await handler.ProcessAsync(_httpContextAccessor.HttpContext); if (result is JsonWebKeysResult) { return HealthCheckResult.Healthy(); } } } } catch { } return new HealthCheckResult(context.Registration.FailureStatus); } } ``` ----- # FIPS 140-2 Compliance in IdentityServer > Explains Duende IdentityServer Federal Information Processing Standard (FIPS) compliance. The Federal Information Processing Standard (FIPS) Publication 140-2 is a U.S. government standard that defines minimum security requirements for cryptographic modules in information technology products. IdentityServer does not provide built-in FIPS enforcement or a configuration option to enable FIPS compliance. There is no toggle switch or configuration profile that will automatically make your solution FIPS-compliant. You are solely responsible for ensuring FIPS compliance in your application and infrastructure. This includes: * Configuring your operating system for FIPS mode * Selecting and using only FIPS-validated cryptographic algorithms * Properly managing and storing cryptographic key material * Validating that your complete solution meets FIPS requirements Duende IdentityServer does not contain its own cryptographic algorithm implementations. Instead, it relies on cryptographic primitives provided by: * The underlying .NET runtime * The operating system When IdentityServer signs tokens or protects cookies, it uses the cryptographic modules provided by these underlying platforms. However, IdentityServer does not restrict or enforce which algorithms or key sizes you use. This is your responsibility to configure correctly. To build a FIPS-compliant solution with Duende IdentityServer, here is some guidance: 1. **Configure your operating system and .NET Core codebase** for FIPS mode following the guidance in the [Microsoft documentation on .NET Core FIPS compliance](https://learn.microsoft.com/en-us/dotnet/standard/security/fips-compliance) 2. **Select only FIPS-validated algorithms** in your IdentityServer configuration: * **Do not use:** `RS256`, `RS384`, or `RS512` * **Use instead:** `PS*` or `ES*` token signing algorithms 3. **Use secure key storage** for private key material, such as: * Azure Key Vault Hardware Security Module (HSM) * Other FIPS 140-2 validated hardware security modules 4. **Configure ASP.NET Core Data Protection** appropriately: * Use FIPS-compliant algorithms for generating data protection keys * Store data protection keys securely in a FIPS-validated module Remember, it is your responsibility to validate that your complete solution meets FIPS compliance requirements for your specific use case and regulatory environment. ----- # Rate Limiting Duende IdentityServer Endpoints > When to rate limit Duende IdentityServer, and how to do it at the network layer, with ASP.NET Core middleware, or with a custom token request validator. Duende IdentityServer does not include built-in rate limiting. It’s an infrastructure concern, and the right approach depends on your architecture, threat model, and traffic, so IdentityServer leaves it to your [deployment infrastructure](/identityserver/deployment/). This page covers whether you need it and the three places you can apply it. ## Do You Need Rate Limiting? [Section titled “Do You Need Rate Limiting?”](#do-you-need-rate-limiting) For most deployments, you don’t. IdentityServer usually serves a known set of clients and users, and a flood of requests is normally a symptom of a misconfiguration: token lifetimes that are too short, missing token caching, or a retry loop in a client. Investigate the cause before you reach for rate limiting. Consider rate limiting when: * A **misbehaving client** cannot be fixed immediately, such as a third-party or legacy application * The authorize or token endpoints are **exposed to the public internet** * A **multi-tenant** deployment must stop one tenant’s traffic from affecting others * **Compliance requirements** mandate throttling on authentication endpoints ## Where To Apply Rate Limiting [Section titled “Where To Apply Rate Limiting”](#where-to-apply-rate-limiting) The three options below run from coarse to fine-grained, and you can combine them. ### Rate Limiting At The Network Layer [Section titled “Rate Limiting At The Network Layer”](#rate-limiting-at-the-network-layer) Throttle traffic before it reaches your application using a reverse proxy, load balancer, or API gateway such as nginx, Azure Application Gateway, AWS API Gateway, or Cloudflare. This needs no application changes and rejects requests before they consume any application resources. It can only partition by request properties like IP address or path, not by OAuth client or user identity, so it works best as a first line of defense. ### Rate Limiting With ASP.NET Core Middleware [Section titled “Rate Limiting With ASP.NET Core Middleware”](#rate-limiting-with-aspnet-core-middleware) Use the built-in [ASP.NET Core rate limiting middleware](https://learn.microsoft.com/aspnet/core/performance/rate-limit) to throttle requests in the HTTP pipeline. Register it before IdentityServer so traffic is throttled before IdentityServer does any work. Program.cs ```csharp builder.Services.AddRateLimiter(options => { options.GlobalLimiter = PartitionedRateLimiter.Create(context => { var partitionKey = context.Connection.RemoteIpAddress?.ToString() ?? "unknown"; return RateLimitPartition.GetFixedWindowLimiter(partitionKey, _ => new FixedWindowRateLimiterOptions { PermitLimit = 100, Window = TimeSpan.FromMinutes(1), }); }); options.RejectionStatusCode = StatusCodes.Status429TooManyRequests; }); // ... app.UseRateLimiter(); app.UseIdentityServer(); ``` Caution IdentityServer matches its protocol endpoints (such as `/connect/authorize` and `/connect/token`) with its own middleware, not ASP.NET Core endpoint routing. You cannot attach a named, per-endpoint rate limiting policy to a protocol endpoint; only the **global limiter** applies. To approximate per-endpoint limits, partition the global limiter on `context.Request.Path`. Named policies still work on your own routed pages, such as the login and consent Razor Pages. For the token endpoint, return a descriptive JSON error instead of a bare `429`, and add a `Retry-After` header where you can, so clients know when to try again. ### Rate Limiting With A Custom Token Request Validator [Section titled “Rate Limiting With A Custom Token Request Validator”](#rate-limiting-with-a-custom-token-request-validator) For identity-aware limits, implement [`ICustomTokenRequestValidator`](/identityserver/tokens/dynamic-validation/). It runs after the token request is validated, so you already know the authenticated client and user, and rejected requests come back as proper OAuth errors. ClientRateLimitTokenRequestValidator.cs ```csharp public class ClientRateLimitTokenRequestValidator : ICustomTokenRequestValidator { private static readonly PartitionedRateLimiter Limiter = PartitionedRateLimiter.Create(clientId => RateLimitPartition.GetFixedWindowLimiter(clientId, _ => new FixedWindowRateLimiterOptions { PermitLimit = 10, Window = TimeSpan.FromMinutes(1), })); public Task ValidateAsync( CustomTokenRequestValidationContext context, CancellationToken cancellationToken) { var clientId = context.Result.ValidatedRequest.ClientId; using var lease = Limiter.AttemptAcquire(clientId); if (!lease.IsAcquired) { context.Result.IsError = true; context.Result.Error = "rate_limit_exceeded"; context.Result.ErrorDescription = "Too many token requests for this client."; } return Task.CompletedTask; } } ``` Program.cs ```csharp idsvrBuilder.AddCustomTokenRequestValidator(); ``` By the time the validator runs, client authentication, secret validation, and database lookups have already happened, so you still pay for requests you end up rejecting. For high-volume abuse, pair it with a coarser layer above. ## How To Choose A Rate Limiting Approach? [Section titled “How To Choose A Rate Limiting Approach?”](#how-to-choose-a-rate-limiting-approach) Most deployments that need rate limiting only need the network layer. If you need more, stack them: the network appliance for volumetric protection, the ASP.NET Core global limiter to shield the pipeline, and a custom token request validator for per-client or per-user decisions. Either way, find out why a client is sending so many requests first. Rate limiting is a safety net, not a fix for a misconfigured client. ----- # Diagnostics > Overview of IdentityServer's diagnostic capabilities including logging, OpenTelemetry integration, and event system for monitoring and troubleshooting ## Logging [Section titled “Logging”](#logging) IdentityServer offers multiple diagnostics possibilities. The logs contains detailed information and are your best friend when troubleshooting. For security reasons the error messages returned to the UI/client are very brief - the logs always have all the details of what went wrong. [Read More](/identityserver/diagnostics/logging/) ## OpenTelemetry [Section titled “OpenTelemetry”](#opentelemetry) OpenTelemetry is a standard way of emitting diagnostics information from a process and IdentityServer supports Traces (.NET Activities), Metrics and Logs. [Read More](/identityserver/diagnostics/otel/) ## Events [Section titled “Events”](#events) The eventing system was created as an extension point to integrate with application monitoring systems (APM). They used to have their own different APIs so IdentityServer only provided events that could be used to call the APM’s APIs. Thanks to OpenTelemetry there is now a standardized way to emit diagnostic information from a process. The events may eventually be deprecated and removed. [Read More](/identityserver/diagnostics/events/) ## Conformance Report [Section titled “Conformance Report”](#conformance-report) IdentityServer can generate a conformance report that assesses your configuration against OAuth 2.1 and FAPI 2.0 specifications. [Read More](/identityserver/diagnostics/conformance-report/) ----- # Financial-Grade Security and Conformance Report > How to install, configure, and use the IdentityServer Financial-Grade Security and Conformance report to assess OAuth 2.1 and FAPI 2.0 compliance. Added in 8.0 Part of Financial-Grade Security and Conformance, the conformance report assesses your IdentityServer deployment against [OAuth 2.1](https://datatracker.ietf.org/doc/html/draft-ietf-oauth-v2-1) and [FAPI 2.0 Security Profile](https://openid.net/specs/fapi-2_0-security-profile.html) specifications, generating an HTML report accessible via a protected endpoint. ## Installation [Section titled “Installation”](#installation) Install the NuGet package: Terminal ```bash dotnet add package Duende.IdentityServer.ConformanceReport ``` ## Setup [Section titled “Setup”](#setup) ### 1. Register the Financial-Grade Security and Conformance Report [Section titled “1. Register the Financial-Grade Security and Conformance Report”](#1-register-the-financial-grade-security-and-conformance-report) Call `AddConformanceReport()` on the IdentityServer builder: Program.cs ```csharp builder.Services.AddIdentityServer() .AddConformanceReport(options => { options.Enabled = true; }); ``` ### 2. Map the Endpoint [Section titled “2. Map the Endpoint”](#2-map-the-endpoint) Add the Financial-Grade Security and Conformance report endpoint to your middleware pipeline: Program.cs ```csharp app.MapConformanceReport(); ``` ### 3. Access the Report [Section titled “3. Access the Report”](#3-access-the-report) Navigate to: `https://your-server/_duende/conformance-report` The endpoint requires an authenticated user by default (see [Authorization](#authorization) below). ## Configuration Options [Section titled “Configuration Options”](#configuration-options) `ConformanceReportOptions` controls the Financial-Grade Security and Conformance report feature: * **`Enabled`** Enable or disable the conformance report endpoint. Defaults to `false`. * **`EnableOAuth21Assessment`** Include OAuth 2.1 profile assessment in the report. Defaults to `true`. * **`EnableFapi2SecurityAssessment`** Include FAPI 2.0 Security Profile assessment in the report. Defaults to `true`. * **`PathPrefix`** URL path prefix for the conformance endpoint (no leading slash). Defaults to `"_duende"`. * **`ConfigureAuthorization`** Authorization policy for the HTML report endpoint. Defaults to require an authenticated user. * **`AuthorizationPolicyName`** ASP.NET Core authorization policy name used internally. Defaults to `"ConformanceReport"`. * **`HostCompanyName`** Optional company name shown in the report header. Defaults to `null`. * **`HostCompanyLogoUrl`** Optional company logo URL shown in the report header. Defaults to `null`. ## Authorization [Section titled “Authorization”](#authorization) By default, the report endpoint requires an authenticated user. Customize the policy using `ConfigureAuthorization`: Program.cs ```csharp builder.Services.AddIdentityServer() .AddConformanceReport(options => { options.Enabled = true; // Require a specific role options.ConfigureAuthorization = policy => policy.RequireRole("Admin"); // Or require multiple conditions // options.ConfigureAuthorization = policy => policy // .RequireRole("Admin") // .RequireClaim("department", "IT"); // Or allow anonymous (development/testing only) // options.ConfigureAuthorization = policy => // policy.RequireAssertion(_ => builder.Environment.IsDevelopment()); }); ``` Caution If you set `ConfigureAuthorization = null`, you must manually register an ASP.NET Core authorization policy with the name specified in `AuthorizationPolicyName` (default: `"ConformanceReport"`). Otherwise, the endpoint will fail at runtime with a “policy not found” error. ## Understanding the Report [Section titled “Understanding the Report”](#understanding-the-report) The HTML report displays: * **Server Configuration** — a matrix of server-level conformance rules and their status * **Client Configurations** — a matrix of per-client conformance rules and their status * **Rule Legend** — explanation of each rule identifier * **Notes** — detailed messages for warnings and failures ### Status Indicators [Section titled “Status Indicators”](#status-indicators) | Symbol | Meaning | | ------- | -------------------------------------------------------- | | Pass | Requirement is met | | Fail | Requirement is not met (configuration is non-conformant) | | Warning | Recommended practice is not followed | | N/A | Rule is not applicable to this configuration | ## Requirements [Section titled “Requirements”](#requirements) The conformance report uses `IClientStore.GetAllClientsAsync` to enumerate all clients for assessment. Custom `IClientStore` implementations must implement this method (added in v8.0). See the [upgrade guide](/identityserver/upgrades/v7_4-to-v8_0/#iclientstoregetallclientsasync-now-required) for details. ## Full Example [Section titled “Full Example”](#full-example) Program.cs ```csharp builder.Services.AddIdentityServer() .AddInMemoryClients(Config.Clients) .AddConformanceReport(options => { options.Enabled = true; options.EnableOAuth21Assessment = true; options.EnableFapi2SecurityAssessment = true; options.HostCompanyName = "Acme Corp"; options.ConfigureAuthorization = policy => policy.RequireRole("ComplianceTeam"); }); // ... app.MapConformanceReport(); app.UseIdentityServer(); ``` ----- # Diagnostics Data Added in 7.3 To make troubleshooting easier, newer versions of IdentityServer can collect important configuration and operational diagnostics data from your IdentityServer host. Diagnostics data is [written to logs periodically](/identityserver/reference/v8/options/#diagnostics), and can be used by your operations team to help analyze your IdentityServer configuration. Diagnostics information is never automatically shared with Duende. In support scenarios, you can choose to manually share this diagnostics data with [Duende priority support](/general/support-and-issues/#priority-support) to provide additional context. If needed, you can redact/remove entries before doing so. ## Diagnostics Data Contents [Section titled “Diagnostics Data Contents”](#diagnostics-data-contents) Diagnostics data contains information that is relevant to the configuration and behavior of your IdentityServer instance. The diagnostics data contains the following information: * Assembly information for [IdentityServer-related assemblies](https://github.com/DuendeSoftware/products/blob/main/identity-server/src/IdentityServer/Licensing/V2/Diagnostics/DiagnosticEntries/AssemblyInfoDiagnosticEntry.cs#L17) * .NET runtime version * IdentityServer version * Assembly name and version * Registered authentication schemes (does not include [dynamic providers](/identityserver/ui/login/dynamicproviders/)) * Name of the scheme and authentication handler type * Registered non-default implementations of Duende IdentityServer extension points * Extension point type, implementation type, assembly name and version * [`IdentityServerOptions`](/identityserver/reference/v8/options/) configuration * [Data Protection](/identityserver/deployment/#aspnet-core-data-protection) configuration * `ApplicationDiscriminator`, `XmlEncryptor` and `XmlRepository` * Basic server information * Host name * [License Usage Summary](/identityserver/reference/v8/models/license-usage-summary/) data * Token issue counts (for various token types) * Endpoint usage (only for IdentityServer endpoints) * Clients configuration (limited to first 100 clients, excluding sensitive information/secrets) * Resources configuration (limited to the first 100 resources) * Identity resources * API resources * API scopes Diagnostics data [is formatted as JSON](#diagnostics-data-format). ## Capturing Diagnostics Data [Section titled “Capturing Diagnostics Data”](#capturing-diagnostics-data) The IdentityServer diagnostics data is [written to logs periodically](/identityserver/reference/v8/options/#diagnostics). By default, you will see log entries similar to the following in your IdentityServer logs ```log info: Duende.IdentityServer.Diagnostics.Summary[7000] Diagnostic data (1 of 2): { ... info: Duende.IdentityServer.Diagnostics.Summary[7000] Diagnostic data (2 of 2): ... } ``` Diagnostics data [may be chunked](/identityserver/reference/v8/options/#diagnostics), and you will need to concatenate chunks to collect the full diagnostics JSON data. To capture diagnostics data from your IdentityServer instance, you can log entries written to the `Duende.IdentityServer.Diagnostics.Summary` log category. You may want to set up your IdentityServer logging to filter diagnostics data and emit these to a separate log provider/sink. Let’s look at some examples of how you can filter diagnostics data and write it to a separate log file. Note that to read the contents of this log file, you will need access to your IdentityServer host storage (or use another provider/sink to extract log data). ### .NET Core Default Logger [Section titled “.NET Core Default Logger”](#net-core-default-logger) To write log entries to a file using the default [.NET Core `ILogger` API](https://learn.microsoft.com/en-us/dotnet/core/extensions/logging), you will need a log provider that supports doing this. In the example below, we are using the [`NReco.Logging.File`](https://www.nuget.org/packages/NReco.Logging.File) package. * In code Use the `AddFile()` extension method to add a file logger to your `ILoggingBuilder` instance. The `FilterLogEntry` property on the file logger can be used to filter log entries based on the log category, which is what we are using to filter the `Duende.IdentityServer.Diagnostics.Summary` log category. Program.cs ```csharp // ... builder.Services.AddLogging(configure => { configure.AddFile("diagnostics.log", options => { options.Append = true; options.FilterLogEntry = entry => entry.LogName == "Duende.IdentityServer.Diagnostics.Summary"; }); }); ``` * With configuration pattern The file logger will need to be registered in your application. Use the `AddFile()` extension method to add a file logger to your `ILoggingBuilder` instance. Note the `NReco.Logging.File` requires a file name to be specified. Program.cs ```csharp // ... builder.Services.AddLogging(configure => { configure.AddFile("diagnostics.log", append: true); }); ``` In your `appsettings.json`, you can configure the file logger to filter log entries based on the log category. appsettings.json ```json { "Logging": { "LogLevel": { "Default": "Information", "Microsoft.AspNetCore": "Warning", "Duende.IdentityServer.Diagnostics.Summary": "None" }, "File": { "LogLevel": { "Default": "None", "Duende.IdentityServer.Diagnostics.Summary": "Information" } } } } ``` ### Serilog [Section titled “Serilog”](#serilog) When using [Serilog](https://serilog.net/), you can configure a separate file logger sink to write `Duende.IdentityServer.Diagnostics.Summary` log entries to. * In code In the `AddSerilog()` extension method’s configuration builder, you can add a file logger that filters log messages and only emits those from the `Duende.IdentityServer.Diagnostics.Summary` category. The console logger (or another default logger you are using) can be configured to exclude this category. Program.cs ```csharp // ... builder.Services.AddSerilog((services, configuration) => { configuration .ReadFrom.Configuration(builder.Configuration) .ReadFrom.Services(services) .Enrich.FromLogContext() .MinimumLevel.Debug() .MinimumLevel.Override("Microsoft", LogEventLevel.Warning) .MinimumLevel.Override("Microsoft.AspNetCore.Authentication", LogEventLevel.Information) .MinimumLevel.Override("Microsoft.Hosting.Lifetime", LogEventLevel.Information) .MinimumLevel.Override("System", LogEventLevel.Warning) .WriteTo.Logger(fileLogger => { fileLogger .WriteTo.File("./diagnostics/diagnostic.log", rollingInterval: RollingInterval.Day, fileSizeLimitBytes: 1024 * 1024 * 10, // 10 MB rollOnFileSizeLimit: true, outputTemplate: "[{Timestamp:HH:mm:ss} {Level} {EventId}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}") .Filter.ByIncludingOnly(Matching.FromSource("Duende.IdentityServer.Diagnostics.Summary")); }) .WriteTo.Logger(consoleLogger => { consoleLogger .WriteTo.Console( outputTemplate: "[{Timestamp:HH:mm:ss} {Level} {EventId}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}") .Filter.ByExcluding(Matching.FromSource("Duende.IdentityServer.Diagnostics.Summary")); }); ``` * With configuration pattern When using the configuration pattern, you can configure the file logger to filter log entries based on the log category. Note that you will need the [`Serilog.Expressions`](https://github.com/serilog/serilog-expressions) package installed and configured in your IdentityServer host In your `appsettings.json`, you can configure Serilog to filter log entries based on the log category. appsettings.json ```json { "Serilog":{ "Using":[ "Serilog.Sinks.Console", "Serilog.Sinks.File" ], "Enrich":[ "FromLogContext" ], "MinimumLevel":{ "Default":"Debug", "Override":{ "Microsoft":"Warning", "Microsoft.Hosting.Lifetime":"Information", "Microsoft.AspNetCore.Authentication":"Debug", "System":"Warning" } }, "WriteTo":[ { "Name":"Logger", "Args":{ "configureLogger":{ "WriteTo":[ { "Name":"File", "Args":{ "path":"diagnostics/identity-server-diagnostics.log", "outputTemplate":"[{Timestamp:HH:mm:ss} {Level} {EventId}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}", "rollingInterval":"Day", "fileSizeLimitBytes":10000000, "rollOnFileSizeLimit":true } } ], "Filter":[ { "Name":"ByIncludingOnly", "Args":{ "expression":"StartsWith(SourceContext, 'Duende.IdentityServer.Diagnostics.Summary')" } } ] } } }, { "Name":"Logger", "Args":{ "configureLogger":{ "WriteTo":[ { "Name":"Console", "Args":{ "outputTemplate":"[{Timestamp:HH:mm:ss} {Level} {EventId}] {SourceContext}{NewLine}{Message:lj}{NewLine}{Exception}{NewLine}" } } ], "Filter":[ { "Name":"ByExcluding", "Args":{ "expression":"SourceContext = 'Duende.IdentityServer.Diagnostics.Summary'" } } ] } } } ] } } ``` ### log4net [Section titled “log4net”](#log4net) When using [log4net](https://logging.apache.org/log4net/index.html), you can use the `log4net.config` configuration file to configure a file appender that writes `Duende.IdentityServer.Diagnostics.Summary` log entries to a separate file. log4net.config ```xml ``` ### NLog [Section titled “NLog”](#nlog) When using [NLog](https://nlog-project.org/) and the [`NLog.Extensions.Logging`](https://www.nuget.org/packages/NLog.Extensions.Logging) package, you can use the configuration pattern to configure a file logger that writes `Duende.IdentityServer.Diagnostics.Summary` log entries to a separate file. appsettings.json ```json { "NLog": { "ThrowConfigExceptions": true, "Targets": { "file": { "type": "File", "fileName": "${basedir}/diagnostics/${shortdate}.log", "layout": "${longdate} ${level:uppercase=true} ${logger} ${message} ${exception:format=ToString}" }, "console": { "type": "ColoredConsole", "layout": "${longdate} ${level:uppercase=true} ${logger} ${message} ${exception:format=ToString}" } }, "Rules": [ { "logger": "Duende.IdentityServer.Diagnostics.Summary", "writeTo": "file", "final": true, "maxLevel": "Info" }, { "logger": "*", "minLevel": "Info", "writeTo": "console" } ] } } ``` ## Diagnostics Data Format [Section titled “Diagnostics Data Format”](#diagnostics-data-format) Diagnostics data is written to logs in one or more chunks containing data formatted as JSON. Example diagnostics data JSON ```json { "AssemblyInfo":{ "DotnetVersion":".NET 9.0.6", "IdentityServerVersion":"7.3", "Assemblies":[ { "Name":"Duende.IdentityModel", "Version":"7.0.0.0" }, { "Name":"Duende.IdentityServer", "Version":"7.0.0.0" }, { "Name":"Duende.IdentityServer.Storage", "Version":"7.0.0.0" }, { "Name":"Microsoft.AspNetCore", "Version":"9.0.0.0" }, { "Name":"Microsoft.AspNetCore.Authentication.Abstractions", "Version":"9.0.0.0" }, { "Name":"Microsoft.AspNetCore.Authentication.Cookies", "Version":"9.0.0.0" }, { "Name":"Microsoft.AspNetCore.Authentication.Core", "Version":"9.0.0.0" }, { "Name":"Microsoft.AspNetCore.Authentication.Google", "Version":"9.0.3.0" }, { "Name":"Microsoft.AspNetCore.Authentication.OAuth", "Version":"9.0.0.0" }, { "Name":"Microsoft.AspNetCore.Authentication.OpenIdConnect", "Version":"9.0.3.0" }, { "Name":"Microsoft.IdentityModel.Abstractions", "Version":"8.0.1.0" }, { "Name":"Microsoft.IdentityModel.JsonWebTokens", "Version":"8.0.1.0" }, { "Name":"Microsoft.IdentityModel.Logging", "Version":"8.0.1.0" }, { "Name":"Microsoft.IdentityModel.Protocols", "Version":"8.0.1.0" }, { "Name":"Microsoft.IdentityModel.Protocols.OpenIdConnect", "Version":"8.0.1.0" }, { "Name":"Microsoft.IdentityModel.Tokens", "Version":"8.0.1.0" }, { "Name":"System.IdentityModel.Tokens.Jwt", "Version":"8.0.1.0" } ] }, "AuthSchemeInfo":{ "Schemes":[ { "idsrv":"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler" }, { "idsrv.external":"Microsoft.AspNetCore.Authentication.Cookies.CookieAuthenticationHandler" }, { "Google":"Google.Apis.Auth.AspNetCore3.GoogleOpenIdConnectHandler" } ] }, "RegisteredImplementations":{ "Root":[ ], "Hosting":[ ], "Infrastructure":[ ], "ResponseHandling":[ ], "Services":[ { "ICorsPolicyService":[ { "TypeName":"Duende.IdentityServer.Services.InMemoryCorsPolicyService", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] }, { "IProfileService":[ { "TypeName":"Duende.IdentityServer.Test.TestUserProfileService", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] }, { "IRefreshTokenService":[ { "TypeName":"Duende.IdentityServer.Services.ServerSideSessionRefreshTokenService", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] }, { "ISessionManagementService":[ { "TypeName":"Duende.IdentityServer.Services.DefaultSessionManagementService", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] } ], "Stores":[ { "IClientStore":[ { "TypeName":"Duende.IdentityServer.Stores.ValidatingClientStore\u00601[[Duende.IdentityServer.Stores.InMemoryClientStore, Duende.IdentityServer, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null]]", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] }, { "IDeviceFlowStore":[ { "TypeName":"Duende.IdentityServer.Stores.InMemoryDeviceFlowStore", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] }, { "IIdentityProviderStore":[ { "TypeName":"Duende.IdentityServer.Hosting.DynamicProviders.NonCachingIdentityProviderStore\u00601[[Duende.IdentityServer.Hosting.DynamicProviders.ValidatingIdentityProviderStore\u00601[[Duende.IdentityServer.Hosting.DynamicProviders.InMemoryIdentityProviderStore, Duende.IdentityServer, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null]], Duende.IdentityServer, Version=7.0.0.0, Culture=neutral, PublicKeyToken=null]]", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] }, { "IResourceStore":[ { "TypeName":"Duende.IdentityServer.Stores.InMemoryResourcesStore", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" }, { "TypeName":"Duende.IdentityServer.Stores.InMemoryResourcesStore", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" }, { "TypeName":"Duende.IdentityServer.Stores.InMemoryResourcesStore", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] }, { "IServerSideSessionsMarker":[ { "TypeName":"Duende.IdentityServer.Stores.NopIServerSideSessionsMarker", "Assembly":"Duende.IdentityServer.Storage", "AssemblyVersion":"7.0.0.0" } ] }, { "IServerSideSessionStore":[ { "TypeName":"Duende.IdentityServer.Stores.InMemoryServerSideSessionStore", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] }, { "IServerSideTicketStore":[ { "TypeName":"Duende.IdentityServer.Stores.ServerSideTicketStore", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] } ], "Validation":[ { "IBackchannelAuthenticationUserValidator":[ { "TypeName":"Microsoft.Extensions.DependencyInjection.TestBackchannelLoginUserValidator", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] }, { "IResourceOwnerPasswordValidator":[ { "TypeName":"Duende.IdentityServer.Test.TestUserResourceOwnerPasswordValidator", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] }, { "ISecretParser":[ { "TypeName":"Duende.IdentityServer.Validation.JwtBearerClientAssertionSecretParser", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] }, { "ISecretValidator":[ { "TypeName":"Duende.IdentityServer.Validation.PrivateKeyJwtSecretValidator", "Assembly":"Duende.IdentityServer", "AssemblyVersion":"7.0.0.0" } ] } ] }, "IdentityServerOptions":{ "IssuerUri":null, "LowerCaseIssuerUri":true, "AccessTokenJwtType":"at\u002Bjwt", "LogoutTokenJwtType":"logout\u002Bjwt", "EmitStaticAudienceClaim":true, "EmitScopesAsSpaceDelimitedStringInJwt":false, "EmitIssuerIdentificationResponseParameter":true, "EmitStateHash":false, "StrictJarValidation":false, "ValidateTenantOnAuthorization":false, "Endpoints":{ "EnableAuthorizeEndpoint":true, "EnableJwtRequestUri":false, "EnableTokenEndpoint":true, "EnableUserInfoEndpoint":true, "EnableDiscoveryEndpoint":true, "EnableEndSessionEndpoint":true, "EnableCheckSessionEndpoint":true, "EnableTokenRevocationEndpoint":true, "EnableIntrospectionEndpoint":true, "EnableDeviceAuthorizationEndpoint":true, "EnableBackchannelAuthenticationEndpoint":true, "EnablePushedAuthorizationEndpoint":true }, "Discovery":{ "ShowEndpoints":true, "ShowKeySet":true, "ShowIdentityScopes":true, "ShowApiScopes":true, "ShowClaims":true, "ShowResponseTypes":true, "ShowResponseModes":true, "ShowGrantTypes":true, "ShowExtensionGrantTypes":true, "ShowTokenEndpointAuthenticationMethods":true, "ExpandRelativePathsInCustomEntries":true, "ResponseCacheInterval":null, "EnableDiscoveryDocumentCache":false, "DiscoveryDocumentCacheDuration":"00:01:00", "CustomEntries":{ } }, "Authentication":{ "CookieAuthenticationScheme":null, "CookieLifetime":"10:00:00", "CookieSlidingExpiration":false, "CookieSameSiteMode":0, "RequireAuthenticatedUserForSignOutMessage":false, "CheckSessionCookieName":"idsrv.session", "CheckSessionCookieDomain":null, "CheckSessionCookieSameSiteMode":0, "RequireCspFrameSrcForSignout":true, "CoordinateClientLifetimesWithUserSession":false }, "Events":{ "RaiseSuccessEvents":true, "RaiseFailureEvents":true, "RaiseInformationEvents":true, "RaiseErrorEvents":true }, "InputLengthRestrictions":{ "ClientId":100, "ClientSecret":100, "Scope":300, "RedirectUri":400, "Nonce":300, "UiLocale":100, "LoginHint":100, "AcrValues":300, "GrantType":100, "UserName":100, "Password":100, "CspReport":2000, "IdentityProvider":100, "ExternalError":100, "AuthorizationCode":100, "DeviceCode":100, "RefreshToken":100, "TokenHandle":100, "Jwt":51200, "CodeChallengeMinLength":43, "CodeChallengeMaxLength":128, "CodeVerifierMinLength":43, "CodeVerifierMaxLength":128, "ResourceIndicatorMaxLength":512, "BindingMessage":100, "UserCode":100, "IdTokenHint":4000, "LoginHintToken":4000, "AuthenticationRequestId":100, "DPoPKeyThumbprint":100, "DPoPProofToken":4000 }, "UserInteraction":{ "LoginUrl":"/Account/Login", "LoginReturnUrlParameter":"ReturnUrl", "LogoutUrl":"/Account/Logout", "LogoutIdParameter":"logoutId", "ConsentUrl":"/consent", "ConsentReturnUrlParameter":"returnUrl", "CreateAccountUrl":null, "CreateAccountReturnUrlParameter":"returnUrl", "ErrorUrl":"/home/error", "ErrorIdParameter":"errorId", "CustomRedirectReturnUrlParameter":"returnUrl", "CookieMessageThreshold":2, "DeviceVerificationUrl":"/device", "DeviceVerificationUserCodeParameter":"userCode", "AllowOriginInReturnUrl":false, "PromptValuesSupported":[ "none", "login", "consent", "select_account" ] }, "Caching":{ "ClientStoreExpiration":"00:15:00", "ResourceStoreExpiration":"00:15:00", "CorsExpiration":"00:15:00", "IdentityProviderCacheDuration":"01:00:00", "CacheLockTimeout":"00:01:00" }, "Cors":{ "CorsPolicyName":"Duende.IdentityServer", "PreflightCacheDuration":null, "CorsPaths":[ { "Value":"/.well-known/openid-configuration", "HasValue":true }, { "Value":"/.well-known/openid-configuration/jwks", "HasValue":true }, { "Value":"/connect/token", "HasValue":true }, { "Value":"/connect/userinfo", "HasValue":true }, { "Value":"/connect/revocation", "HasValue":true } ] }, "Csp":{ "Level":1, "AddDeprecatedHeader":true }, "Validation":{ "InvalidRedirectUriPrefixes":[ "javascript:", "file:", "data:", "mailto:", "ftp:", "blob:", "about:", "ssh:", "tel:", "view-source:", "ws:", "wss:" ] }, "DeviceFlow":{ "DefaultUserCodeType":"Numeric", "Interval":5 }, "Ciba":{ "DefaultLifetime":300, "DefaultPollingInterval":5 }, "Logging":{ "BackchannelAuthenticationRequestSensitiveValuesFilter":[ "client_secret", "client_assertion", "id_token_hint", "request" ], "TokenRequestSensitiveValuesFilter":[ "client_secret", "password", "client_assertion", "refresh_token", "device_code", "code", "subject_token" ], "AuthorizeRequestSensitiveValuesFilter":[ "id_token_hint", "request" ], "PushedAuthorizationSensitiveValuesFilter":[ "client_secret", "client_assertion", "request" ] }, "MutualTls":{ "Enabled":false, "ClientCertificateAuthenticationScheme":"Certificate", "DomainName":null, "AlwaysEmitConfirmationClaim":false }, "KeyManagement":{ "Enabled":true, "RsaKeySize":2048, "SigningAlgorithms":[ { "Name":"RS256", "UseX509Certificate":false } ], "InitializationDuration":"00:05:00", "InitializationSynchronizationDelay":"00:00:05", "InitializationKeyCacheDuration":"00:01:00", "KeyCacheDuration":"1.00:00:00", "PropagationTime":"14.00:00:00", "RotationInterval":"90.00:00:00", "RetentionDuration":"14.00:00:00", "DeleteRetiredKeys":true, "DataProtectKeys":true, "KeyPath":"/Users/maartenba/Projects/AcmeCorp/AcmeCorp.IdentityServer/keys" }, "PersistentGrants":{ "DataProtectData":true, "DeleteOneTimeOnlyRefreshTokensOnUse":true }, "DPoP":{ "ProofTokenValidityDuration":"00:01:00", "ServerClockSkew":"00:00:00", "SupportedDPoPSigningAlgorithms":[ "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512" ] }, "DynamicProviders":{ "PathPrefix":{ "Value":"/federation", "HasValue":true }, "SignInScheme":"idsrv.external", "SignOutScheme":"idsrv", "SignOutSchemeSetExplicitly":false }, "ServerSideSessions":{ "UserDisplayNameClaimType":"name", "RemoveExpiredSessions":true, "ExpiredSessionsTriggerBackchannelLogout":true, "RemoveExpiredSessionsFrequency":"00:10:00", "FuzzExpiredSessionRemovalStart":true, "RemoveExpiredSessionsBatchSize":100 }, "PushedAuthorization":{ "Required":false, "Lifetime":600, "AllowUnregisteredPushedRedirectUris":true }, "JwtValidationClockSkew":"00:05:00", "SupportedRequestObjectSigningAlgorithms":[ "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "HS256", "HS384", "HS512" ], "SupportedClientAssertionSigningAlgorithms":[ "RS256", "RS384", "RS512", "PS256", "PS384", "PS512", "ES256", "ES384", "ES512", "HS256", "HS384", "HS512" ], "StrictClientAssertionAudienceValidation":false, "Diagnostics":{ "LogFrequency":"00:10:00", "ChunkSize":8160 } }, "DataProtectionConfiguration":{ "ApplicationDiscriminator":"/Users/maartenba/Projects/AcmeCorp/AcmeCorp.IdentityServer/", "XmlEncryptor":"Not Configured", "XmlRepository":"Not Configured" }, "TokenIssueCounts":{ "Jwt":1, "Reference":0, "JwtDPoP":0, "ReferenceDPoP":0, "JwtMTLS":0, "ReferenceMTLS":0, "Refresh":0, "Id":1, "implicit":0, "hybrid":0, "authorization_code":1, "client_credentials":0, "password":0, "urn:ietf:params:oauth:grant-type:device_code":0, "Other":0 }, "LicenseUsageSummary":{ "ClientsUsedCount":1, "IssuersUsed":[ "https://localhost:5443" ], "FeaturesUsed":[ "Automatic Key Management", "PAR (RFC 9126)", "Server-side Sessions" ], "EntitledSkus":[] }, "BasicServerInfo":{ "HostName":"M4-MAARTEN" }, "EndpointUsage":{ "/connect/authorize/callback":2, "/connect/authorize":1, "/connect/ciba":0, "/connect/checksession":0, "/connect/deviceauthorization":0, "/.well-known/openid-configuration/jwks":3, "/.well-known/openid-configuration":3, "/connect/endsession/callback":1, "/connect/endsession":1, "/connect/introspect":0, "/connect/par":1, "/connect/revocation":0, "/connect/token":1, "/connect/userinfo":1, "other":0 }, "Clients":[ { "ClientId":"interactive", "SecretTypes":[ "SharedSecret" ], "RequireConsent":true, "AllowedGrantTypes":[ "authorization_code" ], "RedirectUris":[ "https://localhost:5444/signin-oidc" ], "PostLogoutRedirectUris":[ "https://localhost:5444/" ], "FrontChannelLogoutUri":"https://localhost:5444/signout-oidc", "AllowOfflineAccess":true, "AllowedScopes":[ "openid", "profile", "email", "weatherapi.read" ], "CoordinateLifetimeWithUserSession":true, "InitiateLoginUri":"https://localhost:5444/signin-idp" } ], "Resources":{ "IdentityResource":[ "email", "openid", "profile" ], "ApiResource":[ { "Name":"weatherapi", "ResourceIndicatorRequired":false, "SecretTypes":[ "SharedSecret" ] } ], "ApiScope":[ "weatherapi.read" ] } } ``` Diagnostics data format The structure and format of the diagnostics data output should not be considered stable, and may change in future IdentityServer versions. ----- # IdentityServer Events And Audit Logging > How to configure IdentityServer events and send structured authentication, token, consent, and security events to an audit store. [Logs](/identityserver/diagnostics/logging/) describe low-level application activity. Events describe higher-level operations in IdentityServer, such as user login, client authentication, token issuance, consent, and token revocation. Events are structured data with event IDs, success or failure information, categories, and details, which makes them easier to query and process than application logs. You can send events to structured logging stores such as [ELK](https://www.elastic.co/webinars/introduction-elk-stack), [Seq](https://getseq.net), or [Splunk](https://www.splunk.com/). ## How To Configure IdentityServer Events [Section titled “How To Configure IdentityServer Events”](#how-to-configure-identityserver-events) Events are not enabled by default. Choose which event types to raise when you call `AddIdentityServer`: Program.cs ```csharp builder.Services.AddIdentityServer(options => { options.Events.RaiseSuccessEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseErrorEvents = true; options.Events.RaiseInformationEvents = true; }); ``` IdentityServer raises protocol events itself. Events for user-interface actions, such as a successful or failed login, must be raised by your UI code because that code belongs to your application. Inject `IEventService` and call `RaiseAsync`: LoginController.cs ```csharp public async Task Login(LoginInputModel model) { if (_users.ValidateCredentials(model.Username, model.Password)) { // issue authentication cookie with subject ID and username var user = _users.FindByUsername(model.Username); await _events.RaiseAsync(new UserLoginSuccessEvent(user.Username, user.SubjectId, user.Username), HttpContext.RequestAborted); } else { await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials"), HttpContext.RequestAborted); } } ``` ## When To Use Events For Audit Logging [Section titled “When To Use Events For Audit Logging”](#when-to-use-events-for-audit-logging) Use logs to diagnose application behavior. Use events when you need a smaller, structured record of security-relevant operations. An audit trail commonly records: * Successful and failed user logins * Client and API authentication * Token issuance, revocation, and introspection * Consent grants and denials * Custom events for application-specific security decisions Enabling events does not create a complete audit system by itself. You must decide where to store raised events, how long to retain them, and who can access them. For an audit trail, send events to a dedicated append-only or tamper-resistant store rather than relying only on general application logs. Built-in events can include usernames, subject IDs, display names, client IDs, scopes, redirect URIs, and local and remote IP addresses. Treat this data as personal or security-sensitive information when you set access and retention policies for the audit store. Issued token values are obfuscated, so full tokens are not written to events. Custom events and sinks should not add secrets, full tokens, or personal data that the audit trail does not need. ## How To Store Events In An Audit System [Section titled “How To Store Events In An Audit System”](#how-to-store-events-in-an-audit-system) The default event sink serializes each event to JSON and forwards it to the ASP.NET Core logging system. To send events to an audit database, SIEM, or another store, implement [`IEventSink`](/identityserver/reference/v8/services/event-sink/) and register it with the ASP.NET Core service provider. The following example uses [Seq](https://getseq.net) to emit events: SeqEventSink.cs ```csharp public class SeqEventSink : IEventSink { private readonly Logger _log; public SeqEventSink() { _log = new LoggerConfiguration() .WriteTo.Seq("http://localhost:5341") .CreateLogger(); } public Task PersistAsync(Event evt, CancellationToken cancellationToken) { if (evt.EventType == EventTypes.Success || evt.EventType == EventTypes.Information) { _log.Information("{Name} ({Id}), Details: {@details}", evt.Name, evt.Id, evt); } else { _log.Error("{Name} ({Id}), Details: {@details}", evt.Name, evt.Id, evt); } return Task.CompletedTask; } } ``` Add the `Serilog.Sinks.Seq` package to your host, then register the sink: ```shell dotnet add package Serilog.Sinks.Seq ``` Program.cs ```csharp builder.Services.AddTransient(); ``` `IEventService` sends each event to one `IEventSink`. Registering a custom sink replaces the default sink, so events are no longer forwarded to the standard ASP.NET Core logger unless your custom sink does that too. Your sink controls the reliability and retention of the audit trail. In production, account for temporary failures in the destination, restrict write and delete access, and monitor the sink so dropped events do not go unnoticed. If you need events in both the default logger and a dedicated audit store, implement that fan-out in your sink. ## Built-In IdentityServer Events [Section titled “Built-In IdentityServer Events”](#built-in-identityserver-events) The following events are defined in IdentityServer: * **`ApiAuthenticationFailureEvent`** & **`ApiAuthenticationSuccessEvent`** Gets raised for successful/failed API authentication at the introspection endpoint. * **`ClientAuthenticationSuccessEvent`** & **`ClientAuthenticationFailureEvent`** Gets raised for successful/failed client authentication at the token endpoint. * **`TokenIssuedSuccessEvent`** & **`TokenIssuedFailureEvent`** Gets raised for successful/failed attempts to request identity tokens, access tokens, refresh tokens and authorization codes. * **`TokenIntrospectionSuccessEvent`** & **`TokenIntrospectionFailureEvent`** Gets raised for successful token introspection requests. * **`TokenRevokedSuccessEvent`** Gets raised for successful token revocation requests. * **`UserLoginSuccessEvent`** & **`UserLoginFailureEvent`** Gets raised by the quickstart UI for successful/failed user logins. * **`UserLogoutSuccessEvent`** Gets raised for successful logout requests. * **`ConsentGrantedEvent`** & **`ConsentDeniedEvent`** Gets raised in the consent UI. * **`UnhandledExceptionEvent`** Gets raised for unhandled exceptions. * **`DeviceAuthorizationFailureEvent`** & **`DeviceAuthorizationSuccessEvent`** Gets raised for successful/failed device authorization requests. ### SAML Events v8.0 [Section titled “SAML Events ”v8.0](#saml-events) The following events are raised by SAML components: * **`SamlSsoSuccessEvent`** Raised when a SAML single sign-on request completes successfully. * **`SamlSsoFailureEvent`** Raised when a SAML single sign-on request fails. * **`SamlSloSuccessEvent`** Raised when a SAML single logout request completes successfully. * **`SamlSloFailureEvent`** Raised when a SAML single logout request fails. * **`SamlAuthnRequestValidationFailureEvent`** Raised when validation of an incoming SAML authentication request fails. * **`SamlLogoutRequestValidationFailureEvent`** Raised when validation of an incoming SAML logout request fails. * **`InvalidSamlServiceProviderConfigurationEvent`** Raised when a SAML Service Provider’s configuration fails runtime validation (performed by [`ISamlServiceProviderConfigurationValidator`](/identityserver/saml/extensibility#isamlserviceproviderconfigurationvalidator)). Includes the SP’s `EntityId` and `DisplayName`. ## How To Create Custom IdentityServer Events [Section titled “How To Create Custom IdentityServer Events”](#how-to-create-custom-identityserver-events) You can create your own events and emit them through the same event pipeline. Derive from the `Event` base class, which adds contextual information such as the activity ID and timestamp. Choose an event ID that does not conflict with the [built-in events](#built-in-identityserver-events), then add the fields your application needs: AccountLockedEvent.cs ```csharp public class AccountLockedEvent : Event { private const int AccountLockedEventId = 9000; public AccountLockedEvent(string subjectId) : base(EventCategories.Authentication, "Account Locked", EventTypes.Information, AccountLockedEventId) { SubjectId = subjectId; } public string SubjectId { get; set; } } ``` ----- # Logging > Documentation for logging configuration and usage in Duende IdentityServer, including log levels and Serilog setup Duende IdentityServer uses the standard logging facilities provided by ASP.NET Core. You don’t need to do any extra configuration to benefit from rich logging functionality. For log level definitions, environment guidance, and actionable next steps for each level, see the [Logging Fundamentals](/general/logging) guide. [Logging Fundamentals](/general/logging)Log level definitions, environment configuration table, and the log level anxiety spectrum. ## Configuration [Section titled “Configuration”](#configuration) Logs are written under the `Duende.IdentityServer` category. To enable detailed logging for IdentityServer, set that namespace in your `appsettings.json`: appsettings.json ```json { "Logging": { "LogLevel": { "Default": "Information", "Duende.IdentityServer": "Debug" } } } ``` Note In production, logging can produce significant volume. It is recommended to default to `Warning` level and drop to `Information` or `Debug` only when actively investigating an issue. For high-level production instrumentation, see the [Events](/identityserver/diagnostics/events) system. ### Filtering Exceptions [Section titled “Filtering Exceptions”](#filtering-exceptions) The `LoggingOptions` class allows you to filter out exceptions that could lead to log bloat. For example, `OperationCanceledException` is extremely common in web applications (clients frequently abort HTTP requests) and is excluded by default. ```csharp /// /// Called when the IdentityServer middleware detects an unhandled exception, and is used to determine if the exception is logged. /// Returns true to emit the log, false to suppress. /// public Func UnhandledExceptionLoggingFilter = (context, exception) => { var result = !(context.RequestAborted.IsCancellationRequested && exception is OperationCanceledException); return result; }; ``` To apply custom filtering, set the `UnhandledExceptionLoggingFilter` property on `LoggingOptions`: ```csharp var isBuilder = builder.Services.AddIdentityServer(options => { options.Logging.UnhandledExceptionLoggingFilter = (ctx, ex) => { if (ctx.User is { Identity.Name: "Jeff" }) { // Oh Jeff... return false; } if (ex.Message.Contains("Oops")) { // ignore this exception return false; } // this is a real exception return true; }; }) .AddTestUsers(TestUsers.Users) .AddLicenseSummary(); ``` Returning `true` emits the log; returning `false` suppresses it. ## Advanced Topics [Section titled “Advanced Topics”](#advanced-topics) ### OpenTelemetry [Section titled “OpenTelemetry”](#opentelemetry) Logs written to the standard `ILogger` system in .NET 8+ can be exported to OpenTelemetry traces at runtime. This helps visualize when a log statement occurred in relation to the entire request. Logs are augmented with trace IDs and correlated with traces. See [Logs in OpenTelemetry](/identityserver/diagnostics/otel#logs) for setup details. ----- # OpenTelemetry > Documentation for OpenTelemetry integration in IdentityServer, covering metrics, traces and logs collection for monitoring and diagnostics Tip Added in Duende IdentityServer v6.1 and expanded in v7.0 [OpenTelemetry](https://opentelemetry.io) (OTel) is a collection of tools, APIs, and SDKs for generating and collecting telemetry data (metrics, logs, and traces). This is very useful for analyzing software performance and behavior, especially in highly distributed systems. ## OpenTelemetry Signals [Section titled “OpenTelemetry Signals”](#opentelemetry-signals) OpenTelemetry signals are the information collected and processed to describe the internal activity of the system. The most common signals are traces, metrics, and logs. .NET 8+ comes with first class support for OpenTelemetry. IdentityServer emits traces, metrics, and logs you can collect. ### Metrics [Section titled “Metrics”](#metrics) Metrics are high level statistic counters. They provide an aggregated overview and can be used to set monitoring rules. ### Traces [Section titled “Traces”](#traces) Traces shows individual requests and dependencies. The output is very useful for visualizing the control flow and finding performance bottlenecks. This is an example of distributed traces from a web application calling an API (displayed using our [Aspire sample](/identityserver/samples/diagnostics/)). The web application uses a refresh token to call IdentityServer to get a new access token and then calls the API. The API reads the discovery endpoint, finds the jwks url and then gets the keys from jwks endpoint. ![.NET Aspire dashboard showing Duende IdentityServer traces](/_astro/aspire_traces.C5IYKs1g_8l8HE.webp) ### Logs [Section titled “Logs”](#logs) OpenTelemetry in .NET 8+ can export logs written to the standard `ILogger` system. The logs are augmented with trace ids and correlated with traces. This is an example of a structured log message from a web application calling an API (also displayed using our [Aspire sample](/identityserver/samples/diagnostics/)). ![.NET Aspire dashboard showing Duende IdentityServer Structured Logs](/_astro/aspire_structured_logs.C4_GEVBr_Z2uoiap.webp) Here is an example of that same log message appearing in the trace. Aspire displays the log entry details as dots on the trace timeline. ![.NET Aspire dashboard showing Duende IdentityServer a trace with a log entry](/_astro/aspire_structured_logs_in_trace.DDvSbnq__27frWP.webp) ## Setup [Section titled “Setup”](#setup) To start emitting OpenTelemetry tracing and metrics information you need to: * add the OpenTelemetry libraries to your IdentityServer and client applications * start collecting traces and metrics from the various IdentityServer sources (and other sources e.g. ASP.NET Core) * add the OpenTelemetry configuration to your service setup For development a simple option is to export the tracing information to the console and use the Prometheus exporter to create a human-readable `/metrics` endpoint for the metrics. ```bash dotnet add package OpenTelemetry dotnet add package OpenTelemetry.Extensions.Hosting dotnet add package OpenTelemetry.Instrumentation.AspNetCore dotnet add package OpenTelemetry.Exporter.OpenTelemetryProtocol ``` Program.cs ```csharp using OpenTelemetry.Resources; // Add OpenTelemetry logging infrastructure // to correlate logs with traces builder.Logging.AddOpenTelemetry(); // Enable OpenTelemetry var openTelemetry = builder.Services.AddOpenTelemetry(); openTelemetry.ConfigureResource(r => r .AddService(builder.Environment.ApplicationName)); openTelemetry.WithMetrics(m => m .AddMeter(Telemetry.ServiceName) .AddMeter(Pages.Telemetry.ServiceName) .AddPrometheusExporter()); openTelemetry.WithTracing(t => t .AddSource(IdentityServerConstants.Tracing.Basic) .AddSource(IdentityServerConstants.Tracing.Cache) .AddSource(IdentityServerConstants.Tracing.Services) .AddSource(IdentityServerConstants.Tracing.Stores) .AddSource(IdentityServerConstants.Tracing.Validation) .AddAspNetCoreInstrumentation() .AddConsoleExporter()); ``` Add the Prometheus exporter to the pipeline Program.cs ```csharp // Map /metrics that displays OpenTelemetry data in human-readable form. app.UseOpenTelemetryPrometheusScrapingEndpoint(); ``` This setup will write the tracing information to the console and provide metrics on the /metrics endpoint. ## Metrics [Section titled “Metrics”](#metrics-1) Tip Added in Duende IdentityServer v7.0 OpenTelemetry metrics are run-time measurements that are intended to provide an indication of overall health and are typically used to show graphs on a dashboard or to set up monitoring rules. When that monitoring reveals issues, traces and logs are used to investigate further. OpenTelemetry monitoring tools often provide features to find the traces and logs corresponding to certain metrics. IdentityServer emits metrics from the IdentityServer middleware and services. Our quick start for the UI also [contains metrics](#metrics-in-the-ui) that can be used as a starting point for monitoring UI events. The metric counters that IdentityServer emits are designed to not contain any sensitive information. They are often tagged to indicate the source of the events. ### High level Metrics [Section titled “High level Metrics”](#high-level-metrics) These metrics are instrumented by the IdentityServer middleware and services and are intended to describe the overall usage and health of the system. They could provide the starting point for building a metrics dashboard. The high level metrics are created by the meter named “Duende.IdentityServer”, which is the value of the `Duende.IdentityServer.Telemetry.ServiceName` constant. #### Telemetry.Metrics.Counters.Operation [Section titled “Telemetry.Metrics.Counters.Operation”](#telemetrymetricscountersoperation) Counter name: `tokenservice.operation` Aggregated counter of failed and successful operations. The result tag indicates if an operation succeeded, failed, or caused an internal error. It is expected to have some failures during normal operations. In contrast, operations tagged with a result of internal\_error are abnormal and indicate an unhandled exception. The error/success ratio can be used as a very high level health metric. | Tag | Description | | ------ | ---------------------------------------------------- | | error | Error label on errors | | result | Success, error or internal\_error | | client | Id of client requesting the operation. May be empty. | #### Telemetry.Metrics.Counters.ActiveRequests [Section titled “Telemetry.Metrics.Counters.ActiveRequests”](#telemetrymetricscountersactiverequests) Counter name: `active_requests` Gauge/up-down counter that shows current active requests that are processed by any IdentityServer endpoint. Note that the pages in the user interface are not IdentityServer endpoints and are not included in this count. | Tag | Description | | -------- | ---------------------------------------- | | endpoint | The type name for the endpoint processor | | path | The path of the request | ### Detailed Metrics [Section titled “Detailed Metrics”](#detailed-metrics) These detailed metrics are instrumented by the IdentityServer middleware and services and track usage of specific flows and features. Note In IdentityServer versions <7.3, these metrics are created by the meter named “Duende.IdentityServer.Experimental”, starting with IdentityServer 7.3, they are created by the meter named “Duende.IdentityServer”. #### Telemetry.Metrics.Counters.ApiSecretValidation [Section titled “Telemetry.Metrics.Counters.ApiSecretValidation”](#telemetrymetricscountersapisecretvalidation) Counter name: `tokenservice.api.secret_validation` Number of successful/failed validations of API Secrets. | Tag | Description | | ------------ | -------------------------- | | api | The Api Id | | auth\_method | Authentication method used | | error | Error label on errors | #### Telemetry.Metrics.Counters.BackchannelAuthentication [Section titled “Telemetry.Metrics.Counters.BackchannelAuthentication”](#telemetrymetricscountersbackchannelauthentication) Counter name: `tokenservice.backchannel_authentication` Number of successful/failed back channel authentications (CIBA). | Tag | Description | | ------ | --------------------- | | client | The client Id | | error | Error label on errors | #### Telemetry.Metrics.Counters.ClientConfigValidation [Section titled “Telemetry.Metrics.Counters.ClientConfigValidation”](#telemetrymetricscountersclientconfigvalidation) Counter name: `tokenservice.client.config_validation` Number of successful/failed client validations. | Tag | Description | | ------ | --------------------- | | client | The client Id | | error | Error label on errors | #### Telemetry.Metrics.Counters.ClientSecretValidation [Section titled “Telemetry.Metrics.Counters.ClientSecretValidation”](#telemetrymetricscountersclientsecretvalidation) Counter name: `tokenservice.client.secret_validation` Number of successful/failed client secret validations. | Tag | Description | | ------------ | ------------------------------------ | | client | The client Id | | auth\_method | The authentication method on success | | error | Error label on errors | #### Telemetry.Metrics.Counters.DeviceAuthentication [Section titled “Telemetry.Metrics.Counters.DeviceAuthentication”](#telemetrymetricscountersdeviceauthentication) Counter name: `tokenservice.device_authentication` Number of successful/failed device authentications. | Tag | Description | | ------ | --------------------- | | client | The client Id | | error | Error label on errors | #### Telemetry.Metrics.Counters.DynamicIdentityProviderValidation [Section titled “Telemetry.Metrics.Counters.DynamicIdentityProviderValidation”](#telemetrymetricscountersdynamicidentityprovidervalidation) Counter name: `tokenservice.dynamic_identityprovider.validation` Number of successful/failed validations of dynamic identity providers. | Tag | Description | | ------ | ------------------------------- | | scheme | The scheme name of the provider | | error | Error label on errors | #### Telemetry.Metrics.Counters.Introspection [Section titled “Telemetry.Metrics.Counters.Introspection”](#telemetrymetricscountersintrospection) Counter name: `tokenservice.introspection` Number of successful/failed token introspections. | Tag | Description | | ------ | -------------------------------------------------- | | caller | The caller of the endpoint, a client id or api id. | | active | Was the token active? Only sent on success | | error | Error label on errors | #### Telemetry.Metrics.Counters.PushedAuthorizationRequest [Section titled “Telemetry.Metrics.Counters.PushedAuthorizationRequest”](#telemetrymetricscounterspushedauthorizationrequest) Counter name: `tokenservice.pushed_authorization_request` Number of successful/failed pushed authorization requests. | Tag | Description | | ------ | --------------------- | | client | The client Id | | error | Error label on errors | #### Telemetry.Metrics.Counters.ResourceOwnerAuthentication [Section titled “Telemetry.Metrics.Counters.ResourceOwnerAuthentication”](#telemetrymetricscountersresourceownerauthentication) Counter name: `tokenservice.resourceowner_authentication` Number of successful/failed resource owner authentications. | Tag | Description | | ------ | --------------------- | | client | The client Id | | error | Error label on errors | #### Telemetry.Metrics.Counters.Revocation [Section titled “Telemetry.Metrics.Counters.Revocation”](#telemetrymetricscountersrevocation) Counter name: `tokenservice.revocation` Number of successful/failed token revocations. | Tag | Description | | ------ | --------------------- | | client | The client Id | | error | Error label on errors | #### Telemetry.Metrics.Counters.TokenIssued [Section titled “Telemetry.Metrics.Counters.TokenIssued”](#telemetrymetricscounterstokenissued) Counter name: `tokenservice.token_issued` Number of successful/failed token issuance attempts. Note that a token issuance might include multiple actual tokens (id\_token, access token, refresh token). | Tag | Description | | ------------------------ | ---------------------------------------------------------------- | | client | The client Id | | grant\_type | The grant type used | | authorize\_request\_type | The authorize request type, if information about it is available | | error | Error label on errors | #### Telemetry.Metrics.Counters.SamlSso v8.0 [Section titled “Telemetry.Metrics.Counters.SamlSso ”v8.0](#telemetrymetricscounterssamlsso) Counter name: `tokenservice.saml.sso` Number of SAML SSO attempts, both successful and failed. On success, the counter is tagged with the service provider entity ID and the SAML binding used. On failure, the binding tag is replaced with an error code so you can quickly see what went wrong without flooding your metrics system with high-cardinality data. On success: | Tag | Description | | -------------- | ------------------------------------------------------------------------------------- | | sp\_entity\_id | The entity ID of the service provider | | binding | The SAML binding used (for example, `urn:oasis:names:tc:SAML:2.0:bindings:HTTP-POST`) | On failure: | Tag | Description | | -------------- | ------------------------------------- | | sp\_entity\_id | The entity ID of the service provider | | error | Bounded error code (see below) | The `error` tag uses a bounded set of values to prevent cardinality explosion in your metrics backend: `invalid`, `unknown`, `sp_not_found`, `sp_disabled`, `invalid_acs_url`, `access_denied`, `interaction_error` #### Telemetry.Metrics.Counters.SamlSlo [Section titled “Telemetry.Metrics.Counters.SamlSlo”](#telemetrymetricscounterssamlslo) Counter name: `tokenservice.saml.slo` Number of SAML Single Logout (SLO) attempts, both successful and failed. Like the SSO counter, error codes are kept to a bounded set to keep your metrics cardinality under control. On success: | Tag | Description | | -------------- | ------------------------------------- | | sp\_entity\_id | The entity ID of the service provider | On failure: | Tag | Description | | -------------- | ------------------------------------- | | sp\_entity\_id | The entity ID of the service provider | | error | Bounded error code (see below) | The `error` tag uses a bounded set of values to prevent cardinality explosion in your metrics backend: `invalid`, `unknown`, `sp_not_found`, `sp_disabled`, `invalid_acs_url`, `access_denied`, `partial_logout`, `interaction_error` ### Metrics In The UI [Section titled “Metrics In The UI”](#metrics-in-the-ui) The [UI in your IdentityServer host](/identityserver/ui/) can instrument these events to measure activities that occur during interactive flows, such as user login and logout. These events are not instrumented by the IdentityServer middleware or services because they are the responsibility of the UI. Our templated UI does instrument these events, and you can alter and add metrics as needed to the UI in your context. #### Telemetry.Metrics.Counters.Consent [Section titled “Telemetry.Metrics.Counters.Consent”](#telemetrymetricscountersconsent) Counter name: `tokenservice.consent` Consent requests granted or denied. The counters are per scope, so if a user consents to multiple scopes, the counter is increased multiple times, one for each scope. This allows the scope name to be included as a tag without causing an explosion of combination of tags. | Tag | Description | | ------- | ----------------- | | client | The client Id | | scope | The scope names | | consent | granted or denied | #### Telemetry.Metrics.Counters.GrantsRevoked [Section titled “Telemetry.Metrics.Counters.GrantsRevoked”](#telemetrymetricscountersgrantsrevoked) Counter name: `tokenservice.grants_revoked` Revocation of grants. | Tag | Description | | ------ | --------------------------------------------------------------------------------------------------------- | | client | The client Id, if grants are revoked only for one client. If not set, the revocation was for all clients. | #### Telemetry.Metrics.Counters.UserLogin [Section titled “Telemetry.Metrics.Counters.UserLogin”](#telemetrymetricscountersuserlogin) Counter names: `tokenservice.user_login` Successful and failed user logins. | Tag | Description | | ------ | ----------------------------------------------------------------- | | client | The client Id, if the login was caused by a request from a client | | idp | The idp (ASP.NET Core Scheme name) used to log in | | error | Error label on errors | #### Telemetry.Metrics.Counters.UserLogout [Section titled “Telemetry.Metrics.Counters.UserLogout”](#telemetrymetricscountersuserlogout) Counter name: `user_logout` User logout. Note that this is only raised on explicit user logout, not if the session times out. The number of logouts will typically be lower than the number of logins. | Tag | Description | | --- | ---------------------------------------------- | | idp | The idp (ASP.NET scheme name) logging out from | ### .NET Authentication And Authorization Metrics [Section titled “.NET Authentication And Authorization Metrics”](#net-authentication-and-authorization-metrics) Tip Added in .NET 10 Starting with .NET 10, metrics are available for certain authentication and authorization events in ASP.NET Core. You can get metrics for the following events: * [Authentication](https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/built-in?view=aspnetcore-10.0#microsoftaspnetcoreauthentication) * Authenticated request duration (`aspnetcore.authentication.authenticate.duration`) * Challenge count (`aspnetcore.authentication.challenges`) * Forbid count (`aspnetcore.authentication.forbids`) * Sign in count (`aspnetcore.authentication.sign_ins`) * Sign out count (`aspnetcore.authentication.sign_outs`) * [Authorization](https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/built-in?view=aspnetcore-10.0#microsoftaspnetcoreauthorization) * Count of requests requiring authorization (`aspnetcore.authorization.attempts`) Refer to the [ASP.NET Core documentation](https://learn.microsoft.com/en-us/aspnet/core/log-mon/metrics/built-in?view=aspnetcore-10.0) for more information about ASP.NET Core built-in metrics. ### ASP.NET Core Identity metrics [Section titled “ASP.NET Core Identity metrics”](#aspnet-core-identity-metrics) Tip Added in .NET 10 When using ASP.NET Identity, metrics are available for key user and sign-in operation metrics. These let you monitor user management activities like creating users, changing passwords, etc. It’s also possible to track login attempts, sign-ins, sign-outs, and two-factor authentication usage. The `Microsoft.AspNetCore.Identity` meter provides the following metrics: * User management metrics * Duration of user creation operations (`aspnetcore.identity.user.create.duration`) * Duration of user update operations (`aspnetcore.identity.user.update.duration`) * Duration of user deletion operations (`aspnetcore.identity.user.delete.duration`) * Number of password verification attempts (`aspnetcore.identity.user.check_password_attempts`) * Number of tokens generated for users, such as password reset tokens (`aspnetcore.identity.user.generated_tokens`) * Number of token verification attempts (`aspnetcore.identity.user.verify_token_attempts`) * Authentication metrics * Duration of authentication operations (`aspnetcore.identity.sign_in.authenticate.duration`) * Number of password check attempts at sign-in (`aspnetcore.identity.sign_in.check_password_attempts`) * Number of successful sign-ins (`aspnetcore.identity.sign_in.sign_ins`) * Number of sign-outs (`aspnetcore.identity.sign_in.sign_outs`) * Number of remembered two-factor authentication (2FA) clients (`aspnetcore.identity.sign_in.two_factor_clients_remembered`) * Number of forgotten two-factor authentication (2FA) clients (`aspnetcore.identity.sign_in.two_factor_clients_forgotten`) ## Traces [Section titled “Traces”](#traces-1) Tip Added in Duende IdentityServer v6.1 Here’s e.g. the output for a request to the discovery endpoint: ![Honeycomb UI showing traces for discovery document endpoint](/_astro/otel_disco.BBgm8ly2_RcRU3.webp) When multiple applications send their traces to the same OpenTelemetry server, this becomes super useful for following e.g. authentication flows over service boundaries. The following screenshot shows the ASP.NET Core OpenID Connect authentication handler redeeming the authorization code: ![HoneyComb UI showing traces for the OpenID Connect authentication handler](/_astro/otel_flow_1.BBYe6Iu9_1HRa9h.webp) …and then contacting the userinfo endpoint: ![Honeycomb UI showing traces for the userinfo endpoint](/_astro/otel_flow_2.DcVRg6r2_Z1FNh6z.webp) *The above screenshots are from .* ### Tracing Sources [Section titled “Tracing Sources”](#tracing-sources) IdentityServer can emit very fine-grained traces which is useful for performance troubleshooting and general exploration of the control flow. This might be too detailed in production. You can select which information you are interested in by selectively listening to various traces: * *`IdentityServerConstants.Tracing.Basic`* High level request processing like request validators and response generators * *`IdentityServerConstants.Tracing.Cache`* Caching related tracing * *`IdentityServerConstants.Tracing.Services`* Services related tracing * *`IdentityServerConstants.Tracing.Stores`* Store related tracing * *`IdentityServerConstants.Tracing.Validation`* More detailed tracing related to validation ## OpenTelemetry From 3rd Party Logging Frameworks [Section titled “OpenTelemetry From 3rd Party Logging Frameworks”](#opentelemetry-from-3rd-party-logging-frameworks) If you’re unable to use the `ILogger` system in .NET, your choice of logging framework may be able to push log messages to traces. You can view their documentation to set that up. ### OpenTelemetry with Serilog [Section titled “OpenTelemetry with Serilog”](#opentelemetry-with-serilog) If you are logging with Serilog and want to use that framework’s native API to push log messages to traces, you need to: * Add the Serilog OpenTelemetry sink library * Instruct the Serilog logger object to write to the OpenTelemetry sink Note: See the Serilog [OpenTelemetry sink](https://github.com/serilog/serilog-sinks-opentelemetry) documentation for the most up to date information. ```bash dotnet add package Serilog.Sinks.OpenTelemetry ``` ```csharp Log.Logger = new LoggerConfiguration() .WriteTo.OpenTelemetry() .CreateLogger(); ``` ----- # Fundamentals > An overview of the building blocks of Duende IdentityServer, including core concepts (clients, resources, claims, keys, hosting) and what you provide (UI, data stores, identity management). Duende IdentityServer implements the OpenID Connect, OAuth and SAML protocol layers: issuing tokens, managing clients and scopes, and enforcing authorization policies. This section covers everything fundamental to working with IdentityServer, from the core concepts you need to understand to the components you provide. ## Core concepts [Section titled “Core concepts”](#core-concepts) These are the core concepts you’ll work with when configuring and running IdentityServer: * **Clients** — A client is any application that requests tokens from IdentityServer, whether a web app, SPA, mobile app, API, or backend service. Each client is registered with an allowed set of scopes, grant types, and redirect URIs. See [Clients](/identityserver/fundamentals/clients/). * **Resources** — Resources model what your system protects. Identity resources represent user claims (like profile or email), API scopes define logical permission boundaries, and API resources group scopes for audience-based access control. See [Resources](/identityserver/fundamentals/resources/). * **Claims** — Claims are name-value pairs that describe a user or client. They flow through tokens and are the primary way downstream applications learn about the authenticated entity. Understanding how claims are requested, issued, and transformed is central to working with IdentityServer. See [Claims](/identityserver/fundamentals/claims/) and [Claims Lifecycle](/identityserver/fundamentals/claims-lifecycle/). * **Users** — Users are the people who authenticate through IdentityServer. How users are modeled, stored, and resolved into claims depends on your identity management choice. See [Users](/identityserver/fundamentals/users/). * **Key Management** — IdentityServer uses cryptographic keys to sign tokens. Key management covers how signing keys are created, rotated, and stored, which is critical for production deployments. See [Key Management](/identityserver/fundamentals/key-management/). * **Hosting** — IdentityServer runs as middleware in an ASP.NET Core application. Hosting covers how to configure the host, set up endpoints, and prepare for production deployment. See [Hosting](/identityserver/fundamentals/hosting/). * **Events** — IdentityServer raises events for key operations like token issuance, login, and errors. These are useful for auditing, monitoring, and diagnostics. See [Events](/identityserver/fundamentals/openid-connect-events/). [Clients](/identityserver/fundamentals/clients/)Register and configure applications that request tokens from IdentityServer. [Resources](/identityserver/fundamentals/resources/)Model what your system protects: identity resources, API scopes, and API resources. [Claims](/identityserver/fundamentals/claims/)Understand how claims are requested, issued, and transformed in tokens. [Users](/identityserver/fundamentals/users/)How users are modeled and resolved into claims. [Key Management](/identityserver/fundamentals/key-management/)Create, rotate, and store the cryptographic keys that sign your tokens. [Hosting](/identityserver/fundamentals/hosting/)Configure and deploy IdentityServer as ASP.NET Core middleware. [Events](/identityserver/fundamentals/openid-connect-events/)Audit, monitor, and diagnose with IdentityServer's event system. ## What you provide [Section titled “What you provide”](#what-you-provide) IdentityServer handles protocols, but it relies on you for three things: a user interface, data storage, and identity management. There are ready-made components for each of these, and you can also implement your own. ![Duende IdentityServer - Components](/_astro/duende-identityserver-components-old.Dgloa-V7_1txqS6.svg) ### User Interface [Section titled “User Interface”](#user-interface) IdentityServer has no built-in UI. It relies on pages you provide for login, logout, consent, and error handling. This keeps you in full control of the user experience: credential types, visual design, multi-factor flows, and any additional pages (registration, password reset, etc.) are all yours to build. Ready-made UI is available via the [IdentityServer templates](/identityserver/overview/packaging/#templates), which give you a working starting point that you can customize completely. See [User Interface](/identityserver/ui/) for full coverage of login, logout, consent, and more. ### Data Stores [Section titled “Data Stores”](#data-stores) IdentityServer needs two kinds of persistent data: * **Configuration data**: clients, resources, and identity providers. Defines what your IdentityServer deployment supports. * **Operational data**: tokens, authorization codes, grants, and sessions. Generated at runtime during authentication flows. All data access is behind store interfaces, so you can use any database. Ready-made providers are available, or you can implement the interfaces yourself. See [Data Stores & Persistence](/identityserver/data/) for the full details. ### Identity & Profile Management [Section titled “Identity & Profile Management”](#identity--profile-management) IdentityServer needs to know who your users are and what claims to include in their tokens. It does not have a built-in user database; you connect it to yours via the `IProfileService` interface. Ready-made integrations are available for [User Management](/identityserver/identity/user-management/) and [ASP.NET Identity](/identityserver/identity/aspnet-identity/), or you can [implement `IProfileService`](/identityserver/identity/custom/) yourself to connect to any user store. [User Interface](/identityserver/ui/)Build login, logout, consent, and error pages. Start from the templates or build your own. [Data Stores](/identityserver/data/)Choose a provider for configuration and operational data: Entity Framework Core, in-memory, or custom. [Identity & Profile Management](/identityserver/identity/)Connect IdentityServer to your user store via User Management, ASP.NET Identity, or a custom IProfileService. ----- # Controlling Claims In IdentityServer Tokens > How IdentityServer selects, filters, and serializes user and client claims for identity tokens, access tokens, and the userinfo endpoint. IdentityServer emits claims about users and clients into tokens. You are in full control of which claims you want to emit, in which situations you want to emit those claims, and where to retrieve those claims from. ## User Claims [Section titled “User Claims”](#user-claims) User claims can be emitted in both identity and access tokens and in the [userinfo endpoint](/identityserver/reference/v8/endpoints/userinfo/). The central extensibility point to implement to emit claims is called the [profile service](/identityserver/reference/v8/services/profile-service/). The profile service is responsible for both gathering claim data and deciding which claims should be emitted. Whenever IdentityServer needs the claims for a user, it invokes the registered profile service with a [context](/identityserver/reference/v8/services/profile-service/#duendeidentityservermodelsprofiledatarequestcontext) that presents detailed information about the current request, including * the client that is making the request * the identity of the user * the type of the request (access token, id token, or userinfo) * the requested claim types, which are the claims types associated with requested scopes and resources ### Strategies For Emitting Claims [Section titled “Strategies For Emitting Claims”](#strategies-for-emitting-claims) You can use different strategies to determine which claims to emit based on the information in the profile context. * emit claims based on the requested claim types * emit claims based on user or client identity * always emit certain claims #### Emit Claims Based On The Client’s Request [Section titled “Emit Claims Based On The Client’s Request”](#emit-claims-based-on-the-clients-request) You can filter the claims you emit to only include the claim types requested by the client. If your client requires consent, this will also give end users the opportunity to approve or deny sharing those claims with the client. Clients can request claims in several ways: * Requesting an [IdentityResource](/identityserver/fundamentals/resources/identity/) by including the scope parameter for the `IdentityResource` requests the claims associated with the `IdentityResource` in its `UserClaims` collection. * Requesting an [ApiScope](/identityserver/fundamentals/resources/api-scopes/) by including the scope parameter for the `ApiScope` requests the claims associated with the `ApiScope` in its `UserClaims` collection. * Requesting an [ApiResource](/identityserver/fundamentals/resources/api-resources/) by including the resource indicator parameter for the `ApiResource` requests the claims associated with the `ApiResource` in its `UserClaims` collection. The `RequestedClaimTypes` property of the `ProfileDataRequestContext` contains the collection of claims requested by the client. If your profile service extends the `DefaultProfileService`, you can use its `AddRequestedClaims` method to add only requested and approved claims. The intent is that your profile service can retrieve claim data and then filter that claim data based on what was requested by the client. For example: SampleProfileService.cs ```csharp public class SampleProfileService : DefaultProfileService { public virtual async Task GetProfileDataAsync(ProfileDataRequestContext context) { var claims = await GetClaimsAsync(context); context.AddRequestedClaims(claims); } private async Task> GetClaimsAsync(ProfileDataRequestContext context) { // Your implementation that retrieves claims goes here } } ``` #### Why Is A User Claim Missing From The Token? [Section titled “Why Is A User Claim Missing From The Token?”](#why-is-a-user-claim-missing-from-the-token) A claim on `HttpContext.User` or `ProfileDataRequestContext.Subject` is not automatically included in a token. `AddRequestedClaims` compares each claim type with `ProfileDataRequestContext.RequestedClaimTypes` and drops claims that were not requested. The default profile service uses this filtering behavior too. IdentityServer builds the requested claim types from the resources in the authorization request: 1. The client requests a scope or resource. 2. IdentityServer resolves the matching resources. Identity resources supply claims for identity tokens and the `userinfo` endpoint; API scopes and API resources supply claims for access tokens. 3. The relevant `UserClaims` collections become `RequestedClaimTypes`. 4. `AddRequestedClaims` adds only matching claims to `IssuedClaims`. For example, adding a `department` claim to the signed-in user is not enough. The IdentityServer host must define a resource containing that claim, allow the client to request it, and receive the matching scope in the authorization request: ```csharp // Config.cs in the IdentityServer host public static IEnumerable IdentityResources => [ new IdentityResources.OpenId(), new IdentityResource( name: "department_info", userClaims: ["department"], displayName: "Your department") ]; public static Client Client => new() { ClientId = "web", // ... other client settings AllowedScopes = { "openid", "department_info" } }; ``` The client must request `department_info`. The profile service will then see `department` in `RequestedClaimTypes`, and `AddRequestedClaims` can include it in the appropriate token or `userinfo` endpoint’s response. Note When an authorization request produces both an identity token and an access token, IdentityServer keeps the identity token small by default and makes most identity claims available from the [`userinfo` endpoint](/identityserver/reference/v8/endpoints/userinfo/). If a claim is configured correctly but is not in the identity token, check the `userinfo` endpoint before changing the profile service. See the [Claims Lifecycle](/identityserver/fundamentals/claims-lifecycle/) page for the full flow. If a claim is still missing, enable debug logging for `Duende.IdentityServer`. The default profile service logs the requested claim types and the claim types it issued. Before bypassing `AddRequestedClaims`, check the resource’s `UserClaims`, the client’s `AllowedScopes`, and the scopes in the request. Adding a claim directly to `IssuedClaims` bypasses this filter. Do that only when the claim must be sent regardless of the requested scopes, and make sure it does not expose data to a client that should not receive it. #### Always Emit Claims [Section titled “Always Emit Claims”](#always-emit-claims) We generally recommend emitting claims based on the requested claim types, as that respects the scopes and resources requested by the client and gives the end user an opportunity to consent to this sharing of information. However, if you have claims that don’t need to follow such rules, such as claims that are an integral part of the user’s identity and that are needed in most scenarios, they can be added by directly updating the `context.IssuedClaims` collection. For example: SampleProfileService.cs ```csharp public class SampleProfileService : DefaultProfileService { public virtual async Task GetProfileDataAsync(ProfileDataRequestContext context) { var claims = await GetClaimsAsync(context); context.IssuedClaims.AddRange(claims); } private async Task GetClaimsAsync(ProfileDataRequestContext context) { // Your implementation that retrieves claims goes here } } ``` #### Emit Claims Based On The User Or Client Identity [Section titled “Emit Claims Based On The User Or Client Identity”](#emit-claims-based-on-the-user-or-client-identity) Finally, you might have claims that are only appropriate for certain users or clients. Your `ProfileService` can add whatever filtering or logic that you like. ### The Subject Of The ProfileDataRequestContext [Section titled “The Subject Of The ProfileDataRequestContext”](#the-subject-of-the-profiledatarequestcontext) When the profile service is invoked to add claims to tokens, the `Subject` property on the `ProfileDataRequestContext` contains the principal that was issued during user sign-in. Typically, the profile service will source some claims from the `Subject` and others from databases or other data sources. When the profile service is called for requests to the [userinfo endpoint](/identityserver/reference/v8/endpoints/userinfo/), the `Subject` property will not contain the principal issued during user sign-in, since userinfo calls don’t happen as part of a session. Instead, the `Subject` property will contain a claims principal populated with the claims in the access token used to authorize the userinfo call. You can check the caller of the profile service by querying the `Caller` property on the context. ## Client Claims [Section titled “Client Claims”](#client-claims) Client claims are a set of pre-defined claims that are emitted in access tokens. They are defined on a per-client basis, meaning that each client can have its own unique set of client claims. The following shows an example of a client that is associated with a certain customer in your system: ```csharp // Config.cs in the IdentityServer host var client = new Client { ClientId = "client", // rest omitted Claims = { new ClientClaim("customer_id", "123") } }; ``` To avoid accidental collision with user claims, client claims are prefixed with `client_`. For example, the above `ClientClaim` would be emitted as the `client_customer_id` claim type in access tokens. You can change or remove this prefix by setting the `ClientClaimsPrefix` on the [client definition](/identityserver/reference/v8/models/client/#token). Note By default, client claims are only sent in the client credentials flow. If you want to enable them for other flows, you need to set the `AlwaysSendClientClaims` property on the client definition. ### Setting Client Claims Dynamically [Section titled “Setting Client Claims Dynamically”](#setting-client-claims-dynamically) If you want to set client claims dynamically, you could either do that at client load time (via a client [store](/identityserver/data) implementation), or using a [custom token request validator](/identityserver/tokens/dynamic-validation/). ## Claim Serialization [Section titled “Claim Serialization”](#claim-serialization) Claim values are serialized based on the `ClaimValueType` of the claim. Claims that don’t specify a `ClaimValueType` are serialized as strings. Claims that specify a `ClaimValueType` of `System.Security.Claims.ClaimValueTypes.Integer`, `System.Security.Claims.ClaimValueTypes.Integer32`, `System.Security.Claims.ClaimValueTypes.Integer64`, `System.Security.Claims.ClaimValueTypes.Double`, or `System.Security.Claims.ClaimValueTypes.Boolean` are parsed as the corresponding type, while those that specify `IdentityServerConstants.ClaimValueTypes.Json` are serialized to JSON using `System.Text.Json`. ----- # Claims Lifecycle > Visual guide showing where claims exist during each OIDC protocol flow and when the Profile Service is invoked. Understanding where claims live at each step of a protocol flow is essential for configuring IdentityServer correctly. This page provides sequence diagrams for each major flow, showing: * **Where** claims are stored (session cookie, persisted grants DB, tokens, client cookie, API context) * **When** the [Profile Service](/identityserver/reference/v8/services/profile-service/) is invoked * **How** configuration options change claim placement ## Claim Locations [Section titled “Claim Locations”](#claim-locations) | Location | Description | | --------------------------- | ---------------------------------------------------------------------------------------------- | | **IdentityServer Session** | Cookie storing `sub`, `name`, `amr`, `auth_time`, `idp`, `sid` after login | | **Persisted Grants DB** | Stores authorization codes, refresh tokens, and reference access tokens with associated claims | | **Identity Token** | JWT sent to the client describing the authentication event | | **Access Token** | JWT or reference token authorizing API access, contains scope-based claims | | **UserInfo Endpoint** | Returns identity claims on demand when presented with a valid access token | | **Client Session (Cookie)** | ASP.NET Core auth cookie storing claims the client chose to persist | | **API HttpContext** | Claims principal populated from the validated access token | *** ## Authorization Code with UserInfo Recommended [Section titled “Authorization Code with UserInfo ”](#authorization-code-with-userinfo) This is the recommended default. The id\_token is minimal (just `sub`), and the client retrieves full identity claims from the userinfo endpoint. This keeps tokens small and is the standard behavior for confidential clients. ``` sequenceDiagram participant B as Browser participant C as Client App participant IS as IdentityServer participant PS as Profile Service B->>IS: GET /authorize (response_type=code) IS-->>B: Redirect with authorization code B->>C: Authorization code C->>IS: POST /token (grant_type=authorization_code) IS->>PS: GetProfileData
(Caller=IdentityToken,
includeAllIdentityClaims=false) PS-->>IS: sub (minimal) IS->>PS: GetProfileData
(Caller=AccessToken) PS-->>IS: sub, scope-based claims IS-->>C: id_token (sub only) + access_token C->>IS: GET /connect/userinfo (Bearer token) IS->>PS: GetProfileData
(Caller=UserInfoEndpoint) PS-->>IS: name, email, roles... IS-->>C: JSON { name, email, ... } Note over C: Client cookie stores:
sub + userinfo response claims ``` **Key points:** * Profile Service is called **3 times** with different `Caller` values * The id\_token is small (reduces redirect URL size) * Identity claims come from userinfo, not the id\_token * The ASP.NET Core OIDC handler does this automatically when `GetClaimsFromUserInfoEndpoint = true` *** ## Authorization Code without UserInfo [Section titled “Authorization Code without UserInfo”](#authorization-code-without-userinfo) When `AlwaysIncludeUserClaimsInIdToken = true` on the client, identity claims are placed directly in the id\_token even though an access token is available. This avoids the extra round-trip to userinfo. ``` sequenceDiagram participant B as Browser participant C as Client App participant IS as IdentityServer participant PS as Profile Service B->>IS: GET /authorize (response_type=code) IS-->>B: Redirect with authorization code B->>C: Authorization code C->>IS: POST /token (grant_type=authorization_code) IS->>PS: GetProfileData
(Caller=IdentityToken,
includeAllIdentityClaims=true) PS-->>IS: sub, name, email, roles... IS->>PS: GetProfileData
(Caller=AccessToken) PS-->>IS: sub, scope-based claims IS-->>C: id_token + access_token Note over C: id_token has ALL identity claims
access_token has API-scoped claims Note over C: Client cookie stores id_token claims
No userinfo call needed ``` **Configuration:** Set `AlwaysIncludeUserClaimsInIdToken = true` on the [Client](/identityserver/reference/v8/models/client/) to enable this behavior. **When to use:** When your client doesn’t want to make an extra HTTP call to userinfo, or when you need claims immediately available at sign-in without a round-trip. *** ## Refresh Token Renewal [Section titled “Refresh Token Renewal”](#refresh-token-renewal) When a client refreshes an access token, claims may be reloaded from the Profile Service or reused from the original grant. The behavior depends on the `UpdateAccessTokenClaimsOnRefresh` client setting. * Default (reuse claims) The new access token reuses claims from the original authorization. The Profile Service is only called to verify the user is still active. ``` sequenceDiagram participant C as Client participant IS as IdentityServer participant DB as Persisted Grants DB participant PS as Profile Service C->>IS: POST /token (grant_type=refresh_token) IS->>DB: Load refresh token grant DB-->>IS: Original subject + claims metadata IS->>PS: IsActiveAsync (check user still valid) PS-->>IS: active=true Note over IS: Reuse claims from
original authorization IS-->>C: New access_token with ORIGINAL claims
(+ new refresh_token if rotated) ``` This is the default behavior when `UpdateAccessTokenClaimsOnRefresh = false`. * Fresh claims The Profile Service is re-invoked to load the latest claims from your user store. Use this when tokens need to reflect current user state (e.g., role changes, email updates). ``` sequenceDiagram participant C as Client participant IS as IdentityServer participant DB as Persisted Grants DB participant PS as Profile Service C->>IS: POST /token (grant_type=refresh_token) IS->>DB: Load refresh token grant DB-->>IS: Original subject + claims metadata IS->>PS: IsActiveAsync (check user still valid) PS-->>IS: active=true IS->>PS: GetProfileData(Caller=AccessToken) PS-->>IS: Fresh claims from user store IS-->>C: New access_token with CURRENT claims
(+ new refresh_token if rotated) ``` Set `UpdateAccessTokenClaimsOnRefresh = true` on the [Client](/identityserver/reference/v8/models/client/) to enable this. **Trade-off:** This adds a Profile Service call on every refresh, which may increase load on your user store. *** ## API with JWT Access Token [Section titled “API with JWT Access Token”](#api-with-jwt-access-token) With JWT access tokens, the API validates the token locally. There is no backchannel call to IdentityServer. Claims are frozen at the time the token was issued. ``` sequenceDiagram participant C as Client participant API as API (Resource Server) participant IS as IdentityServer Note over C: Has JWT access_token with:
sub, client_id, scope, aud,
custom claims, exp, iss C->>API: GET /resource (Authorization: Bearer {jwt}) API->>API: Validate JWT signature
(using cached JWKS from IS) API->>API: Check exp, iss, aud API->>API: Extract claims → HttpContext.User API-->>C: 200 OK (resource data) Note over API: No call to IdentityServer!
Claims reflect state at issuance time ``` **Key points:** * The API never contacts IdentityServer during request processing * Claims are **frozen** at token issuance. If a user’s role changes, the old token still has the old claims until it expires * Keep JWT lifetimes short (5-15 min) and use refresh tokens for longevity * The API gets its JWKS keys from the discovery document (cached) *** ## API with Reference Token + Introspection [Section titled “API with Reference Token + Introspection”](#api-with-reference-token--introspection) With reference tokens, the API must call IdentityServer’s introspection endpoint on every request to resolve the opaque token handle. ``` sequenceDiagram participant C as Client participant API as API (Resource Server) participant IS as IdentityServer participant DB as Persisted Grants DB participant PS as Profile Service Note over C: Has opaque reference token handle C->>API: GET /resource (Authorization: Bearer {handle}) API->>IS: POST /connect/introspect
(token={handle}, client_id, client_secret) IS->>DB: Lookup token by handle DB-->>IS: Token data + claims IS->>PS: IsActiveAsync (validate user) PS-->>IS: active=true IS-->>API: { active: true, sub, scope,
client_id, claims... } API->>API: Build ClaimsPrincipal from response API-->>C: 200 OK (resource data) ``` **Key points:** * Every API request triggers an introspection call (cache where appropriate) * Reference tokens can be **revoked immediately** by deleting from the grants DB * The API must have an `ApiSecret` configured on its `ApiResource` for introspection * Claims reflect the stored state (from original issuance unless updated) *** ## Implicit Flow Legacy [Section titled “Implicit Flow ”](#implicit-flow) Caution The implicit flow is **no longer recommended** for new applications. It exposes tokens in the browser URL fragment and lacks the security benefits of PKCE. Use the authorization code flow with PKCE instead. This flow is documented here for reference, as it is still supported. In the implicit flow, ALL identity claims go directly into the `id_token` because no access token is issued (so no userinfo endpoint is available). ``` sequenceDiagram participant B as Browser participant IS as IdentityServer participant PS as Profile Service participant C as Client App B->>IS: GET /authorize (response_type=id_token) Note over IS: Session cookie: sub, name,
amr, auth_time, idp, sid IS->>PS: GetProfileData
(Caller=IdentityToken,
includeAllIdentityClaims=true) PS-->>IS: sub, name, email, roles... IS-->>B: Redirect with id_token (fragment) Note over B: id_token contains ALL
identity resource claims B->>C: POST id_token Note over C: Client cookie stores claims
from id_token (sub, name, email...) ``` **Key points:** * `includeAllIdentityClaims` is `true` because there’s no access token to use at the userinfo endpoint * The Profile Service is called once * All identity resource claims are packed into the id\_token * Tokens in URL fragments are visible in browser history and logs. Prefer authorization code flow with PKCE *** ## Configuration Options Summary [Section titled “Configuration Options Summary”](#configuration-options-summary) | Option | Default | Effect | Relevant Flows | | ---------------------------------- | ----------- | ---------------------------------------------------------------- | -------------- | | `AlwaysIncludeUserClaimsInIdToken` | `false` | When true, all identity claims go in id\_token (skips userinfo) | Code flow | | `UpdateAccessTokenClaimsOnRefresh` | `false` | When true, Profile Service is re-invoked on refresh | Refresh | | `AccessTokenType` | `Jwt` | `Jwt` = self-contained, `Reference` = opaque + introspection | API calls | | `AlwaysSendClientClaims` | `false` | Include client claims in all flows (not just client credentials) | All flows | | `ClientClaimsPrefix` | `"client_"` | Prefix added to client claims in access tokens | All flows | ## See Also [Section titled “See Also”](#see-also) [Claims Emission Strategies](/identityserver/fundamentals/claims/)How to control which claims the Profile Service returns [Profile Service Reference](/identityserver/reference/v8/services/profile-service/)The IProfileService API [Refreshing Tokens](/identityserver/tokens/refresh/)Refresh token configuration and lifecycle [Claims Service](/identityserver/reference/v8/services/claims-service/)IClaimsService for token-level claim filtering ----- # Clients > Learn about configuring and managing client applications that can request tokens from IdentityServer [Clients](/identityserver/overview/terminology/#client), or [connected applications](/general/glossary/#connected-application), represent applications that can request tokens from your IdentityServer. The details vary, but you typically define the following common settings for a client: * a unique client ID * a secret if needed * the allowed interactions with the token service (called a grant type) * a network location where identity and/or access token gets sent to (called a redirect URI) * a list of scopes (aka resources) the client is allowed to access ## Defining A Client For Server To Server Communication [Section titled “Defining A Client For Server To Server Communication”](#defining-a-client-for-server-to-server-communication) In this scenario no interactive user is present - a service (i.e. the client) wants to communicate with an API (i.e. the resource that supports the scope): ```csharp public class Clients { public static IEnumerable Get() { return new List { new Client { ClientId = "service.client", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.ClientCredentials, AllowedScopes = { "api1", "api2.read_only" } } }; } } ``` ## Defining An Interactive Application: Authentication And Delegated API Access [Section titled “Defining An Interactive Application: Authentication And Delegated API Access”](#defining-an-interactive-application-authentication-and-delegated-api-access) Interactive applications (e.g. web applications or native desktop/mobile applications) use the authorization code flow. This flow gives you the best security because the access tokens are transmitted via back-channel calls only (and gives you access to refresh tokens): ```csharp var interactiveClient = new Client { ClientId = "interactive", AllowedGrantTypes = GrantTypes.Code, AllowOfflineAccess = true, ClientSecrets = { new Secret("secret".Sha256()) }, RedirectUris = { "http://localhost:21402/signin-oidc" }, PostLogoutRedirectUris = { "http://localhost:21402/" }, FrontChannelLogoutUri = "http://localhost:21402/signout-oidc", AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Email, "api1", "api2.read_only" }, }; ``` ## Defining Clients In `appsettings.json` [Section titled “Defining Clients In appsettings.json”](#defining-clients-in-appsettingsjson) The `AddInMemoryClients` extensions method also supports adding clients from the ASP.NET Core configuration file. This allows you to define static clients directly from the appsettings.json file: appsettings.json ```json { "IdentityServer": { "Clients": [ { "Enabled": true, "ClientId": "local-dev", "ClientName": "Local Development", "ClientSecrets": [ { "Value": "" } ], "AllowedGrantTypes": [ "client_credentials" ], "AllowedScopes": [ "api1" ] } ] } } ``` Then pass the configuration section to the `AddInMemoryClients` method: Program.cs ```csharp AddInMemoryClients(configuration.GetSection("IdentityServer:Clients")) ``` ----- # Hosting > Learn how to host and configure Duende IdentityServer in ASP.NET Core applications by adding services and middleware to the pipeline You add the Duende IdentityServer engine to any ASP.NET Core application by adding the relevant services to the dependency injection (DI) system and adding the middleware to the processing pipeline. Note While technically you could share the ASP.NET Core host between Duende IdentityServer, clients or APIs, we recommend putting your IdentityServer into a separate application. ## Dependency Injection System [Section titled “Dependency Injection System”](#dependency-injection-system) You add the necessary services to the ASP.NET Core service provider by calling `AddIdentityServer` at application startup: Program.cs ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { // ... }); ``` Many of the fundamental configuration settings can be set on the options. See the [`IdentityServerOptions`](/identityserver/reference/v8/options/) reference for more details. The builder object has a number of extension methods to add additional services to the ASP.NET Core service provider. You can see the full list in the [reference](/identityserver/reference/v8/di/) section, but very commonly you start by adding the configuration stores for clients and resources, e.g.: Program.cs ```csharp var idsvrBuilder = builder.Services.AddIdentityServer() .AddInMemoryClients(Config.Clients) .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes) ``` The above is using the in-memory stores, but we also support EntityFramework-based implementations and custom stores. See [here](/identityserver/data) for more information. Note The `AddIdentityServer` extensions method also adds the required authentication services (it calls `AddAuthentication` internally). If you want to configure the authentication options, or be explicit about which services are registered, you can use the `AddAuthentication` (and `AddAuthorization`) extension method directly: Program.cs ```csharp builder.Services.AddAuthentication(); builder.Services.AddAuthorization(); ``` ## Request Pipeline [Section titled “Request Pipeline”](#request-pipeline) You need to add the Duende IdentityServer middleware to the pipeline by calling `UseIdentityServer`. Since ordering is important in the pipeline, you typically want to put the IdentityServer middleware after the static files, but before the UI framework like MVC. This would be a very typical minimal pipeline: Program.cs ```csharp var app = builder.Build(); app.UseStaticFiles(); app.UseRouting(); app.UseIdentityServer(); app.UseAuthorization(); app.MapDefaultControllerRoute(); ``` Note `UseIdentityServer` includes a call to `UseAuthentication`, so it’s not necessary to have both. However, IdentityServer does not include a call to `UseAuthorization`. You will need to add `UseAuthorization` (after `UseIdentityServer`/`UseAuthentication`) to include the authorization middleware into your pipeline. This will enable you to use various authorization features in your application. If you use the Duende UI template and its various pages, the use of `UseAuthorization` is required. ----- # Signing Key Management And Rotation In IdentityServer > How to create, store, rotate, and retire signing keys in IdentityServer using automatic or static key management. Duende IdentityServer issues several types of tokens that are cryptographically signed, including identity tokens, JWT access tokens, and logout tokens. To create those signatures, IdentityServer needs key material. That key material can be configured automatically, by using the Automatic Key Management feature, or manually, by loading the keys from a secured location with static configuration. IdentityServer supports [signing](https://tools.ietf.org/html/rfc7515) tokens using the `RS`, `PS` and `ES` family of cryptographic signing algorithms. ## Why Rotate Signing Keys? [Section titled “Why Rotate Signing Keys?”](#why-rotate-signing-keys) IdentityServer can use a private signing key to sign identity tokens, JWT access tokens, and logout tokens; clients and APIs can then use the corresponding public key to verify that those tokens came from IdentityServer and were not changed. A signing key is trusted by every client and API that accepts tokens from your IdentityServer. If its private key is compromised, an attacker can create tokens that those applications may trust. Regular rotation limits how long one key remains in active use and makes emergency replacement a process you have already exercised. Caution Rotation must not invalidate tokens that are still in use. Publish a new public key before using it to sign tokens, and keep the retired public key available until tokens signed with it have expired. Automatic Key Management handles this overlap for you. If you manage keys yourself, follow the [phased manual rotation](#solution-2-phased-rotation) process described below. ## Automatic Key Management [Section titled “Automatic Key Management”](#automatic-key-management) Duende IdentityServer can manage signing keys for you using the Automatic Key Management feature. Automatic Key Management follows best practices for handling signing key material, including * automatic rotation of keys * secure storage of keys at rest using data protection * announcement of upcoming new keys * maintenance of retired keys Note This feature is part of the [Duende IdentityServer Business (legacy), Enterprise (legacy), Standard (add-on), Advanced, and Custom Edition](https://duendesoftware.com/products/identityserver). ### Configuration [Section titled “Configuration”](#configuration) Automatic Key Management is configured by the options in the `KeyManagement` property on the [`IdentityServerOptions`](/identityserver/reference/v8/options/#key-management). ### Managed Key Lifecycle [Section titled “Managed Key Lifecycle”](#managed-key-lifecycle) Keys created by Automatic Key Management move through several phases. First, new keys are announced, that is, they are added to the list of keys in discovery, but not yet used for signing. After a configurable amount of `PropagationTime`, keys are promoted to be signing credentials, and will be used by IdentityServer to sign tokens. Eventually, enough time will pass that the key is older than the configurable `RotationTime`, at which point the key is retired, but kept in discovery for a configurable `RetentionDuration`. After the `RetentionDuration` has passed, keys are removed from discovery, and optionally deleted. The default is to rotate keys every 90 days, announce new keys with 14 days of propagation time, retain old keys for a duration of 14 days, and to delete keys when they are retired. ``` --- config: theme: default gantt: useWidth: 800 useMaxWidth: false --- gantt title 90 Day Key Rotation Schedule per Signing Algorithm todayMarker off section RS256 Signing :active, rsa_s, 2025-01-01, 76d Retire :rsa_r, after rsa_s, 14d Delete :crit, rsa_d, after rsa_r, 1d Announce :rsa_na, 2025-03-03, 14d Signing :active, rsa_ns, after rsa_na, 62d Retire :rsa_nr, after rsa_ns, 14d Delete :crit, rsa_nd, after rsa_nr, 1d section ES256 Signing :active, es_s, 2025-01-01, 76d Retire :es_r, after es_s, 14d Delete :crit, :es_d, after es_r, 1d Announce :es_na, 2025-03-03, 14d Signing :active, es_ns, after es_na, 62d Retire :es_nr, after es_ns, 14d Delete :crit, es_nd, after es_nr, 1d ``` All of these options are configurable in the `KeyManagement` options. For example: Program.cs ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { // new key every 30 days options.KeyManagement.RotationInterval = TimeSpan.FromDays(30); // announce new key 2 days in advance in discovery options.KeyManagement.PropagationTime = TimeSpan.FromDays(2); // keep old key for 7 days in discovery for validation of tokens options.KeyManagement.RetentionDuration = TimeSpan.FromDays(7); // don't delete keys after their retention period is over options.KeyManagement.DeleteRetiredKeys = false; }); ``` ### Key Storage [Section titled “Key Storage”](#key-storage) Automatic Key Management stores keys through the abstraction of the [`ISigningKeyStore`](/identityserver/data/operational/#keys). You can implement this extensibility point to customize the storage of your keys (perhaps using a key vault of some kind), or use one of the two implementations of the `ISigningKeyStore` that we provide: * the default `FileSystemKeyStore`, which writes keys to the file system. * the [EntityFramework operational store](/identityserver/data/providers/entityframework-core/#operational-store) which writes keys to a database using EntityFramework. The default `FileSystemKeyStore` writes keys to the `KeyPath` directory configured in your IdentityServer host, which defaults to the directory `~/keys`. This directory should be excluded from source control. If you are deploying in a load balanced environment and wish to use the `FileSystemKeyStore`, all instances of IdentityServer will need read/write access to the `KeyPath`. Program.cs ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { // set path to store keys options.KeyManagement.KeyPath = "/home/shared/keys"; }); ``` ### Encryption Of Keys at Rest [Section titled “Encryption Of Keys at Rest”](#encryption-of-keys-at-rest) The keys created by Automatic Key Management are sensitive cryptographic secrets that should be encrypted at rest. By default, keys managed by Automatic Key Management are protected at rest using ASP.NET Core Data Protection. This is controlled with the `DataProtectKeys` flag, which is on by default. We recommend leaving this flag on unless you are using a custom `ISigningKeyStore` to store your keys in a secure location that will ensure keys are encrypted at rest. For example, if you implement the `ISigningKeyStore` to store your keys in Azure Key Vault, you could safely disabled `DataProtectKeys`, relying on Azure Key Vault to encrypt your signing keys at rest. See the [deployment](/identityserver/deployment/) section for more information about setting up data protection. ### Manage Multiple Keys [Section titled “Manage Multiple Keys”](#manage-multiple-keys) By default, Automatic Key Management will maintain a signing credential and validation keys for a single cryptographic algorithm (`RS256`). You can specify multiple keys, algorithms, and if those keys should additionally get wrapped in an X.509 certificate. Automatic key management will create and rotate keys for each signing algorithm you specify. Note *X.509 certificates* have an expiration date, but IdentityServer does not use this data to validate the certificate and throw an exception. If a certificate has expired then you must decide whether to continue using it or replace it with a new certificate. ```csharp options.KeyManagement.SigningAlgorithms = new[] { // RS256 for older clients (with additional X.509 wrapping) new SigningAlgorithmOptions(SecurityAlgorithms.RsaSha256) { UseX509Certificate = true }, // PS256 new SigningAlgorithmOptions(SecurityAlgorithms.RsaSsaPssSha256), // ES256 new SigningAlgorithmOptions(SecurityAlgorithms.EcdsaSha256) }; ``` Note When you register multiple signing algorithms, the first in the list will be the default used for signing tokens. Client and API resource definitions both have an `AllowedTokenSigningAlgorithm` property to override the default on a per resource and client basis. ### Using The Same Signing Keys For OIDC And SAML [Section titled “Using The Same Signing Keys For OIDC And SAML”](#using-the-same-signing-keys-for-oidc-and-saml) When SAML is enabled, IdentityServer uses the same signing credentials for OIDC tokens and SAML messages. This gives both protocols one rotation schedule and one key store. The active key signs new JWTs and SAML messages, while current and recently rotated public keys are published through the OIDC JWKS endpoint and SAML IdP metadata during rollover. SAML metadata requires X.509 certificates. Automatic Key Management creates RSA keys by default, and the SAML component automatically wraps those managed RSA keys in self-signed X.509 certificates that contain the RSA public key. You do not need to set `UseX509Certificate` just to enable SAML. These generated certificates are containers for signing-key material, not PKI identity certificates. Service Providers should establish trust from the IdP metadata and refresh it during rotation rather than rely on the certificate subject or validity dates. If you use Static Key Management, configure an X.509 signing certificate with a private key. SAML cannot automatically wrap a manually registered raw RSA key, including one registered by `AddDeveloperSigningCredential()`. Caution The default SAML signing service supports RSA signing credentials. `UseX509Certificate` is not supported for EC keys. Use a custom [`ISamlSigningService`](/identityserver/saml/extensibility/#isamlsigningservice) if SAML needs a different certificate, an independent rotation schedule, or integration with an external key system. `PropagationTime` determines how long a new managed key is published before IdentityServer starts using it. Set this long enough for every Service Provider to refresh the IdP metadata. `RetentionDuration` keeps the previous certificate in metadata while Service Providers may still be validating SAML messages signed with the previous key and previously issued OIDC tokens remain valid. Service Providers that use a statically configured certificate must be updated as part of every rotation. ## Static Key Management [Section titled “Static Key Management”](#static-key-management) Instead of using [Automatic Key Management](#automatic-key-management), IdentityServer’s signing keys can be set manually. Automatic Key Management is generally recommended, but if you want to explicitly control your keys statically, or you have a license that does not include the feature, you will need to manually manage your keys. With static configuration you are responsible for secure storage, loading and rotation of keys. ## Disabling Key Management [Section titled “Disabling Key Management”](#disabling-key-management) The automatic key management feature can be disabled by setting the `Enabled` flag to `false` on the `KeyManagement` property of [`IdentityServerOptions`](/identityserver/reference/v8/options/#key-management): Program.cs ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = false; }); ``` ## Key Creation [Section titled “Key Creation”](#key-creation) Without automatic key management, you are responsible for creating your own cryptographic keys. Such keys can be created with many tools. Some options include: * Use the PowerShell commandlet [New-SelfSignedCertificate](https://learn.microsoft.com/en-us/powershell/module/pki/new-selfsignedcertificate?view=windowsserver2022-ps) to self-sign your own certificate * Create certificates using [Azure Key Vault](https://learn.microsoft.com/en-us/azure/key-vault/certificates/certificate-scenarios) * Create certificates using your Public Key Infrastructure. * Create certificates using C# (see below) ```csharp var name = "MySelfSignedCertificate"; // Generate a new key pair using var rsa = RSA.Create(keySizeInBits: 2048); // Create a certificate request var request = new CertificateRequest( subjectName: $"CN={name}", rsa, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1 ); // Self-sign the certificate var certificate = request.CreateSelfSigned( DateTimeOffset.Now, DateTimeOffset.Now.AddYears(1) ); // Export the certificate to a PFX file var pfxBytes = certificate.Export( // TODO: pick a format X509ContentType.Pfx, // TODO: change the password password: "password" ); File.WriteAllBytes($"{name}.pfx", pfxBytes); Console.Write(certificate); Console.WriteLine("Self-signed certificate created successfully."); Console.WriteLine($"Certificate saved to {name}.pfx"); ``` ## Adding Keys [Section titled “Adding Keys”](#adding-keys) Signing keys are added with the [`AddSigningCredential`](/identityserver/reference/v8/di/#signing-keys) configuration method: Program.cs ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(); var key = LoadKeyFromVault(); // (Your code here) idsvrBuilder.AddSigningCredential(key, SecurityAlgorithms.RsaSha256); ``` You can call `AddSigningCredential` multiple times if you want to register more than one signing key. When you register multiple signing algorithms, the first one added will be the default used for signing tokens. Client and API resource definitions both have an `AllowedTokenSigningAlgorithm` property to override the default on a per resource and client basis. Another configuration method called `AddValidationKey` can be called to register public keys that should be accepted for token validation. ## Key Storage [Section titled “Key Storage”](#key-storage-1) With automatic key management disabled, secure storage of the key material is left to you. This key material should be treated as highly sensitive. Key material should be encrypted at rest, and access to it should be restricted. Loading a key from disk into memory can be done using the `X509CertificateLoader` found in .NET assuming your hosting environment has proper security practices in place. ```csharp // load certificate from disk var bytes = File.ReadAllBytes("mycertificate.pfx"); var importedCertificate = X509CertificateLoader.LoadPkcs12(bytes, "password"); ``` You may also choose to load a certificate from the current environment’s key store using the `X509Store` class. ```csharp // Pick the appropriate StoreName and StoreLocation var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadWrite); var certificate = store .Certificates .First(c => c.Thumbprint == ""); ``` If you’re generating self-signed certificates using C#, you can use the `X509Store` to store the certificate into the current hosting environment as well. ```csharp // Pick the appropriate StoreName and StoreLocation var store = new X509Store(StoreName.My, StoreLocation.CurrentUser); store.Open(OpenFlags.ReadWrite); // push certificate into store var certificate = CreateCertificate(); store.Add(certificate); ``` ## Manual Key Rotation [Section titled “Manual Key Rotation”](#manual-key-rotation) With automatic key management disabled, you will need to rotate your keys manually. The rotation process must be done carefully for two reasons: 1. Client applications and APIs cache key material. If you begin using a new key too quickly, new tokens will be signed with a key that is not yet in their caches. This will cause clients to not be able to validate the signatures of new id tokens which will prevent users from logging in, and APIs will not be able to validate signatures of access tokens, which will prevent authorization of calls to those APIs. 2. Tokens signed with the old key material probably exist. If you tell APIs to stop using the old key too quickly, APIs will reject the signatures of old tokens, again causing authorization failures at your APIs. There are two solutions to these problems. Which one is right for you depends on the level of control you have over client applications, the amount of downtime that is acceptable, and the degree to which invalidating old tokens matters to you. ### Solution 1: Invalidate All Caches When Keys Are Rotated [Section titled “Solution 1: Invalidate All Caches When Keys Are Rotated”](#solution-1-invalidate-all-caches-when-keys-are-rotated) One solution to these problems is to invalidate the caches in all the client applications and APIs immediately after the key is rotated. In ASP.NET, the simplest way to do so is to restart the hosting process, which clears the cached signing keys of the authentication middleware. This is only appropriate if all the following are true: * You have control over the deployment of all the client applications. * You can tolerate a maintenance window in which your services are all restarted. * You don’t mind that users will need to log in again after the key is rotated. ### Solution 2: Phased Rotation [Section titled “Solution 2: Phased Rotation”](#solution-2-phased-rotation) A more robust solution is to gradually transition from the old to the new key. This requires three phases. #### Phase 1: Announce The New Key [Section titled “Phase 1: Announce The New Key”](#phase-1-announce-the-new-key) First, announce a new key that will be used for signing in the future. During this phase, continue to sign tokens with the old key. The idea is to allow for all the applications and APIs to update their caches without any interruption in service. Configure IdentityServer for phase 1 by registering the new key as a validation key. Program.cs ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = false; }); var oldKey = LoadOldKeyFromVault(); var newKey = LoadNewKeyFromVault(); idsvrBuilder.AddSigningCredential(oldKey, SecurityAlgorithms.RsaSha256); idsvrBuilder.AddValidationKey(newKey, SecurityAlgorithms.RsaSha256) ``` Once IdentityServer is updated with the new key as a validation key, wait to proceed to phase 2 until all the applications and services have updated their signing key caches. The default cache duration in .NET is 24 hours, but this is customizable. You may also need to support clients or APIs built with other platforms or that were customized to use a different value. Ultimately you have to decide how long to wait to proceed to phase 2 in order to ensure that all clients and APIs have updated their caches. #### Phase 2: Start Signing With The New Key [Section titled “Phase 2: Start Signing With The New Key”](#phase-2-start-signing-with-the-new-key) Next, start signing tokens with the new key, but continue to publish the public key of the old key so that tokens that were signed with that key can continue to be validated. The IdentityServer configuration change needed is to swap the signing credential and validation key. Program.cs ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = false; }); var oldKey = LoadOldKeyFromVault(); var newKey = LoadNewKeyFromVault(); idsvrBuilder.AddSigningCredential(newKey, SecurityAlgorithms.RsaSha256); idsvrBuilder.AddValidationKey(oldKey, SecurityAlgorithms.RsaSha256) ``` Again, you need to wait to proceed to phase 3. The delay here is typically shorter, because the reason for the delay is to ensure that tokens signed with the old key remain valid until they expire. IdentityServer’s token lifetime defaults to 1 hour, though it is configurable. #### Phase 3: Remove The Old Key [Section titled “Phase 3: Remove The Old Key”](#phase-3-remove-the-old-key) Once enough time has passed that there are no unexpired tokens signed with the old key, it is safe to completely remove the old key. ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = false; }); var newKey = LoadNewKeyFromVault(); idsvrBuilder.AddSigningCredential(newKey, SecurityAlgorithms.RsaSha256); ``` ## Migrating From Static Keys To Automatic Key Management [Section titled “Migrating From Static Keys To Automatic Key Management”](#migrating-from-static-keys-to-automatic-key-management) To migrate from static to automatic key management, you can set keys manually and enable automatic key management at the same time. This allows the automatic key management feature to begin creating keys and announce them in discovery, while you continue to use the old statically configured key. Eventually you can transition from the statically configured key to the automatically managed keys. A signing key registered with `AddSigningCredential` will take precedence over any keys created by the automatic key management feature. IdentityServer will sign tokens with the credential specified in `AddSigningCredential`, but also automatically create and manage validation keys. Validation keys registered manually with `AddValidationKey` are added to the collection of validation keys along with the keys produced by automatic key management. When automatic key management is enabled and there are keys statically specified with `AddValidationkey`, the set of validation keys will include: * new keys created by automatic key management that are not yet used for signing * old keys created by automatic key management that are retired * the keys added explicitly with calls to `AddValidationKey`. The migration path from manual to automatic keys is a three-phase process, similar to the phased approach to [manual key rotation](#manual-key-rotation). The difference here is that you are phasing out the old key and allowing the automatically generated keys to phase in. ### Phase 1: Announce New (Automatic) Key [Section titled “Phase 1: Announce New (Automatic) Key”](#phase-1-announce-new-automatic-key) First, enable automatic key management while continuing to register your old key as the signing credential. In this phase, the new automatically managed key will be announced so that as client apps and APIs update their caches, they get the new key. IdentityServer will continue to sign keys with your old static key. ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = true; }); var oldKey = LoadOldKeyFromVault(); idsvrBuilder.AddSigningCredential(oldKey, SecurityAlgorithms.RsaSha256); ``` Wait until all APIs and applications have updated their signing key caches, and then proceed to phase 2. ### Phase 2: Start Signing With The New (Automatic) Key [Section titled “Phase 2: Start Signing With The New (Automatic) Key”](#phase-2-start-signing-with-the-new-automatic-key) Next, switch to using the new automatically managed keys for signing, but still keep the old key for validation purposes. ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = true; }); var oldKey = LoadOldKeyFromVault(); idsvrBuilder.AddValidationKey(oldKey, SecurityAlgorithms.RsaSha256); ``` Keep the old key as a validation key until all tokens signed with that key are expired, and then proceed to phase 3. ### Phase 3: Drop the old key [Section titled “Phase 3: Drop the old key”](#phase-3-drop-the-old-key) Now the static key configuration can be removed entirely. ```csharp var idsvrBuilder = builder.Services.AddIdentityServer(options => { options.KeyManagement.Enabled = true; }); ``` ----- # ASP.NET Core OpenID Connect Handler Events > ASP.NET Core's OpenID Connect handler events, what they are, and why you might want to use them. The ASP.NET Core [OpenID Connect handler](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnecthandler?view=aspnetcore-9.0) exposes events that a client can subscribe to intercept the OpenID Connect protocol flow. Understanding these events is important to understanding how to customize the OpenID Connect protocol flow from the client. We’ll cover each of the events, what they are, and why you might want to subscribe to them. To use the `OpenIdConnectHandler` in your client applications, you will first need to install the `Microsoft.AspNetCore.Authentication.OpenIdConnect` NuGet package. ```bash dotnet package add Microsoft.AspNetCore.Authentication.OpenIdConnect ``` Followed by adding the `OpenIdConnectHandler` to your application. Program.cs ```csharp builder.Services.AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { options.Cookie.Name = "__Host-bff"; options.Cookie.SameSite = SameSiteMode.Strict; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.MapInboundClaims = false; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }); ``` From here you can use the `options.Events` property to subscribe to the events you want to use. Let’s look at each of the events in more detail. ## OpenID Connect Events [Section titled “OpenID Connect Events”](#openid-connect-events) All events either occur before a request is sent to the identity provider, or after a response is received from the identity provider. Understanding the direction of these events can help you determine when to subscribe to them. Let’s call events coming from the identity provider **incoming** and events going to the identity provider **outgoing** for an easier understanding. | **Event Name** | **Usage** | | ---------------------------------------- | ------------ | | `OnAuthenticationFailed` | **Incoming** | | `OnAuthorizationCodeReceived` | **Incoming** | | `OnMessageReceived` | **Incoming** | | `OnRedirectToIdentityProvider` | **Outgoing** | | `OnRedirectToIdentityProviderForSignOut` | **Outgoing** | | `OnSignedOutCallbackRedirect` | **Outgoing** | | `OnRemoteSignOut` | **Incoming** | | `OnTokenResponseReceived` | **Incoming** | | `OnTokenValidated` | **Incoming** | | `OnUserInformationReceived` | **Incoming** | | `OnTicketReceived` | **Incoming** | | `OnPushAuthorization` (**.NET 9+ only**) | **Outgoing** | ## Commonly Subscribed Events [Section titled “Commonly Subscribed Events”](#commonly-subscribed-events) While there are many events available in the `OpenIdConnectEvents` class, only a few are commonly subscribed. We suggest you start with the most commonly subscribed events and then subscribe to the remaining events as needed. For ASP.NET Core developers, the most commonly subscribed events are: 1. **`OnRedirectToIdentityProvider`**: Useful for customizing login requests (e.g., appending extra parameters). 2. **`OnRedirectToIdentityProviderForSignOut`**: Often required to customize the behavior of sign-out requests. 3. **`OnTokenValidated`**: Frequently used to customize the claims processing or validate custom claims included in the ID token. 4. **`OnUserInformationReceived`**: Sometimes used to process additional user data retrieved from the UserInfo endpoint (if enabled). ## Descriptions [Section titled “Descriptions”](#descriptions) ### OnAuthenticationFailed [Section titled “OnAuthenticationFailed”](#onauthenticationfailed) * **When called**: Triggered whenever an exception occurs during the authentication process. This event provides an opportunity to handle or log errors. * **How often**: Only called when an authentication error happens. * **Example use case**: Use this event to log detailed error messages or display a custom error page to the user instead of the default behavior. * **Commonly subscribed**: No, unless you need specific error-handling logic. ### OnAuthorizationCodeReceived [Section titled “OnAuthorizationCodeReceived”](#onauthorizationcodereceived) * **When called**: Invoked after an authorization code is received and before it is redeemed for tokens. * **How often**: Called once per successful authorization code flow request. * **Example use case**: Validate the authorization code or add extra functionality (e.g., logging or monitoring) when the code is received. * **Commonly subscribed**: Rarely, unless custom logic is required before token redemption. ### OnMessageReceived [Section titled “OnMessageReceived”](#onmessagereceived) * **When called**: Triggered when a protocol message (e.g., an authorization response, logout request) is first received. * **How often**: Called once per incoming protocol message. * **Example use case**: Inspect or modify protocol messages for debugging or to handle additional query parameters passed by the identity provider. * **Commonly subscribed**: No, unless advanced customization is needed. ### OnRedirectToIdentityProvider [Section titled “OnRedirectToIdentityProvider”](#onredirecttoidentityprovider) * **When called**: Invoked when redirecting the user to the identity provider for authentication. You can modify the outgoing authentication request. * **How often**: Called once per user authentication attempt (e.g., a “login”). * **Example use case**: Add custom query parameters to the request or modify the state parameter. * **Commonly subscribed**: Yes—often used to customize the authentication request. ### OnRedirectToIdentityProviderForSignOut [Section titled “OnRedirectToIdentityProviderForSignOut”](#onredirecttoidentityproviderforsignout) * **When called**: Triggered before redirecting the user to the identity provider to start the sign-out process. * **How often**: Called once per user sign-out request. * **Example use case**: Modify the logout request, such as appending additional parameters. * **Commonly subscribed**: Yes, if signing out requires customization. ### OnSignedOutCallbackRedirect [Section titled “OnSignedOutCallbackRedirect”](#onsignedoutcallbackredirect) * **When called**: Invoked after a remote sign-out is completed and before redirecting the user to the `SignedOutRedirectUri`. * **How often**: Called once per remote sign-out. * **Example use case**: Log or perform business logic after the remote sign-out. * **Commonly subscribed**: Rarely, unless additional behavior is needed. ### OnRemoteSignOut [Section titled “OnRemoteSignOut”](#onremotesignout) * **When called**: Called when a remote sign-out request is received on the `RemoteSignOutPath` endpoint. * **How often**: Called once per incoming remote sign-out request. * **Example use case**: Perform cleanup tasks such as clearing local session data upon receiving a sign-out request from the identity provider. * **Commonly subscribed**: Rarely, but important in distributed or multi-tenant systems. ### OnTokenResponseReceived [Section titled “OnTokenResponseReceived”](#ontokenresponsereceived) * **When called**: Triggered after an authorization code exchange is completed and the token endpoint returns tokens. * **How often**: Called once per token request. * **Example use case**: Log or debug the token response, or inspect additional data included in the token response. * **Commonly subscribed**: No, unless debugging or inspection of tokens is required. ### OnTokenValidated [Section titled “OnTokenValidated”](#ontokenvalidated) * **When called**: Invoked after the ID token has been validated and an `AuthenticationTicket` has been created. * **How often**: Called once per token validation process. * **Example use case**: Add or modify claims in the `ClaimsPrincipal` or validate custom claims included in the token. * **Commonly subscribed**: Yes—this is one of the most commonly used events for customizing claims. ### OnUserInformationReceived [Section titled “OnUserInformationReceived”](#onuserinformationreceived) * **When called**: Triggered when retrieving user information from the UserInfo endpoint (if `GetClaimsFromUserInfoEndpoint = true`). * **How often**: Called once per user information fetch (e.g., per login). * **Example use case**: Extend or modify user claims based on the additional information retrieved from the UserInfo endpoint. * **Commonly subscribed**: Sometimes, if extra claims processing is required. ### OnTicketReceived [Section titled “OnTicketReceived”](#onticketreceived) * **When called**: Invoked after the OpenID Connect authentication flow is complete and before the authentication ticket is returned. * **How often**: Called once per successful authentication flow completion. * **Example use case**: Modify the final authentication ticket, perform additional validation, or execute custom logic before completing the authentication process. * **Commonly subscribed**: Sometimes, when final authentication customization is needed before completing the flow or for diagnostics and troubleshooting purposes. ### OnPushAuthorization [Section titled “OnPushAuthorization”](#onpushauthorization) * **When called**: Invoked before sending authorization parameters using the Pushed Authorization Request (PAR) mechanism. * **How often**: Called once per outgoing PAR-based authorization request. * **Example use case**: Modify or log pushed authorization parameters. * **Commonly subscribed**: Rarely, as this is used mainly in advanced scenarios. ----- # Resources > Overview of resource types in Duende IdentityServer including API resources, identity resources, API scopes, and resource isolation concepts The ultimate job of Duende IdentityServer is to control access to resources. ## API Resources [Section titled “API Resources”](#api-resources) In Duende IdentityServer, the *ApiResource* class allows for some additional organization and grouping and isolation of scopes and providing some common settings. [Read More](/identityserver/fundamentals/resources/api-resources/) ## Identity Resources [Section titled “Identity Resources”](#identity-resources) An identity resource is a named group of claims about a user that can be requested using the *scope* parameter. The OpenID Connect specification [suggests](https://openid.net/specs/openid-connect-core-1_0.html#scopeclaims) a couple of standard scope name to claim type mappings that might be useful to you for inspiration, but you can freely design them yourself. [Read More](/identityserver/fundamentals/resources/identity/) ## API Scopes [Section titled “API Scopes”](#api-scopes) Designing your API surface can be a complicated task. Duende IdentityServer provides a couple of primitives to help you with that. The original OAuth 2.0 specification has the concept of scopes, which is just defined as *the scope of access* that the client requests. Technically speaking, the *scope* parameter is a list of space delimited values - you need to provide the structure and semantics of it. In more complex systems, often the notion of a *resource* is introduced. This might be e.g. a physical or logical API. In turn each API can potentially have scopes as well. Some scopes might be exclusive to that resource, and some scopes might be shared. [Read More](/identityserver/fundamentals/resources/api-scopes/) ## Resources Isolation [Section titled “Resources Isolation”](#resources-isolation) OAuth itself only knows about scopes - the (API) resource concept does not exist from a pure protocol point of view. This means that all the requested scope and audience combination get merged into a single access token. This has a couple of downsides, e.g. * tokens can become very powerful (and big) * if such a token leaks, it allows access to multiple resources * resources within that single token might have conflicting settings, e.g. * user claims of all resources share the same token * resource specific processing like signing or encryption algorithms conflict * without sender-constraints, a resource could potentially re-use (or abuse) a token to call another contained resource directly To solve this problem [RFC 8707](https://tools.ietf.org/html/rfc8707) adds another request parameter for the authorize and token endpoint called *resource*. This allows requesting a token for a specific resource (in other words - making sure the audience claim has a single value only, and all scopes belong to that single resource). [Read More](/identityserver/fundamentals/resources/isolation/) ----- # API Resources > Learn how API Resources in Duende IdentityServer help organize and group scopes, manage token claims, and control access token properties When the API/resource surface gets larger, a flat list of scopes might become hard to manage. In Duende IdentityServer, the `ApiResource` class allows for some additional organization and grouping and isolation of scopes and providing some common settings. Let’s use the following scope definition as an example: ```csharp public static IEnumerable GetApiScopes() { return new List { // invoice API specific scopes new ApiScope(name: "invoice.read", displayName: "Reads your invoices."), new ApiScope(name: "invoice.pay", displayName: "Pays your invoices."), // customer API specific scopes new ApiScope(name: "customer.read", displayName: "Reads you customers information."), new ApiScope(name: "customer.contact", displayName: "Allows contacting one of your customers."), // shared scopes new ApiScope(name: "manage", displayName: "Provides administrative access."), new ApiScope(name: "enumerate", displayName: "Allows enumerating data.") }; } ``` With `ApiResource` you can now create two logical APIs and their corresponding scopes: ```csharp public static readonly IEnumerable GetApiResources() { return new List { new ApiResource("invoice", "Invoice API") { Scopes = { "invoice.read", "invoice.pay", "manage", "enumerate" } }, new ApiResource("customer", "Customer API") { Scopes = { "customer.read", "customer.contact", "manage", "enumerate" } } }; } ``` Using the API resource grouping gives you the following additional features * support for the JWT `aud` claim. The value(s) of the audience claim will be the name of the API resource(s) * support for adding common user claims across all contained scopes * support for introspection by assigning an API secret to the resource * support for configuring the access token signing algorithm for the resource Let’s have a look at some example access tokens for the above resource configuration. Client requests: *`invoice.read`* and *`invoice.pay`*: ```json { "typ": "at+jwt" }. { "client_id": "client", "sub": "123", "aud": "invoice", "scope": "invoice.read invoice.pay" } ``` Client requests: *`invoice.read`* and *`customer.read`*: ```json { "typ": "at+jwt" }. { "client_id": "client", "sub": "123", "aud": [ "invoice", "customer" ], "scope": "invoice.read customer.read" } ``` Client requests: *`manage`*: ```json { "typ": "at+jwt" }. { "client_id": "client", "sub": "123", "aud": [ "invoice", "customer" ], "scope": "manage" } ``` ### Adding User Claims [Section titled “Adding User Claims”](#adding-user-claims) You can specify that an access token for an API resource (regardless of which scope is requested) should contain additional user claims. ```csharp var customerResource = new ApiResource("customer", "Customer API") { Scopes = { "customer.read", "customer.contact", "manage", "enumerate" }, // additional claims to put into access token UserClaims = { "department_id", "sales_region" } } ``` If a client now requested a scope belonging to the `customer` resource, the access token would contain the additional claims (if provided by your [profile service](/identityserver/reference/v8/services/profile-service/)). ```json { "typ": "at+jwt" }. { "client_id": "client", "sub": "123", "aud": [ "invoice", "customer" ], "scope": "invoice.read customer.read", "department_id": 5, "sales_region": "south" } ``` ### Setting A Signing Algorithm [Section titled “Setting A Signing Algorithm”](#setting-a-signing-algorithm) Your APIs might have certain requirements for the cryptographic algorithm used to sign the access tokens for that resource. An example could be regulatory requirements, or that you are starting to migrate your system to higher security algorithms. The following sample sets `PS256` as the required signing algorithm for the `invoices` API: ```csharp var invoiceApi = new ApiResource("invoice", "Invoice API") { Scopes = { "invoice.read", "invoice.pay", "manage", "enumerate" }, AllowedAccessTokenSigningAlgorithms = { SecurityAlgorithms.RsaSsaPssSha256 } } ``` Note Make sure that you have configured your IdentityServer for the required signing algorithm. See [here](/identityserver/fundamentals/key-management/) for more details. ### Resource Isolation [Section titled “Resource Isolation”](#resource-isolation) See [Resource Isolation](/identityserver/fundamentals/resources/isolation/) for more details on how to use the `resource` parameter to request a token with scopes for a specific resource. ----- # API Scopes > Learn about API scopes in IdentityServer, how to define and use them for access control, and how they work with OAuth 2.0 Designing your API surface can be a complicated task. Duende IdentityServer provides a couple of primitives to help you with that. The original OAuth 2.0 specification has the concept of scopes, which is just defined as *the scope of access* that the client requests. Technically speaking, the `scope` parameter is a list of space delimited values - you need to provide the structure and semantics of it. In more complex systems, often the notion of a `resource` is introduced. This might be e.g. a physical or logical API. In turn each API can potentially have scopes as well. Some scopes might be exclusive to that resource, and some scopes might be shared. Let’s start with simple scopes first, and then we’ll have a look how resources can help structure scopes. ### Scopes [Section titled “Scopes”](#scopes) Let’s model something very simple - a system that has three logical operations `read`, `write`, and `delete`. You can define them using the `ApiScope` class: ```csharp public static IEnumerable GetApiScopes() { return new List { new ApiScope(name: "read", displayName: "Read your data."), new ApiScope(name: "write", displayName: "Write your data."), new ApiScope(name: "delete", displayName: "Delete your data.") }; } ``` You can then assign the scopes to various clients, e.g.: ```csharp var webViewer = new Client { ClientId = "web_viewer", AllowedScopes = { "openid", "profile", "read" } }; var mobileApp = new Client { ClientId = "mobile_app", AllowedScopes = { "openid", "profile", "read", "write", "delete" } } ``` ### Authorization Based On Scopes [Section titled “Authorization Based On Scopes”](#authorization-based-on-scopes) When a client asks for a scope (and that scope is allowed via configuration and not denied via consent), the value of that scope will be included in the resulting access token as a claim of type `scope` (for both JWTs and introspection), e.g.: ```json { "typ": "at+jwt" }. { "client_id": "mobile_app", "sub": "123", "scope": "read write delete" } ``` Note The format of the `scope` parameter can be controlled by the `EmitScopesAsSpaceDelimitedStringInJwt` setting on the options. Historically IdentityServer emitted scopes as an array, but you can switch to a space delimited string instead. The consumer of the access token can use that data to make sure that the client is actually allowed to invoke the corresponding functionality. See the [APIs](/identityserver/apis) section for more information on protecting APIs with access tokens. Caution Be aware, that scopes are purely for authorizing clients, not users. In other words, the `write` scope allows the client to invoke the functionality associated with the scope and is unrelated to the user’s permission to do so. This additional user-centric authorization is application logic and not covered by OAuth, yet still possibly important to implement in your API. ### Adding User Claims [Section titled “Adding User Claims”](#adding-user-claims) You can add more identity information about the user to the access token. The additional claims added are based on the scope requested. The following scope definition tells the configuration system that when a `write` scope gets granted the `user_level` claim should be added to the access token: ```csharp var writeScope = new ApiScope( name: "write", displayName: "Write your data.", userClaims: new[] { "user_level" }); ``` This will pass the `user_level` claim as a requested claim type to the profile service, so that the consumer of the access token can use this data as input for authorization decisions or business logic. Note When using the scope-only model, no aud (audience) claim will be added to the token since this concept does not apply. If you need an aud claim, you can enable the `EmitStaticAudienceClaim` setting on the options. This will emit an aud claim in the `issuer_name/resources` format. If you need more control of the aud claim, use API resources. ### Parameterized Scopes [Section titled “Parameterized Scopes”](#parameterized-scopes) Sometimes scopes have a certain structure, e.g. a scope name with an additional parameter: `transaction:id` or `read_patient:patientid`. This pattern is useful when: * **Transaction-scoped access** - Tokens bound to a specific transaction, order, or workflow * **Multi-tenant systems** - Scopes that encode tenant context like `tenant:acme:read` * **Resource-specific permissions** - Access to a particular document, patient record, or account In this case you would create a scope without the parameter part and assign that name to a client, but in addition provide some logic to parse the structure of the scope at runtime using the `IScopeParser` interface or by deriving from our default implementation. #### Implementing a Scope Parser [Section titled “Implementing a Scope Parser”](#implementing-a-scope-parser) Create a custom parser by extending `DefaultScopeParser`: ```csharp public class ParameterizedScopeParser : DefaultScopeParser { public ParameterizedScopeParser(ILogger logger) : base(logger) { } public override void ParseScopeValue(ParseScopeContext scopeContext) { const string transactionScopeName = "transaction"; const string separator = ":"; const string transactionScopePrefix = transactionScopeName + separator; var scopeValue = scopeContext.RawValue; if (scopeValue.StartsWith(transactionScopePrefix)) { // we get in here with a scope like "transaction:something" var parts = scopeValue.Split(separator, StringSplitOptions.RemoveEmptyEntries); if (parts.Length == 2) { scopeContext.SetParsedValues(transactionScopeName, parts[1]); } else { scopeContext.SetError("transaction scope missing transaction parameter value"); } } else if (scopeValue != transactionScopeName) { // we get in here with a scope not like "transaction" base.ParseScopeValue(scopeContext); } else { // we get in here with a scope exactly "transaction", which is to say we're ignoring it // and not including it in the results scopeContext.SetIgnore(); } } } ``` Register your parser in `ConfigureServices`: ```csharp builder.Services.AddIdentityServer() .AddScopeParser(); ``` #### Accessing Parsed Values [Section titled “Accessing Parsed Values”](#accessing-parsed-values) You then have access to the parsed value throughout the pipeline, e.g. in the profile service: ```csharp public class HostProfileService : IProfileService { public override async Task GetProfileDataAsync(ProfileDataRequestContext context) { var transaction = context.RequestedResources.ParsedScopes.FirstOrDefault(x => x.ParsedName == "transaction"); if (transaction?.ParsedParameter != null) { context.IssuedClaims.Add(new Claim("transaction_id", transaction.ParsedParameter)); } } } ``` #### Validating Dynamic Scopes [Section titled “Validating Dynamic Scopes”](#validating-dynamic-scopes) When using parameterized scopes, you may also want to customize how scopes are validated. For example, you might verify that a transaction ID exists, or that the client is authorized for a specific tenant. Use `IResourceValidator` to add this validation logic. [Scope Parser Reference](/identityserver/reference/v8/parsers/scope-parser/)Full API reference for IScopeParser with additional scenarios [Resource Validator Reference](/identityserver/reference/v8/validators/resource-validator/)Validate parameterized scopes against external systems ----- # Identity Resources > Learn about identity resources in Duende IdentityServer - named groups of claims about users that can be requested using scopes An identity resource is a named group of claims about a user that can be requested using the `scope` parameter. The OpenID Connect specification [suggests](https://openid.net/specs/openid-connect-core-1_0.html#scopeclaims) a couple of standard scope name to claim type mappings that might be useful to you for inspiration, but you can freely design them yourself. One of them is actually mandatory, the `openid` scope, which tells the provider to return the `sub` (subject id) claim in the identity token. This is how you could define the openid scope in code: ```csharp public static IEnumerable GetIdentityResources() { return new List { new IdentityResource( name: "openid", userClaims: new[] { "sub" }, displayName: "Your user identifier") }; } ``` But since this is one of the standard scopes from the spec you can shorten that to: ```csharp public static IEnumerable GetIdentityResources() { return new List { new IdentityResources.OpenId() }; } ``` Note See the [reference](/identityserver/reference/v8/models/identity-resource/) section for more information on `IdentityResource`. The following example shows a custom identity resource called `profile` that represents the display name, email address and website claim: ```csharp public static IEnumerable GetIdentityResources() { return new List { new IdentityResource( name: "profile", userClaims: new[] { "name", "email", "website" }, displayName: "Your profile data") }; } ``` Once the resource is defined, you can give access to it to a client via the `AllowedScopes` option (other properties omitted): ```csharp var client = new Client { ClientId = "client", AllowedScopes = { "openid", "profile" } }; ``` Note See the [reference](/identityserver/reference/v8/models/client/) section for more information on the `Client` class. The client can then request the resource using the scope parameter (other parameters omitted): ```plaintext https://demo.duendesoftware.com/connect/authorize?client_id=client&scope=openid profile ``` IdentityServer will then use the scope names to create a list of requested claim types, and present that to your implementation of the [profile service](/identityserver/reference/v8/services/profile-service/). ----- # Overview > Learn about isolating OAuth resources and using the resource parameter to control access token scope and audience Note This feature is part of the [Duende IdentityServer Enterprise (legacy), Standard, Advanced, and Custom Edition](https://duendesoftware.com/products/identityserver). OAuth itself only knows about scopes - the (API) resource concept does not exist from a pure protocol point of view. This means that all the requested scope and audience combinations get merged into a single access token. This has a couple of downsides: * Tokens can become very powerful (and large) * If such a token leaks, it allows access to multiple resources * Resources within that single token might have conflicting settings, e.g. * User claims of all resources share the same token * Resource-specific processing like signing or encryption algorithms conflict * Without sender-constraints, a resource could potentially re-use (or abuse) a token to call another contained resource directly ### Audience Ambiguity [Section titled “Audience Ambiguity”](#audience-ambiguity) In a system with multiple APIs (e.g., Shipping, Invoicing and Inventory APIs), a single token often lists all of them as valid audiences. ```json { "iss": "https://demo.duendesoftware.com", "aud": ["invoice_api", "shipping_api", "inventory_api"], "scope": ["invoice.read", "shipping.write", "inventory.read"] } ``` This violates the Principle of Least Privilege. If this token is leaked from the Inventory API, it can be used to call the Invoice API. To solve this problem [RFC 8707](https://tools.ietf.org/html/rfc8707) adds another request parameter for the authorize and token endpoint called `resource`. This allows requesting a token for a specific resource (in other words - making sure the audience claim has a single value only, and all scopes belong to that single resource). ## Using The Resource Parameter [Section titled “Using The Resource Parameter”](#using-the-resource-parameter) Let’s assume you have the following resource design and that the client is allowed access to all scopes: ApiResources.cs ```csharp var resources = new[] { new ApiResource("urn:invoices") { Scopes = { "read", "write" } }, new ApiResource("urn:products") { Scopes = { "read", "write" } } }; ``` If the client would request a token for the `read` scope, the resulting access token would contain the audience of both the invoice and the products API and thus be accepted at both APIs. ### Machine to Machine Scenarios [Section titled “Machine to Machine Scenarios”](#machine-to-machine-scenarios) If the client in addition passes the `resource` parameter specifying the name of the resource where it wants to use the access token, the token engine can `down-scope` the resulting access token to the single resource, e.g.: ```text POST /token grant_type=client_credentials& client_id=client& client_secret=...& scope=read& resource=urn:invoices ``` Thus resulting in an access token like this (some details omitted): ```json { "aud": ["urn:invoice"], "scope": "read", "client_id": "client" } ``` ### Interactive Applications [Section titled “Interactive Applications”](#interactive-applications) The authorize endpoint supports the `resource` parameter as well, e.g.: ```text GET /authorize?client_id=client&response_type=code&scope=read&resource=urn:invoices ``` Once the front-channel operations are done, the resulting code can be redeemed by passing the resource name on the token endpoint: ```text POST /token grant_type=authorization_code& client_id=client& client_secret=...& authorization_code=...& redirect_uri=...& resource=urn:invoices ``` ### Requesting Access To Multiple Resources [Section titled “Requesting Access To Multiple Resources”](#requesting-access-to-multiple-resources) It is also possible to request access to multiple resources. This will result in multiple access tokens - one for each request resource. ```text GET /authorize?client_id=client&response_type=code&scope=read offline_access&resource=urn:invoices&resource=urn:products ``` When you redeem the code, you need to specify for which resource you want to have an access token, e.g.: ```text POST /token grant_type=authorization_code& client_id=client& client_secret=...& authorization_code=...& redirect_uri=...& resource=urn:invoices ``` This will return an access token for the invoices API and a refresh token. If you want to also retrieve the access token for the products API, you use the refresh token and make another roundtrip to the token endpoint. ```text POST /token grant_type=refresh_token& client_id=client& client_secret=...& refresh_token=...& resource=urn:products ``` The end-result will be that the client has two access tokens - one for each resource and can manage their lifetime via the refresh token. ## Enforcing Resource Isolation [Section titled “Enforcing Resource Isolation”](#enforcing-resource-isolation) All examples so far used the `resource` parameter optionally. If you have API resources, where you want to make sure they are not sharing access tokens with other resources, you can enforce the resource indicator, e.g.: ApiResources.cs ```csharp var resources = new[] { new ApiResource("urn:invoices") { Scopes = { "read", "write" }, RequireResourceIndicator = true }, new ApiResource("urn:products") { Scopes = { "read", "write" }, RequireResourceIndicator = true } }; ``` The `RequireResourceIndicator` property **does not** mean that clients are forced to send the `resource` parameter when they request scopes associated with the API resource. You can still request those scopes without setting the `resource` parameter (or including the resource), and IdentityServer will issue a token as long as the client is allowed to request the scopes. Instead, `RequireResourceIndicator` controls **when** the resource’s URI is included in the **audience claim** (`aud`) of the issued access token. * When `RequireResourceIndicator` is `false` (the default): IdentityServer **automatically includes** the API’s resource URI in the token’s audience if any of the resource’s scopes are requested, even if the `resource` parameter was not sent in the request or didn’t contain the resource URI. * When `RequireResourceIndicator` is `true`: The API’s resource URI will **only** be included in the audience **if the client explicitly includes the resource URI** via the `resource` parameter when requesting the token. ## .NET Client Implementation [Section titled “.NET Client Implementation”](#net-client-implementation) While the examples above show the underlying HTTP protocol, .NET clients can use the Duende libraries to handle resource indicators easily. ### Machine-to-Machine (Worker) [Section titled “Machine-to-Machine (Worker)”](#machine-to-machine-worker) When using `Duende.IdentityModel` for client credentials, you can pass the `resource` parameter using the `Parameters` dictionary: ```csharp using Duende.IdentityModel.Client; var client = new HttpClient(); var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = "https://demo.duendesoftware.com/connect/token", ClientId = "invoice_worker", ClientSecret = "secret", // The scope defines the permission Scope = "invoice.read", // The parameter defines the target (RFC 8707) Resource = [ "urn:invoices" ] }); ``` ### ASP.NET Core [Section titled “ASP.NET Core”](#aspnet-core) For interactive applications using the standard OpenID Connect handler, use the `Resource` property on `OpenIdConnectOptions`: ```csharp .AddOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive_app"; options.Scope.Add("invoice.read"); // Explicitly set the target resource here options.Resource = "urn:invoices"; options.ResponseType = "code"; options.SaveTokens = true; }); ``` Note that while the RFC allows multiple `resource` parameters, the Microsoft OpenID Connect handler only supports a single resource value here. For dynamic scenarios (e.g. multi-tenant), you can set the resource parameter in the `OnRedirectToIdentityProvider` event: ```csharp options.Events.OnRedirectToIdentityProvider = context => { var tenantSpecificResource = DetermineResource(context); // Overwrite or set the 'resource' parameter context.ProtocolMessage.SetParameter("resource", tenantSpecificResource); return Task.CompletedTask; }; ``` ----- # Isolation Sample > Learn about isolating OAuth resources and using the resource parameter to control access token scope and audience Imagine a set of services with separate APIs for handling orders and tracking inventory, an Orders API and Inventory API. Each has their own distinct set of API scopes, plus a set of scopes shared between the APIs. In addition, there’s a global scope used by legacy systems that haven’t been updated yet to use Resource Isolation. The set of scopes used by each application are: | urn:orders | urn:inventory | Not Shared with any API Resource | | ------------ | --------------- | -------------------------------- | | orders.read | inventory.read | global.audit | | orders.write | inventory.write | | | shared.read | shared.read | | The below code creates in-memory scopes, API resources, and a single client (which knows about the aforementioned resources) inside a Duende IdentityServer application. Notice that all scopes are created in a single `Scopes` collection, then the `Resources` collection groups the scopes per `ApiResource`. Finally, the `Client` includes all scopes in its `AllowedScopes` property because the client will be requesting any combination of those scopes from Duende IdentityServer. The only grouping happening is when the `ApiResource` objects link an API resource to a scope. Config.cs ```csharp // All scopes used by all API Resources and Clients public static readonly IEnumerable Scopes = [ // resource specific scopes new ApiScope("orders.read"), new ApiScope("orders.write"), new ApiScope("inventory.read"), new ApiScope("inventory.write"), // a scope shared by multiple resources new ApiScope("shared.read"), // scopes without resource association new ApiScope("global.audit"), ]; // API resources with the scopes they use public static readonly IEnumerable Resources = [ new ApiResource("urn:orders", "Orders API") { Scopes = { "orders.read", "orders.write", "shared.read" } }, new ApiResource("urn:inventory", "Inventory API") { Scopes = { "inventory.read", "inventory.write", "shared.read" }, RequireResourceIndicator = true } ]; public static readonly IEnumerable Clients = [ new Client { ClientId = "resource.isolation.demo.client", ClientSecrets = { new Secret("my-secret".Sha256()) }, ClientClaimsPrefix = "", AllowedGrantTypes = GrantTypes.ClientCredentials, // Client is allowed to access all scopes for all ApiResources AllowedScopes = { "orders.read", "orders.write", "inventory.read", "inventory.write", "shared.read", "global.audit", } } ]; ``` When requesting an `ApiResource`, IdentityServer will create a token with scopes filtered to what is supported by that `ApiResource`. Scopes are not owned by any individual `ApiResource`, and are global across your applications because internally they’re an arbitrary string. An `ApiResource` doesn’t “own” scopes, it is allowed access to those scopes. The table below shows the resulting **audience claim** (`aud`) when making requests for a token with a specific scope/resource combination. | Scopes | Resource Api | Result **audience claim** (`aud`) | | ------------------------ | ------------- | --------------------------------- | | orders.read | null | urn:orders | | inventory.read | null | NOT SET | | inventory.read | urn:inventory | urn:inventory | | orders.read global.audit | null | urn:orders | | shared.read | null | urn:orders | | orders.read shared.read | null | urn:orders | ## Experimenting with a Code Sample [Section titled “Experimenting with a Code Sample”](#experimenting-with-a-code-sample) The code for the above scenario is written out in the two tabs below. Each tab is a [C# file-based app](https://devblogs.microsoft.com/dotnet/announcing-dotnet-run-app/). One is a Duende IdentityServer application with scopes, API resources, and a client. The second app is a console client that makes requests to Duende IdentityServer, each request with different combinations of scopes and resources to show the result `aud` claim. To help understand how resource isolation works, feel free to run the two apps locally and make modifications as you see fit to experiment. * Duende IdentityServer IdentityServer.cs ```csharp // Run with `dotnet run IdentityServer.cs` #:sdk Microsoft.Net.Sdk.Web #:property PublishAot=false #:package Duende.IdentityServer@8.0.0 using Duende.IdentityServer.Models; var builder = WebApplication.CreateBuilder(args); builder.WebHost.UseUrls("https://localhost:5001"); _ = builder.Services.AddIdentityServer(options => { // emits static audience if required options.EmitStaticAudienceClaim = false; // control format of scope claim options.EmitScopesAsSpaceDelimitedStringInJwt = true; }) .AddInMemoryApiScopes(InMemoryConfig.Scopes) .AddInMemoryApiResources(InMemoryConfig.Resources) .AddInMemoryClients(InMemoryConfig.Clients); var app = builder.Build(); app.UseIdentityServer(); app.Run(); public static class InMemoryConfig { // All scopes used by all API Resources and Clients public static readonly IEnumerable Scopes = [ // resource specific scopes new ApiScope("orders.read"), new ApiScope("orders.write"), new ApiScope("inventory.read"), new ApiScope("inventory.write"), // a scope shared by multiple resources new ApiScope("shared.read"), // scopes without resource association new ApiScope("global.audit"), ]; // API resources with the scopes they use public static readonly IEnumerable Resources = [ new ApiResource("urn:orders", "Orders API") { Scopes = { "orders.read", "orders.write", "shared.read" } }, new ApiResource("urn:inventory", "Inventory API") { Scopes = { "inventory.read", "inventory.write", "shared.read" }, RequireResourceIndicator = true }, ]; public static readonly IEnumerable Clients = [ new Client { ClientId = "resource.isolation.demo.client", ClientSecrets = { new Secret("my-secret".Sha256()) }, ClientClaimsPrefix = "", AllowedGrantTypes = GrantTypes.ClientCredentials, // Client is allowed to access all scopes for all ApiResources AllowedScopes = { "orders.read", "orders.write", "inventory.read", "inventory.write", "shared.read", "global.audit", } } ]; } ``` * Client ResourceIsolationClient.cs ```csharp // Run with `dotnet run ResourceIsolationClient.cs` #:property PublishAot=false // Choose your access package library // #:package Duende.IdentityModel@8.1.0 #:package Duende.AccessTokenManagement@4.2.0 using System.Buffers.Text; using System.Text; using System.Text.Json; using Duende.IdentityModel.Client; var cache = new DiscoveryCache("https://localhost:5001"); Console.WriteLine("Access Token for scope `orders.read`"); await RequestToken(cache, scope: "orders.read", resource: null); Console.WriteLine(); Console.WriteLine("Access Token for scope `inventory.read`"); await RequestToken(cache, scope: "inventory.read", resource: null); Console.WriteLine(); Console.WriteLine("Access Token for scope `inventory.read` and resource `urn:inventory`"); await RequestToken(cache, scope: "inventory.read", resource: "urn:inventory"); Console.WriteLine(); Console.WriteLine("Access Token for scopes `orders.read global.audit`"); await RequestToken(cache, scope: "orders.read global.audit", resource: null); Console.WriteLine(); Console.WriteLine("Access Token for scope `shared.read`"); await RequestToken(cache, scope: "shared.read", resource: null); Console.WriteLine(); Console.WriteLine("Access Token for scopes `orders.read and shared.read`"); await RequestToken(cache, scope: "orders.read shared.read", resource: null); static async Task RequestToken(DiscoveryCache cache, string scope, string? resource) { var client = new HttpClient(); var disco = await cache.GetAsync(); var request = new ClientCredentialsTokenRequest { Address = disco.TokenEndpoint, ClientId = "resource.isolation.demo.client", ClientSecret = "my-secret", Scope = scope, }; if (!string.IsNullOrEmpty(resource)) { request.Resource.Add(resource); } var response = await client.RequestClientCredentialsTokenAsync(request); Show(response); } static void Show(TokenResponse response) { if (!response.IsError) { if (response.AccessToken?.Contains('.') is true) { var parts = response.AccessToken.Split('.'); var claims = parts[1]; var raw = Encoding.UTF8.GetString(Base64Url.DecodeFromChars(claims)); var doc = JsonDocument.Parse(raw).RootElement; var json = JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true }); Console.WriteLine(json); } else { Console.WriteLine($"Token response: {response.Json}"); } } else if (response.ErrorType == ResponseErrorType.Http) { Console.WriteLine($"HTTP error: {response.Error} with HTTP status code: {response.HttpStatusCode}"); } else { Console.WriteLine($"Protocol error response: {response.Raw}"); } } ``` The code above outputs the token response for each request to Duende IdentityServer. Below is that output, but modified to be in a table to simplify | Scopes | Resource Api | Result **audience claim** (`aud`) | | ------------------------ | ------------- | --------------------------------- | | orders.read | null | urn:orders | | inventory.read | null | NOT SET | | inventory.read | urn:inventory | urn:inventory | | orders.read global.audit | null | urn:orders | | shared.read | null | urn:orders | | orders.read shared.read | null | urn:orders | ----- # Users and Logging In > Overview of user management, authentication workflows, and UI customization options in Duende IdentityServer ## Users And User Interface [Section titled “Users And User Interface”](#users-and-user-interface) The design of Duende IdentityServer allows you to use any user database and build any user interface (UI) workflow needed to satisfy your requirements. This means you have the ability to customize any UI page (registration, login, password reset, etc.), support any credential type (password, MFA, etc.), use any user database (greenfield or legacy), and/or use federated logins from any provider (social or enterprise). You have the ability to control the entire user experience while Duende IdentityServer provides the implementation of the security protocol (OpenID Connect and OAuth). Note While you can use any custom user database or identity management library for your users, IdentityServer provides three ready-made options. See [Identity & Profile Management](/identityserver/identity/) for the full overview. * [Duende User Management](/identityserver/identity/user-management/) — first-party, passwordless-first user store with OTP, TOTP, passkeys, roles, and groups. Added in v8. * [ASP.NET Identity](/identityserver/identity/aspnet-identity/) — integration with the standard ASP.NET Core Identity stack. * [Custom via IProfileService](/identityserver/identity/custom/) — implement the interface yourself to connect to any user database or store. ## Authorization Endpoint And Login Page Workflow [Section titled “Authorization Endpoint And Login Page Workflow”](#authorization-endpoint-and-login-page-workflow) The standard mechanism to allow users to login is for the client application to use a web browser. This is obvious if the client application is a web application, but it’s also the recommended practice for native and mobile applications. When a user must log in, the client application will redirect the user to the protocol endpoint called the [authorization endpoint](/identityserver/reference/v8/endpoints/authorize/) in your IdentityServer server to request authentication. As part of the authorize request, your IdentityServer will typically display a login page for the user to enter their credentials. Once the user has authenticated, your IdentityServer will redirect the user back to the application with the protocol response. Note A user’s authentication session is managed using Microsoft’s ASP.NET [cookie authentication framework](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie). It is very important that you understand how it works when building the login pages in IdentityServer. Recall the diagram showing the relationship of your custom UI pages and the IdentityServer middleware in your IdentityServer host application: ``` --- title: ASP.NET Core Middleware Configuration --- flowchart LR login@{ icon: "material-symbols:login-rounded", label: "login", shape: icon } logout@{ icon: "material-symbols:logout-rounded", label: "logout", shape: icon } more@{ icon: "material-symbols:pending", label: "more...", shape: icon } authorize@{ icon: "material-symbols:verified-user-rounded", label: "authorize", shape: icon } token@{ icon: "material-symbols:key-rounded", label: "token", shape: icon } discovery@{ icon: "material-symbols:travel-explore-rounded", label: "discovery", shape: icon } subgraph ASPNET["ASP.NET Core Request Pipeline"] direction TB subgraph IS[" "] is_space@{ icon: "material-symbols:assured-workload-rounded", label: "IdentityServer Middleware", shape: icon } end subgraph YC[" "] yc_space@{ icon: "material-symbols:code-rounded", label: "Your Code", shape: icon } end end login --> YC logout --> YC more --> YC authorize --> IS token --> IS discovery --> IS style YC stroke:#74acfb,stroke-width:2px style IS stroke:#61fb92,stroke-width:2px ``` When your IdentityServer receives an authorize request, it will inspect it for a current authentication session for a user. This authentication session is based on ASP.NET Core’s authentication system and is ultimately determined by a cookie issued from your login page. If the user has never logged in there will be no cookie, and then the request to the authorize endpoint will result in a redirect to your login page. This is the entry point into your custom workflow that can take over to get the user logged in. ![sign in flow](/_astro/signin_flow.CJ2S0qPI_1YTpBz.svg) Once the login page has finished logging in the user with the ASP.NET Core authentication system, it will redirect the user back to the authorize endpoint. This time the request to the authorize endpoint will have an authenticated session for the user, and it can then create the protocol response and redirect to the client application. ## Additional Pages [Section titled “Additional Pages”](#additional-pages) In addition to the login page, there are other pages that Duende IdentityServer expects (e.g. logout, error, consent), and you could implement custom pages as well (e.g. register, forgot password, etc.). Details about building these pages, and coverage of additional topics are in the [User Interaction](/identityserver/ui) section of this documentation. ----- # Identity & Profile Management > Overview of the options for providing user identity and profile data to Duende IdentityServer, including User Management, ASP.NET Identity, and custom IProfileService implementations. Duende IdentityServer needs a source for user identity and profile data: the information used to authenticate users and populate claims in tokens. You choose which implementation backs this. The following options are available, presented from most to least full-featured: [User Management](/identityserver/identity/user-management/)First-party, passwordless-first user store supporting OTP, TOTP, passkeys, external providers, roles, groups, and full lifecycle management. [ASP.NET Identity](/identityserver/identity/aspnet-identity/)Integrate IdentityServer with ASP.NET Core Identity for established, well-known user management using the standard Microsoft identity stack. [Custom](/identityserver/identity/custom/)Implement IProfileService directly to connect IdentityServer to any user database or store. Also the extension point for customizing claims with any provider. ----- # ASP.NET Identity Integration > Guide to integrating ASP.NET Identity with IdentityServer for user management, including setup instructions and configuration options An ASP.NET Identity-based implementation is provided for managing the identity database for users of IdentityServer. This implementation implements the extensibility points in IdentityServer needed to load identity data for your users to emit claims into tokens. Duende User Management Added in v8 If you are starting a new project or want a more modern user management solution, consider [Duende User Management](/identityserver/identity/user-management/identityserver-integration/) as an alternative to ASP.NET Identity. It provides built-in support for passwordless authentication (OTP, passkeys), profile attribute management, and role-based authorization. To use the ASP.NET Identity-based implementation, ensure that you have the NuGet package for the ASP.NET Identity integration. It is called `Duende.IdentityServer.AspNetIdentity`: Terminal ```bash dotnet add package Duende.IdentityServer.AspNetIdentity ``` Next, configure ASP.NET Identity normally in your IdentityServer host with the standard calls to `AddIdentity` and any other related configuration. Then in your `Program.cs`, use the `AddAspNetIdentity` extension method after the call to `AddIdentityServer`: Program.cs ```csharp builder.Services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); builder.Services.AddIdentityServer() .AddAspNetIdentity(); ``` `AddAspNetIdentity` requires as a generic parameter the class that models your user for ASP.NET Identity (and the same one passed to `AddIdentity` to configure ASP.NET Identity). This configures IdentityServer to use the ASP.NET Identity implementations of [IUserClaimsPrincipalFactory](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.identity.iuserclaimsprincipalfactory-1) to convert the user data into claims, `IResourceOwnerPasswordValidator` to support the [password grant type](/identityserver/tokens/password-grant/), and `IProfileService`, which uses the `IUserClaimsPrincipalFactory` to add [claims](/identityserver/fundamentals/claims/) to tokens. It also configures some of ASP.NET Identity’s options for use with IdentityServer (such as claim types to use and authentication cookie settings). If you need to use your own implementation of `IUserClaimsPrincipalFactory`, then that is supported. Our implementation of the `IUserClaimsPrincipalFactory` will use the decorator pattern to encapsulate yours. For this to work correctly, ensure that your implementation is registered in the ASP.NET Core service provider before calling the IdentityServer `AddAspNetIdentity` extension method. The `IUserProfileService` interface has two methods that IdentityServer uses to interact with the user store. The profile service added for ASP.NET Identity implements `GetProfileDataAsync` by invoking the `IUserClaimsPrincipalFactory` implementation registered in the dependency injection container. The other method on `IProfileService` is `IsActiveAsync`, which is used in various places in IdentityServer to validate that the user is ( still) active. There is no built-in concept in ASP.NET Identity to inactive users, so our implementation is hard-coded to return `true`. If you extend the ASP.NET Identity user with enabled/disabled functionality, you should derive from our `ProfileService` and override `IsUserActiveAsync(TUser user)` to check your custom enabled/disabled flags. ## Template [Section titled “Template”](#template) You can use the `duende-is-aspid` [template](/identityserver/overview/packaging/#templates) to create a starter IdentityServer host project configured to use ASP.NET Identity. See the [Quickstart Documentation](/identityserver/quickstarts/5-aspnetid/) for a detailed walkthrough. ## User Management Pages [Section titled “User Management Pages”](#user-management-pages) The IdentityServer templates only include pages necessary for the authentication flow (login, logout, consent, error). User management pages, such as forgot password, password reset, or two-factor authentication setup, are not part of the IdentityServer templates because they are specific to your user store implementation. Since ASP.NET Core Identity provides built-in support for these features, you can add them to your IdentityServer host by [scaffolding Identity into your project](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/scaffold-identity). This gives you ready-made pages for password reset, email confirmation, two-factor authentication, and more, all integrated with the ASP.NET Core Identity user store you’ve already configured. ----- # Authentication Schemes and Cookies > Understanding the authentication schemes and cookies used by Duende IdentityServer, especially when integrated with ASP.NET Identity. Authentication in ASP.NET Core is organized into [authentication schemes](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/#authentication-scheme). A scheme is a name that corresponds to an authentication handler and its configuration options. IdentityServer relies on several specific schemes for different purposes, and understanding them is crucial, especially when integrating with ASP.NET Identity. ## Cookie Schemes [Section titled “Cookie Schemes”](#cookie-schemes) When a user logs in, their identity is established and persisted across requests using a cookie. IdentityServer uses a primary authentication cookie to track the user’s session. ### Standalone IdentityServer [Section titled “Standalone IdentityServer”](#standalone-identityserver) When using IdentityServer without ASP.NET Identity, the default cookie scheme is named `"idsrv"`, though we recommend using the constant `IdentityServerConstants.DefaultCookieAuthenticationScheme` in your code if you ever need it. The default cookie scheme is configured by default in `AddIdentityServer()`, which sets up the cookie authentication handler with this scheme name. This cookie is essential for: * maintaining the user’s authenticated session * supporting single sign-on (SSO) * managing sign-out ### With ASP.NET Identity [Section titled “With ASP.NET Identity”](#with-aspnet-identity) When you integrate ASP.NET Identity, for example using `AddAspNetIdentity()`, the configuration changes to align with ASP.NET Identity’s defaults. In this scenario, the main authentication cookie scheme is not `"idsrv"`. Instead, it uses the ASP.NET Identity default scheme name: `"Identity.Application"` (or the `IdentityConstants.ApplicationScheme` constant). This is a common point of confusion. ASP.NET Identity registers its own cookie handlers, and `AddAspNetIdentity` configures IdentityServer to use them. This means: 1. **Login UI:** When you call `HttpContext.SignInAsync`, you must use the correct scheme. If you use the `SignInManager` provided by ASP.NET Identity, it automatically uses `"Identity.Application"`. 2. **Configuration:** If you need to configure cookie options (like expiration or sliding expiration), you must configure the options for `"Identity.Application"`, not `"idsrv"`. Program.cs ```csharp services.ConfigureApplicationCookie(options => { // The default ("Identity.Application") options.Cookie.Name = IdentityConstants.ApplicationScheme; // Configure other options here... options.ExpireTimeSpan = TimeSpan.FromHours(1); options.SlidingExpiration = true; }); ``` ## Other Important Schemes [Section titled “Other Important Schemes”](#other-important-schemes) Besides the main application cookie, IdentityServer uses other schemes for specific features. ### External Authentication (e.g., Google, OIDC) [Section titled “External Authentication (e.g., Google, OIDC)”](#external-authentication-eg-google-oidc) When a user signs in with an external provider (like Google or another OIDC provider), the result of that remote authentication is temporarily stored in an “external” cookie. This allows your login logic to read the claims from the external provider before fully signing the user into your main local session. IdentityServer always uses the `"idsrv.external"` scheme here, available in the `IdentityServerConstants.ExternalCookieAuthenticationScheme` constant. ### Check Session Cookie [Section titled “Check Session Cookie”](#check-session-cookie) IdentityServer session management requires a separate cookie to monitor the session state without sending the large authentication cookie. The [User Session Service](/identityserver/reference/v8/services/user-session-service/) manages this cookie. * **Default Name:** `"idsrv.session"` (Constant: `IdentityServerConstants.DefaultCheckSessionCookieName`). Note this cookie is not marked as `HttpOnly`, so it can be accessed in client-side code. The JavaScript code that is required to check user sessions in the background also requires access to this cookie, and needs it to be `HttpOnly`. ## Common Pitfalls [Section titled “Common Pitfalls”](#common-pitfalls) * **Mixing Schemes:** Attempting to `SignOutAsync("idsrv")` when ASP.NET Identity is in use will have no effect on the actual `"Identity.Application"` cookie, leaving the user logged in. Always use the constants or the helper services (like `SignInManager`) that match your configuration. * **Cookie Configuration:** Setting options on the default authentication scheme (which might differ from the effective cookie scheme) or configuring the wrong named options instance will result in settings (like `Cookie.SameSite` or `ExpireTimeSpan`) being ignored. ----- # Custom Identity & Profile Management > Guide to implementing IProfileService in Duende IdentityServer to connect to any custom user database or store When neither [User Management](/identityserver/identity/user-management/) nor [ASP.NET Identity](/identityserver/identity/aspnet-identity/) fits your requirements, you can implement `IProfileService` directly. This interface is how IdentityServer connects to your user database or store to load user claims and determine whether a user is active. Implementing `IProfileService` yourself gives you full control over the data access code, letting you connect IdentityServer to any user store, whether a legacy database, an LDAP directory, an external API, or any other source of user data. ## The IProfileService Interface [Section titled “The IProfileService Interface”](#the-iprofileservice-interface) `IProfileService` has two methods: * **`GetProfileDataAsync`**: Called when IdentityServer needs to load claims for a user. You receive a `ProfileDataRequestContext` that contains the subject (the authenticated user), the requested claim types, and the client making the request. Populate `context.IssuedClaims` with the claims to include in the token. * **`IsActiveAsync`**: Called to determine whether a user is currently allowed to obtain tokens. Return `context.IsActive = false` to block token issuance for disabled or locked-out users. ```csharp public class MyProfileService : IProfileService { private readonly IUserRepository _users; public MyProfileService(IUserRepository users) { _users = users; } public async Task GetProfileDataAsync( ProfileDataRequestContext context, CancellationToken cancellationToken) { var user = await _users.FindByIdAsync( context.Subject.GetSubjectId(), cancellationToken); context.IssuedClaims.AddRange(new[] { new Claim(JwtClaimTypes.Name, user.DisplayName), new Claim(JwtClaimTypes.Email, user.Email), // add any other claims your application needs }); } public async Task IsActiveAsync( IsActiveContext context, CancellationToken cancellationToken) { var user = await _users.FindByIdAsync( context.Subject.GetSubjectId(), cancellationToken); context.IsActive = user != null && user.IsEnabled; } } ``` Register your implementation in `Program.cs`: ```csharp builder.Services.AddIdentityServer() .AddProfileService(); ``` ## Customizing Claims with Ready-Made Providers [Section titled “Customizing Claims with Ready-Made Providers”](#customizing-claims-with-ready-made-providers) `IProfileService` is also the extension point for **customizing claims** when using User Management or ASP.NET Identity. You do not need to replace the built-in implementation entirely — you can decorate or extend it. See [Claims](/identityserver/fundamentals/claims/) for more on how claims are populated and transformed. ----- # Duende User Management > Overview of Duende User Management, a passwordless-first identity solution supporting OTP, TOTP, passkeys, external authentication, user profiles, roles, groups, and membership management. Most .NET applications start authentication the same way: wire up ASP.NET Identity, add a login page, and ship. That works until requirements grow. MFA gets bolted on later, and passkey support requires a separate library. Greenfield projects face the same trap. Rolling custom auth feels faster at first, but compliance requirements, account recovery flows, and external provider integration accumulate quickly. The pain points are predictable: no passkey support without significant custom work, MFA that lives outside the core auth flow, migration headaches when the user model needs to evolve, and gaps that surface during security reviews or compliance audits. Each of these is solvable in isolation, but solving them together, consistently and correctly, is where most teams lose time. Duende User Management is an optional, first-party component of Duende IdentityServer. It provides native user storage, a passwordless-first authentication layer (OTP, TOTP, passkeys, external providers, recovery codes), full lifecycle management (profiles, roles, groups), and membership management for assigning users to roles and groups programmatically. You enable it through the IdentityServer builder and get a production-ready identity foundation without assembling it from parts. ## How User Management fits into IdentityServer [Section titled “How User Management fits into IdentityServer”](#how-user-management-fits-into-identityserver) User Management is a component of Duende IdentityServer, not a separate product. IdentityServer handles the OpenID Connect, OAuth 2.0, and SAML protocol layer: issuing tokens, managing clients and scopes, and enforcing authorization policies. User Management provides the user store and authentication UI that plugs into that protocol layer. You can add User Management to a new IdentityServer deployment or integrate it into an existing one. All modules (profiles, authentication, membership) are registered automatically when you call `AddUserManagement()`. ## Authentication methods [Section titled “Authentication methods”](#authentication-methods) User Management supports the authentication methods that modern applications need: * **One-Time Passwords (OTP)**: Passwordless authentication via email or SMS-delivered one-time codes, suitable for both primary and step-up authentication flows. * **TOTP**: Time-based one-time passwords compatible with authenticator apps such as Microsoft Authenticator and Google Authenticator. * **Passkeys (WebAuthn/FIDO2)**: Phishing-resistant, device-bound authentication using the FIDO2/WebAuthn standard. * **External Authentication**: Federate with external identity providers (social logins, enterprise IdPs) via OpenID Connect and OAuth 2.0. * **Username and Password**: Traditional credential-based authentication, supported for scenarios where it is required. * **Recovery Codes**: Single-use backup codes that allow users to regain access when their primary authentication method is unavailable. ## Key features [Section titled “Key features”](#key-features) * **Passwordless-First Design**: Built from the ground up to support modern, password-free authentication flows, with passwords as an opt-in rather than the default. * **User Profiles**: An extensible user profile model for storing and surfacing custom claims and attributes alongside standard identity information. * **Roles and Groups**: Built-in support for role-based access control and group membership management, making it straightforward to model organizational structures and permission boundaries. * **Membership Management**: A dedicated API surface (`IMembershipAdmin`) for assigning and removing users from roles and groups programmatically. This matters when user-to-role and user-to-group relationships need to be managed by application code, for example during provisioning workflows, admin UIs, or automated onboarding, rather than only at login time. * **Opinionated Defaults**: Sensible, security-oriented defaults that reduce the surface area for misconfiguration without sacrificing extensibility. ## When to use User Management [Section titled “When to use User Management”](#when-to-use-user-management) User Management is a good fit when you need: * Modern authentication methods beyond username and password, including passkeys, OTP, and TOTP. * A complete user store that integrates with Duende IdentityServer without requiring you to wire up identity primitives manually. * Enterprise-grade features such as roles, groups, extensible user profiles, and programmatic membership management via `IMembershipAdmin`. * Recovery code support so users are never permanently locked out of their accounts. * A passwordless-first approach that still accommodates password-based authentication where required. [GitHub Repository](https://github.com/DuendeSoftware/products)View the source code for this library on GitHub. [NuGet Package](https://www.nuget.org/packages/Duende.UserManagement.IdentityServer8)View the package on NuGet.org. ## Licensing [Section titled “Licensing”](#licensing) A Duende license is required to use User Management. See the [licensing documentation](/general/licensing/) for details. * **Development and Testing**: You are free to use and explore the code for development, testing, or personal projects without a license. * **Production**: A license is required for production environments. ## Learn More [Section titled “Learn More”](#learn-more) See the [glossary](/general/glossary/) for definitions of terms used throughout this documentation. [Getting Started](/identityserver/identity/user-management/getting-started)Add User Management to an ASP.NET Core app with a working OTP login flow. [IdentityServer Integration](/identityserver/identity/user-management/identityserver-integration)Add User Management to an existing or new IdentityServer deployment. [Authentication Flows](/identityserver/identity/user-management/authentication/overview)Compare OTP, TOTP, passkeys, passwords, external providers, and recovery codes. [Configuration Reference](/identityserver/identity/user-management/reference/configuration)All configuration options for authentication, profiles, and membership modules. [Sample Application](/identityserver/samples/usermanagement)A complete sample showing OTP, passwords, passkeys, external login, profile management, and ASP.NET Identity migration. ----- # External Authentication Flow > How to implement external (federated/social) authentication in Duende User Management, including provider configuration, user provisioning, and managing linked external authenticators. External authentication delegates sign-in to trusted third-party identity providers using OAuth 2.0 and OpenID Connect. Users authenticate with a provider they already trust (e.g. Google, Microsoft, a corporate IdP), and your application receives a verified identity without ever handling a password. Code examples The code examples on this page use ASP.NET Core Razor Pages (`PageModel`, `IActionResult`, `OnPost*` handler methods). The same patterns apply equally to **MVC controllers** (use action methods returning `IActionResult`) or **minimal API endpoints** (use route handler delegates). The Duende User Management interfaces are framework-agnostic. ## When to Use External Authentication [Section titled “When to Use External Authentication”](#when-to-use-external-authentication) **Strongly recommended for:** * Consumer applications where users expect social login options. * Public-facing apps where reducing registration friction is a priority. * B2C platforms where a lower barrier to entry increases conversion. **Good for:** * Internal tools using corporate SSO (Azure AD, Okta). * Developer platforms where GitHub login is a natural fit. * Applications that offer external authentication alongside other methods. **Not ideal for:** * Applications with strict privacy requirements that cannot depend on external services. * Offline scenarios where network connectivity cannot be assumed. ## Comparison With Other Authentication Flows [Section titled “Comparison With Other Authentication Flows”](#comparison-with-other-authentication-flows) | Aspect | External Auth | Password | One-Time Password (OTP) | | ------------------- | -------------------------- | ---------------------- | ------------------------------------------------------------ | | **User Memory** | Nothing to remember | Must remember password | Nothing to remember | | **Offline Support** | No | Yes | No | | **Security** | Provider-dependent | Strength-dependent | Channel-dependent | | **User Friction** | Click button | Type password | Retrieve OTP from email, SMS or other channel and type/paste | | **Infrastructure** | External identity provider | Password hashing | Email, SMS or other channel provider | For a full comparison that includes passkeys and TOTP, see the [Authentication Overview](/identityserver/identity/user-management/authentication/overview/). ## How It Works [Section titled “How It Works”](#how-it-works) External authentication follows the OAuth 2.0 Authorization Code Flow with PKCE: 1. **User initiates login** - User clicks “Sign in with Google” (or other provider). 2. **Authorization request** - Application redirects user to the provider’s authorization endpoint. 3. **User authenticates** - User signs in at the provider (if not already authenticated). 4. **Consent** - User grants permission for the application to access their profile. 5. **Authorization code** - Provider redirects back with an authorization code. 6. **Token exchange** - Application exchanges the code for an ID token and access token. 7. **Claim extraction** - Application validates the ID token and extracts user claims (`sub`, `email`, `name`, etc.). 8. **User provisioning** - Application finds an existing user or creates a new account, optionally prompting the user for further details and/or confirmation before doing or so. 9. **Session establishment** - A local authentication session is created. ## Key Components [Section titled “Key Components”](#key-components) ### IUserAuthenticatorsSelfService Interface [Section titled “IUserAuthenticatorsSelfService Interface”](#iuserauthenticatorsselfservice-interface) `IUserAuthenticatorsSelfService` handles user lookup, registration, and external authenticator management: ```csharp public interface IUserAuthenticatorsSelfService { // Look up a user by their subject ID Task TryGetAsync( UserSubjectId subjectId, Ct ct); // Add a single external authenticator to an existing user Task TryAddExternalAuthenticatorAddressAsync( UserSubjectId subjectId, ExternalAuthenticatorAddress authenticator, Ct ct); // Remove a single external authenticator from a user Task TryRemoveExternalAuthenticatorAddressAsync( UserSubjectId subjectId, ExternalAuthenticatorAddress authenticator, Ct ct); } ``` ### IUserAuthenticatorsAdmin Interface [Section titled “IUserAuthenticatorsAdmin Interface”](#iuserauthenticatorsadmin-interface) `IUserAuthenticatorsAdmin` provides bulk administrative operations for external authenticators: ```csharp public interface IUserAuthenticatorsAdmin { // Add multiple external authenticators to a user Task TryAddExternalAuthenticatorAddressesAsync( UserSubjectId subjectId, IEnumerable authenticators, Ct ct); // Remove multiple external authenticators from a user Task TryRemoveExternalAuthenticatorAddressesAsync( UserSubjectId subjectId, IEnumerable authenticators, Ct ct); } ``` ### Supporting Types [Section titled “Supporting Types”](#supporting-types) `ExternalAuthenticatorAddress` - Represents a linked external account: ```csharp public sealed record ExternalAuthenticatorAddress( ExternalAuthenticatorName Name, // Provider name (e.g., "Google") ISubjectId SubjectId // Provider's subject identifier ); ``` `ExternalAuthenticatorName` - Identifies the external provider: ```csharp public record ExternalAuthenticatorName { // Typically matches the ASP.NET Core authentication scheme name public static ExternalAuthenticatorName Create(string input); public static ExternalAuthenticatorName? CreateOrDefault(string? input); public static bool TryCreate(string? input, [NotNullWhen(true)] out ExternalAuthenticatorName? result); public static bool TryCreate(string? input, [NotNullWhen(true)] out ExternalAuthenticatorName? result, [NotNullWhen(false)] out IReadOnlyList? errors); } ``` `UserAuthenticators` - Returned by lookup and registration methods; contains the user’s full authenticator state: ```csharp public sealed record UserAuthenticators { public UserSubjectId SubjectId { get; } public IReadOnlyCollection ExternalAuthenticatorAddresses { get; } public bool HasPassword { get; } // ... other authenticator collections } ``` ## Configuration [Section titled “Configuration”](#configuration) ### Adding An OpenID Connect Provider [Section titled “Adding An OpenID Connect Provider”](#adding-an-openid-connect-provider) Register an external provider using ASP.NET Core’s `AddOpenIdConnect` extension. Hook into `OnTicketReceived` to run your user provisioning logic after a successful authentication. Install the required NuGet package: * [`Microsoft.AspNetCore.Authentication.OpenIdConnect`](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect) for OpenID Connect providers (Google, Microsoft, Okta, etc.) * [`Microsoft.AspNetCore.Authentication.OAuth`](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OAuth) for generic OAuth 2.0 providers (GitHub, etc.) Program.cs ```csharp builder.Services.AddAuthentication() .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme) .AddOpenIdConnect("Google", "Sign in with Google", options => { options.Authority = "https://accounts.google.com/"; options.ClientId = "your-client-id.apps.googleusercontent.com"; options.ClientSecret = "your-client-secret"; options.CallbackPath = "/signin-google"; // Authorization Code Flow with PKCE options.ResponseType = "code"; options.UsePkce = true; // Request the claims you need options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("email"); options.GetClaimsFromUserInfoEndpoint = true; options.MapInboundClaims = false; options.Events.OnTicketReceived = async context => { await HandleExternalAuthentication(context); }; }); ``` ### Provider-Specific Configurations [Section titled “Provider-Specific Configurations”](#provider-specific-configurations) Each provider has its own developer console where you register your application and obtain credentials. Refer to the official documentation for setup instructions: * **Google**: [Google Identity: OpenID Connect](https://developers.google.com/identity/openid-connect/openid-connect) * **Microsoft / Azure AD**: [Microsoft Entra identity platform](https://learn.microsoft.com/en-us/entra/identity-platform/) * **GitHub**: [Authorizing OAuth Apps](https://docs.github.com/en/apps/oauth-apps/building-oauth-apps/authorizing-oauth-apps) Note These are just a few examples. Any OAuth 2.0 or OpenID Connect provider can be used with User Management. For a broader list of third-party authentication providers and configuration guidance, see the [IdentityServer login documentation](/identityserver/ui/login/). #### Google [Section titled “Google”](#google) Force account selection on every login: ```csharp .AddOpenIdConnect("Google", options => { options.Authority = "https://accounts.google.com/"; options.ClientId = "xxx.apps.googleusercontent.com"; options.Events.OnRedirectToIdentityProvider = context => { context.ProtocolMessage.SetParameter("prompt", "select_account"); return Task.CompletedTask; }; }); ``` #### Microsoft (Azure AD) [Section titled “Microsoft (Azure AD)”](#microsoft-azure-ad) ```csharp .AddOpenIdConnect("Microsoft", options => { options.Authority = "https://login.microsoftonline.com/common/v2.0"; options.ClientId = "your-app-id"; options.ClientSecret = "your-client-secret"; options.Scope.Add("email"); options.Scope.Add("profile"); }); ``` ## Implementation Patterns [Section titled “Implementation Patterns”](#implementation-patterns) ### Handling External Authentication [Section titled “Handling External Authentication”](#handling-external-authentication) The `OnTicketReceived` event fires after the provider has authenticated the user. Use it to find or auto-register the user in your system and replace the incoming principal with a local identity: ```csharp public static async Task HandleExternalAuthentication(TicketReceivedContext context) { var ct = context.HttpContext.RequestAborted; var principal = context.Principal ?? throw new InvalidOperationException("No principal"); var authenticatorsSelfService = context.HttpContext.RequestServices .GetRequiredService(); var profileSelfService = context.HttpContext.RequestServices .GetRequiredService(); // Build the external authenticator from the incoming principal var externalAuthenticatorName = ExternalAuthenticatorName.Create(context.Scheme.Name); var sub = principal.FindFirst(JwtClaimTypes.Subject) ?? throw new InvalidOperationException("No subject claim from provider"); var authenticator = new ExternalAuthenticatorAddress( externalAuthenticatorName, OpaqueSubjectId.Create(sub.Value)); // Look up an existing user by this external authenticator var authenticators = await authenticatorsSelfService.TryGetAsync(authenticator, ct); var profile = authenticators is not null ? await profileSelfService.TryGetAsync(authenticators.SubjectId, ct) : null; // If no existing user, auto-register authenticators ??= await authenticatorsSelfService.TryCreateAsync( UserSubjectId.New(), authenticator, ct: ct); if (authenticators is null) throw new InvalidOperationException("Could not register user"); // Create a profile if one doesn't exist yet if (profile is null) { var schema = await profileSelfService.GetSchemaAsync(ct); var name = principal.FindFirstValue(JwtClaimTypes.Name) ?? string.Empty; var attributes = new AttributeValueCollection(schema); attributes.Set(UserAttributes.Name.Name, name); profile = await profileSelfService.TryCreateAsync( authenticators.SubjectId, attributes.Validate(), ct); if (profile is null) throw new InvalidOperationException("Could not register user profile"); } // Replace the incoming principal with a local claims identity var claims = new Claim[] { new(JwtClaimTypes.Subject, profile.SubjectId.Value), new(JwtClaimTypes.Name, (string?)profile.Attributes.GetValueOrDefault(UserAttributes.Name.Name)?.UntypedValue ?? "") }; context.Principal = new ClaimsPrincipal( new ClaimsIdentity(claims, context.Scheme.Name)); } ``` ### Adding An External Authenticator [Section titled “Adding An External Authenticator”](#adding-an-external-authenticator) Allow signed-in users to link additional external accounts. Start a challenge with the target provider, then handle the callback to add the authenticator to the existing user: ```csharp // Initiate the "link account" flow public IActionResult OnPostAddAuthenticator(string provider) { var properties = new AuthenticationProperties { RedirectUri = "/account/manage", Items = { ["user_id"] = GetCurrentUserId().ToString(), ["action"] = "add_authenticator" } }; return Challenge(properties, provider); } // Handle the callback public static async Task HandleAddAuthenticator(TicketReceivedContext context) { if (!context.Properties.Items.TryGetValue("action", out var action) || action != "add_authenticator") { return; // Not an "add authenticator" flow } var userIdString = context.Properties.Items["user_id"]!; var userId = UserSubjectId.Create(userIdString); var providerName = ExternalAuthenticatorName.Create(context.Scheme.Name); var providerSubject = OpaqueSubjectId.Create( context.Principal.FindFirst(JwtClaimTypes.Subject)!.Value); var authenticatorAddress = new ExternalAuthenticatorAddress(providerName, providerSubject); var userAuthenticatorsSelfService = context.HttpContext.RequestServices .GetRequiredService(); // Ensure this external account is not already linked to a different user var existingUser = await userAuthenticatorsSelfService.TryGetAsync( userId, context.HttpContext.RequestAborted); if (existingUser != null && existingUser.SubjectId != userId) throw new Exception("This external account is already linked to another user"); // Link the authenticator to the current user await userAuthenticatorsSelfService.TryAddExternalAuthenticatorAddressAsync( userId, authenticatorAddress, context.HttpContext.RequestAborted); } ``` ### Removing An External Authenticator [Section titled “Removing An External Authenticator”](#removing-an-external-authenticator) Always verify that the user retains at least one authentication method before removing an external authenticator: ```csharp public async Task OnPostRemoveAuthenticator( string providerName, string providerSubject, CancellationToken ct) { var userId = GetCurrentUserId(); var userAuthenticatorsSelfService = HttpContext.RequestServices .GetRequiredService(); var user = await userAuthenticatorsSelfService.TryGetAsync(userId, ct); if (user == null) return Error("User not found"); // Prevent removing the last authentication method if (user.ExternalAuthenticatorAddresses.Count <= 1 && !user.HasPassword) return Error("Cannot remove the last authentication method"); var authenticatorAddress = new ExternalAuthenticatorAddress( ExternalAuthenticatorName.Create(providerName), OpaqueSubjectId.Create(providerSubject)); var removed = await userAuthenticatorsSelfService.TryRemoveExternalAuthenticatorAddressAsync( userId, authenticatorAddress, ct); return removed ? Success("Authenticator removed") : Error("Authenticator not found"); } ``` ## Security Characteristics [Section titled “Security Characteristics”](#security-characteristics) ### OAuth 2.0 Protections [Section titled “OAuth 2.0 Protections”](#oauth-20-protections) * **State parameter** - Prevents CSRF attacks; automatically managed by ASP.NET Core. * **PKCE** - Prevents authorization code interception; mandatory for public clients and recommended for all clients. * **Token validation** - The ID token is validated for signature, issuer, audience, and expiration before any user data is trusted. ### Authenticator Uniqueness [Section titled “Authenticator Uniqueness”](#authenticator-uniqueness) Each external authenticator (provider + subject ID pair) can only be linked to one user. Before linking an account, check whether it is already associated with a different user to prevent account takeover: ```csharp var existingUser = await userAuthenticatorsSelfService.TryGetAsync( authenticatorAddress, ct); if (existingUser != null && existingUser.SubjectId != currentUserId) return Error("This account is already linked to another user"); ``` ## Security [Section titled “Security”](#security) When you delegate authentication to an external provider, you are trading one set of problems for another. You no longer store credentials, which is good. But you are now trusting a third party’s security posture, and the OAuth/OpenID Connect protocol has enough moving parts that misconfiguration is easy. ### What ASP.NET Core Does for You [Section titled “What ASP.NET Core Does for You”](#what-aspnet-core-does-for-you) ASP.NET Core’s OAuth middleware handles the protocol-level protections: a cryptographically random `state` parameter on every flow to prevent CSRF on the callback, strict `redirect_uri` validation (no wildcards), full ID token validation including issuer, audience, expiry, and signature, and PKCE to prevent authorization code interception. ### What User Management Does for You [Section titled “What User Management Does for You”](#what-user-management-does-for-you) User Management handles the user lifecycle after the protocol completes: looking up users by their external authenticator, auto-registering new users, linking and unlinking external accounts, and ensuring that an external identity cannot be linked to multiple local accounts simultaneously. ### What You Need to Think About [Section titled “What You Need to Think About”](#what-you-need-to-think-about) The most common mistake is using the email address from the external provider as the stable user identifier. Email addresses can be reassigned; a provider can give the same email to a different user after an account is deleted. Use the `sub` claim (subject identifier) instead; it is stable and unique per provider. Be careful with account linking flows. If a user can link a new external provider to their account without being authenticated first, an attacker can pre-link their own identity to an account they do not yet control, then wait for the victim to trigger the link. Require authentication before linking. Every external provider you add is a new trust boundary. If Google’s OAuth is compromised, every user who logs in with Google is affected. Be deliberate about which providers you integrate, and monitor for unexpected changes to a user’s linked authenticators. A sudden change can be a sign of account takeover. For cross-cutting security topics (data protection key persistence and throttling configuration) see [Security Considerations](/identityserver/identity/user-management/fundamentals/security/). ----- # OTP Authentication Flow > How to implement one-time password (OTP) authentication in Duende User Management, including code generation, delivery, verification, auto-registration, and OTP address management. OTP (One-Time Password) authentication is a passwordless flow where users receive a temporary verification code via email or SMS. No passwords to manage, and the code itself proves ownership of the delivery address. ## When to Use OTP Authentication [Section titled “When to Use OTP Authentication”](#when-to-use-otp-authentication) **Good for:** * Consumer applications where users prefer passwordless options. * Infrequent logins where users are unlikely to remember a password. * Quick onboarding flows that require no registration form. * Email or phone ownership verification. * Low-to-moderate security requirements. **Not ideal for:** * High-security applications where channel interception is a concern. * Offline scenarios that require no network connectivity. * High-frequency logins where the context switch to email or SMS creates too much friction. * Regulated industries where OTP may not satisfy compliance requirements. For a comparison of all authentication methods, see [Choosing an Authentication Method](/identityserver/identity/user-management/authentication/overview#choosing-an-authentication-method). ## How It Works [Section titled “How It Works”](#how-it-works) The OTP authentication flow has two main steps. ### Step 1: Code Generation and Delivery [Section titled “Step 1: Code Generation and Delivery”](#step-1-code-generation-and-delivery) 1. **User enters identifier** - The user provides their email address or phone number. 2. **Code generation** - User Management generates a cryptographically secure random code (8 characters, alphanumeric base32 by default). 3. **Code delivery** - The code is sent to the user via the configured channel (email or SMS). 4. **Token creation** - User Management creates an `OtpToken` that links the code to the authentication attempt. 5. **Token storage** - The application stores the token (typically in an encrypted cookie) for use during verification. ### Step 2: Code Verification [Section titled “Step 2: Code Verification”](#step-2-code-verification) 1. **User enters code** - The user retrieves the code from their email or SMS and enters it. 2. **Code validation** - User Management verifies the code matches the stored token and has not expired. 3. **User lookup or creation** - The user is automatically created if this is their first authentication with this address. 4. **Session establishment** - A local authentication session is created. The One-Time Password (OTP) login flow sends a code to the user’s email or phone, then verifies it: ``` sequenceDiagram actor User participant App participant UserManagement as User Management participant Channel as Email / SMS User->>App: Enter email or phone number App->>UserManagement: TryAuthenticateAsync(otpAddress) UserManagement->>Channel: Send OTP code UserManagement-->>App: Challenge issued App-->>User: "Check your email/phone" User->>App: Enter OTP code App->>UserManagement: TryAuthenticateAsync(otpAddress, code) UserManagement-->>App: Authenticated (subject ID) App-->>User: Signed in ``` ## Key Components [Section titled “Key Components”](#key-components) ### IOtpAuthenticator Interface [Section titled “IOtpAuthenticator Interface”](#iotpauthenticator-interface) `IOtpAuthenticator` is the primary interface to verify operations: ```csharp public interface IOtpAuthenticator { // Verify an OTP code; returns an OtpAuthenticationResult discriminated union Task TryAuthenticateAsync(PlainTextOtp otp, OtpToken token, CancellationToken ct); } ``` `TryAuthenticateAsync` returns an `OtpAuthenticationResult`, which is a discriminated union with two subtypes: * `OtpAuthenticationResult.Success` (containing the `Address` and the `UserSubjectId`) * `OtpAuthenticationResult.Failure` on failure ### IOtpSender Interface [Section titled “IOtpSender Interface”](#iotpsender-interface) `IOtpSender` is the primary interface for send OTP operations: ```csharp public interface IOtpSender { // Generate and send an OTP code to the given address Task TrySendOtpAsync(OtpAddress address, CancellationToken ct); } ``` `TrySendOtpAsync` returns a `SendOtpResult` discriminated union indicating one of three outcomes: * `SendOtpResult.Sent` the code was sent successfully * `SendOtpResult.Blocked` sending was blocked by rate limiting * `SendOtpResult.SaveFailed` persisting the OTP failed ### Supporting Types [Section titled “Supporting Types”](#supporting-types) `OtpAddress` - Combines a channel and a subject identifier (e.g., an email address): ```csharp public sealed record OtpAddress(OtpChannel Channel, ISubjectId SubjectId); ``` `OtpChannel` - The delivery mechanism: ```csharp public record OtpChannel { public static OtpChannel Email { get; } // Deliver via email public static OtpChannel Sms { get; } // Deliver via SMS } ``` `OtpToken` - Opaque token linking a code to an authentication attempt: ```csharp public record OtpToken { public static OtpToken Create(string input); public static OtpToken? CreateOrDefault(string? value); public static bool TryCreate(string? s, [NotNullWhen(true)] out OtpToken? result); public static bool TryCreate(string? s, [NotNullWhen(true)] out OtpToken? result, [NotNullWhen(false)] out IReadOnlyList? errors); public override string ToString(); } ``` `PlainTextOtp` - The verification code entered by the user: ```csharp public record PlainTextOtp { public static PlainTextOtp Create(string value); public static PlainTextOtp? CreateOrDefault(string? value); public static bool TryCreate(string? s, [NotNullWhen(true)] out PlainTextOtp? result); public static bool TryCreate(string? s, [NotNullWhen(true)] out PlainTextOtp? result, [NotNullWhen(false)] out IReadOnlyList? errors); } ``` `SendOtpResult` - A discriminated union representing the outcome of a send attempt: ```csharp public abstract record SendOtpResult { // The OTP was dispatched successfully internal sealed record Sent : SendOtpResult { internal Sent(OtpToken token, TimeSpan expiresAfter, DateTimeOffset expiresAtUtc, TimeSpan sendingBlockedFor, DateTimeOffset sendingBlockedUntilUtc){...} } // Sending was blocked by rate limiting public sealed record Blocked : SendOtpResult { internal Blocked(TimeSpan sendingBlockedFor, DateTimeOffset sendingBlockedUntilUtc){...} } // The OTP could not be persisted (storage failure) public sealed record SaveFailed : SendOtpResult; } ``` ### IUserAuthenticatorsSelfService Interface [Section titled “IUserAuthenticatorsSelfService Interface”](#iuserauthenticatorsselfservice-interface) `IUserAuthenticatorsSelfService` handles user lookup, registration, and OTP address management: ```csharp public interface IUserAuthenticatorsSelfService { // Look up a user by their subject ID Task TryGetAsync(UserSubjectId subjectId, CancellationToken ct); // OTP address management Task TryAddOtpAddressAsync(UserSubjectId subjectId, PlainTextOtp otp, OtpToken token, CancellationToken ct); Task TryRemoveOtpAddressAsync(UserSubjectId subjectId, OtpAddress address, CancellationToken ct); } ``` `TryAddOtpAddressAsync` verifies the OTP before persisting the address. The caller must first send an OTP using `IOtpSender.SendOtpAsync`, then pass the OTP code and token back for verification. This ensures the user actually controls the address being added. ## Configuration [Section titled “Configuration”](#configuration) ### Registering the SMTP OTP Sender [Section titled “Registering the SMTP OTP Sender”](#registering-the-smtp-otp-sender) OTP delivery requires configuring a dispatcher. Use `UseSmtpOtpDispatcher` on the authentication builder to configure the built-in SMTP dispatcher: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um.Authentication(auth => { auth.UseSmtpOtpDispatcher(options => { options.Host = "smtp.example.com"; options.Port = 587; options.EnableSsl = true; options.FromEmail = "noreply@example.com"; options.FromName = "My Application"; options.Domain = "example.com"; }); }) ); ``` ### Custom OTP Dispatcher [Section titled “Custom OTP Dispatcher”](#custom-otp-dispatcher) Implement `IOtpDispatcher` to deliver codes via a custom channel (e.g., an SMS gateway or a transactional email service): ```csharp public interface IOtpDispatcher { bool CanDispatch(OtpAddress address); Task DispatchAsync(OtpAddress address, PlainTextOtp otp, TimeSpan expiresAfter, Ct ct); } ``` Register the custom dispatcher using `UseOtpDispatcher`: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.UseOtpDispatcher(); }) ); ``` ### IOtpSender [Section titled “IOtpSender”](#iotpsender) `IOtpSender` is the high-level interface for sending OTPs. It orchestrates the full workflow: creates the OTP, dispatches it via the registered `IOtpDispatcher`, and returns a token for subsequent verification. Inject it when you need to send an OTP outside the authentication flow (for example, to verify an address before adding it to a user): ```csharp public interface IOtpSender { Task TrySendOtpAsync(OtpAddress address, Ct ct); } ``` ## Implementation Patterns [Section titled “Implementation Patterns”](#implementation-patterns) The following examples show common OTP workflows using Razor Pages and Duende User Management. ### Basic OTP Login [Section titled “Basic OTP Login”](#basic-otp-login) The following example shows a two-step OTP login using Razor Pages. Inject `IOtpAuthenticator` into your page model. 1. **Send the OTP:** ```csharp public async Task OnPostSendOtpAsync(string email) { if (!EmailAddress.TryCreate(email, out var emailAddress)) return Error("Invalid email address"); var address = new OtpAddress(OtpChannel.Email, emailAddress); var result = await otpAuthenticator.SendOtpAsync( address, HttpContext.RequestAborted); if (result is not SendOtpResult.Sent sent) return Error("Failed to send verification code. Please try again later."); // Store the token and address in an encrypted cookie for the verify step StoreCookie("otp_token", sent.Token.ToString()); StoreCookie("otp_email", email); return RedirectToPage("/Verify"); } ``` 2. **Verify the OTP:** ```csharp public async Task OnPostVerifyAsync(string code) { var tokenValue = GetCookie("otp_token"); var email = GetCookie("otp_email"); if (!PlainTextOtp.TryCreate(code, out var otp)) return Error("Invalid code format"); var authResult = await otpAuthenticator.TryAuthenticateAsync( otp, OtpToken.Create(tokenValue), HttpContext.RequestAborted); if (authResult is not OtpAuthenticationResult.Success otpSuccess) return Error("Invalid or expired code"); var claims = new List { new(ClaimTypes.NameIdentifier, otpSuccess.UserSubjectId.Value), new(ClaimTypes.Name, otpSuccess.Address.Value), }; await SignIn(new ClaimsPrincipal( new ClaimsIdentity(claims, CookieAuthenticationDefaults.AuthenticationScheme))); ClearCookies(); return RedirectToPage("/Index"); } ``` ### Auto-Registration [Section titled “Auto-Registration”](#auto-registration) When a user authenticates via OTP for the first time, User Management automatically creates a `UserAuthenticators` record and a user profile. The profile is initialized with the email attribute from the OTP address. You do not need a separate sign-up flow. `TryCreateAsync` on `IUserAuthenticatorsSelfService` is still available for programmatic registration scenarios, such as importing users from an external system or linking an external authenticator to an existing account. Skipping auto-registration There is no built-in configuration flag to disable automatic user creation. If you need to control this behavior, for example to require an explicit invitation before a user can sign in, you can provide a custom `IOtpAuthenticator` implementation and register it with the service provider: Program.cs ```csharp builder.Services.AddTransient(); ``` Your implementation can check whether the user already exists before completing authentication and return `OtpAuthenticationResult.Failure.Instance` for unknown addresses. ### Managing OTP Addresses [Section titled “Managing OTP Addresses”](#managing-otp-addresses) Users can have multiple OTP addresses (e.g., both an email and a phone number). Adding an address requires verification: the user must prove they control the address by responding to an OTP challenge. Use `IOtpSender` to send the OTP, then `TryAddOtpAddressAsync` to verify and persist: ```csharp // Step 1: Send an OTP to the new address var phoneAddress = new OtpAddress(OtpChannel.Sms, PhoneNumber.Create("+15551234567")); var sendResult = await otpSender.TrySendOtpAsync(phoneAddress, HttpContext.RequestAborted); if (sendResult is not SendOtpResult.Sent sent) return Error("Failed to send verification code."); // Store sent.Token for the verification step (e.g., in a cookie or session) // Step 2: After the user enters the OTP code, verify and add the address await userAuthenticatorsSelfService.TryAddOtpAddressAsync( subjectId, PlainTextOtp.Create(userEnteredCode), storedToken, HttpContext.RequestAborted); // Remove an OTP address await userAuthenticatorsSelfService.TryRemoveOtpAddressAsync( subjectId, phoneAddress, HttpContext.RequestAborted); ``` To replace an address (e.g., after an email change), remove the old address and add the new one through the verification flow. ### Handling Rate Limiting [Section titled “Handling Rate Limiting”](#handling-rate-limiting) User Management enforces a minimum interval between OTP sends. When sending is blocked, `SendOtpAsync` returns a `SendOtpResult.Blocked` that indicates when the user may request a new code: ```csharp var result = await otpAuthenticator.SendOtpAsync(address, HttpContext.RequestAborted); switch (result) { case SendOtpResult.Sent sent: StoreCookie("otp_token", sent.Token.ToString()); break; case SendOtpResult.Blocked blocked: var retryAt = blocked.SendingBlockedUntilUtc.ToLocalTime(); return Error($"Please wait until {retryAt:t} before requesting a new code."); case SendOtpResult.SaveFailed: return Error("OTP send failed unexpectedly. Please try again."); } ``` ## Security [Section titled “Security”](#security) OTP is a good default for consumer applications: passwordless, and requires no app install. The trade-off is that its security depends entirely on the delivery channel. If someone can read your email or intercept your SMS, they can log in as you. ### What User Management Does for You [Section titled “What User Management Does for You”](#what-user-management-does-for-you) The OTP workflow enforces limits that are not configurable but are deliberately conservative: a maximum of 5 verification attempts per token (a 6-digit code has a million possible values, so 5 guesses is not a meaningful attack surface), a minimum of 1 minute between sends to prevent flooding, and a 5-minute code expiry to limit the window during which an intercepted code is useful. Codes are stored as PBKDF2 hashes, so a stolen database row is useless to an attacker. Verification uses constant-time comparison to prevent timing-based enumeration. ### What You Need to Think About [Section titled “What You Need to Think About”](#what-you-need-to-think-about) The delivery channel is outside User Management’s control. Email is generally more secure than SMS. SIM-swapping attacks, where an attacker convinces a carrier to transfer a phone number to a new SIM, are a real and documented threat. For sensitive applications, prefer email and consider adding a warning in your OTP email template asking users not to forward it. Users who auto-forward all email to a secondary account are effectively sharing their OTP codes with whoever controls that account. OTP is a single factor. For high-value accounts, pair it with TOTP or a passkey. A user who can receive an OTP email is authenticated, but that is a lower bar than you might want for a financial application. Reflect the built-in rate limits in your UI: show a countdown timer for code expiry, disable the “Resend” button during the cooldown period, and display the number of remaining verification attempts. For cross-cutting security topics (data protection key persistence, throttling configuration, and password hashing) see [Security Considerations](/identityserver/identity/user-management/fundamentals/security/). ----- # Authentication Flows Overview > An overview of the authentication flows supported by Duende User Management, including OTP, TOTP, passwords, external providers, passkeys, and recovery codes, with guidance on choosing the right flow for your application. Duende User Management supports multiple authentication flows, each suited to different use cases and security requirements. This page summarizes all available flows and helps you pick the right one. ## Available Flows [Section titled “Available Flows”](#available-flows) ### OTP (One-Time Password) [Section titled “OTP (One-Time Password)”](#otp-one-time-password) Passwordless authentication using temporary codes sent via email or SMS. **Key Characteristics:** * No password storage required * Code sent to the user’s registered email address or phone number * Time-limited verification codes * Auto-registration on first login **Best For:** * Consumer applications wanting a passwordless experience * Quick user onboarding * Applications with email or SMS delivery infrastructure * Low-friction authentication [OTP Authentication](/identityserver/identity/user-management/authentication/otp)Implement passwordless login via email or SMS one-time codes. *** ### TOTP (Time-Based One-Time Password) [Section titled “TOTP (Time-Based One-Time Password)”](#totp-time-based-one-time-password) Two-factor authentication using authenticator apps that generate time-based codes. **Key Characteristics:** * Works with any RFC 6238-compliant authenticator app, including [Microsoft Authenticator](https://www.microsoft.com/en-us/security/mobile-authenticator-app), [Google Authenticator](https://support.google.com/accounts/answer/1066447), [Authy](https://authy.com/), and [1Password](https://1password.com/) * Offline code generation; no network required at authentication time * 30-second rotating codes * Typically paired with recovery codes as a backup **Best For:** * High-security applications * Adding a second factor on top of password authentication * Applications requiring Multi-Factor Authentication (MFA) compliance * User accounts containing sensitive data [TOTP Authentication](/identityserver/identity/user-management/authentication/totp)Add a second factor using authenticator apps and time-based codes. *** ### Passwords [Section titled “Passwords”](#passwords) Traditional username and password authentication. **Key Characteristics:** * Users create and manage their own passwords * Password hashing and secure storage * Configurable password complexity requirements * Foundation for adding two-factor authentication **Best For:** * Enterprise applications * Regulated industries * Applications requiring offline authentication * Scenarios where external providers are not suitable [Password Authentication](/identityserver/identity/user-management/authentication/passwords)Implement traditional username and password login with PBKDF2 hashing and configurable complexity rules. *** ### External Authentication [Section titled “External Authentication”](#external-authentication) Federated authentication using external identity providers (social login, enterprise SSO). **Key Characteristics:** * OAuth 2.0 / OpenID Connect integration * No password storage in your application * Auto-registration from external provider profiles * Multiple external authenticators per user **Best For:** * Consumer-facing applications * Social or collaborative platforms * Reducing password management burden * Using established identity providers (Google, Microsoft, etc.) [External Authentication](/identityserver/identity/user-management/authentication/external)Federate login with external identity providers using OAuth 2.0 and OpenID Connect. *** ### Passkeys (WebAuthn / FIDO2) [Section titled “Passkeys (WebAuthn / FIDO2)”](#passkeys-webauthn--fido2) Phishing-resistant, device-bound authentication using the FIDO2/WebAuthn standard. **Key Characteristics:** * Cryptographic key pair stored on the user’s device * Phishing-resistant by design; credentials are bound to the origin * Supports biometric and hardware security key authenticators * No shared secrets transmitted over the network **Best For:** * Highest-security applications * Applications targeting phishing-resistant authentication * Modern consumer and enterprise applications * Compliance scenarios requiring strong authentication [Passkeys Authentication](/identityserver/identity/user-management/authentication/passkeys)Implement phishing-resistant WebAuthn/FIDO2 authentication with biometrics or hardware keys. *** ### Recovery Codes [Section titled “Recovery Codes”](#recovery-codes) Backup authentication using single-use codes generated when two-factor authentication is enabled. **Key Characteristics:** * Single-use codes for account recovery * Generated when Two-Factor Authentication (2FA) is first enabled * Cryptographically secure * Alternative when the primary 2FA method is unavailable **Best For:** * Backup for TOTP authentication * Account recovery scenarios * Device loss or replacement * Emergency access [Recovery Codes](/identityserver/identity/user-management/authentication/recovery-codes)Generate and use single-use backup codes for account recovery when the primary 2FA method is unavailable. *** ## Flow Comparison [Section titled “Flow Comparison”](#flow-comparison) | Flow | Requires Password | User Experience | Security Level | Network Required | | ------------------ | :---------------: | ------------------------ | :------------: | :-------------------: | | **OTP** | No | Simple, passwordless | Medium | Yes (to receive code) | | **TOTP** | Yes\* | Two-step | High | No | | **Passwords** | Yes | Traditional | Medium | No | | **External** | No | Familiar (social/SSO) | Medium-High | Yes (for auth) | | **Passkeys** | No | Biometric / hardware key | Very High | No | | **Recovery Codes** | No | Emergency only | Medium | No | \* TOTP is typically used as a second factor on top of password authentication. ## Choosing an Authentication Method [Section titled “Choosing an Authentication Method”](#choosing-an-authentication-method) ### OTP vs Password vs External Authentication [Section titled “OTP vs Password vs External Authentication”](#otp-vs-password-vs-external-authentication) | Aspect | OTP | Password | External Auth | | ------------------- | --------------------- | ---------------------- | -------------------------- | | **User Memory** | Nothing to remember | Must remember password | Nothing to remember | | **Offline Support** | No | Yes | No | | **Security** | Channel-dependent | Strength-dependent | Provider-dependent | | **User Friction** | Check email or SMS | Type password | Click button | | **Infrastructure** | Email or SMS provider | Password hashing | External identity provider | ### TOTP vs SMS as a Second Factor [Section titled “TOTP vs SMS as a Second Factor”](#totp-vs-sms-as-a-second-factor) | Aspect | TOTP | SMS | | ----------------------- | -------------------------- | -------------------- | | **Security** | High | Medium | | **Offline** | Yes | No | | **Phishing resistance** | High | Low | | **SIM swapping** | Not vulnerable | Vulnerable | | **Cost** | Free | SMS costs | | **Setup** | Requires authenticator app | Works with any phone | ## Choosing a Flow [Section titled “Choosing a Flow”](#choosing-a-flow) ### For Consumer Applications [Section titled “For Consumer Applications”](#for-consumer-applications) **Recommended: External + Optional TOTP** * Users sign in with familiar social accounts (Google, Microsoft, etc.) * Optional TOTP for users wanting extra security * Minimal friction for new users **Alternative: OTP** * Fully passwordless experience * Simple email-based login * No external provider dependencies ### For Enterprise Applications [Section titled “For Enterprise Applications”](#for-enterprise-applications) **Recommended: Password + TOTP** * Traditional password authentication users expect * Mandatory TOTP for compliance requirements * Recovery codes for account recovery **Alternative: External (OpenID Connect (OIDC))** * Integrate with a corporate identity provider * Single sign-on experience * Centralized access management ### For High-Security Applications [Section titled “For High-Security Applications”](#for-high-security-applications) **Recommended: Passkeys** * Phishing-resistant by design * No shared secrets * Pair with TOTP as a fallback and recovery codes for emergency access **Alternative: Password + Mandatory TOTP** * Strong two-factor authentication * Recovery codes for emergency access * Audit-friendly authentication trail ### For Quick Prototypes and MVPs [Section titled “For Quick Prototypes and MVPs”](#for-quick-prototypes-and-mvps) **Recommended: OTP or External** * Fast to implement * No password management needed * Good user experience out of the box ## Combining Multiple Flows [Section titled “Combining Multiple Flows”](#combining-multiple-flows) User Management lets you combine multiple flows in a single application. A common pattern is OTP for initial registration, TOTP as an optional second factor, and recovery codes as a backup. Tip Flows can be combined. For example, you can allow users to sign in with an external provider and then require TOTP for sensitive operations. ### Password + TOTP (Most Common) [Section titled “Password + TOTP (Most Common)”](#password--totp-most-common) Authenticate with a password first, then check whether the user has a TOTP authenticator enrolled. Passwords can also expire; when they do, `TryAuthenticateAsync` returns `PasswordAuthenticationResult.Expired` so you can redirect the user to a password-change page. See the [passwords page](/identityserver/identity/user-management/authentication/passwords/) for details on configuring expiry. ```csharp // Primary authentication with password var result = await passwordAuth.TryAuthenticateAsync( AttributeCode.Create("email"), email, NonValidatedPassword.Create(password), ct); switch (result) { case PasswordAuthenticationResult.Success success: // Check if user has 2FA enabled var user = await userSelfService.TryGetUser(success.UserSubjectId, ct); if (user?.TotpDeviceNames.Count > 0) { // Require TOTP verification return RedirectToPage("/LoginWith2FA"); } break; case PasswordAuthenticationResult.Expired expired: // The user's password has expired; redirect them to set a new one return RedirectToPage("/ChangePassword", new { userId = expired.UserSubjectId }); default: // Authentication failed return; } ``` ### Progressive Authentication [Section titled “Progressive Authentication”](#progressive-authentication) Upgrade authentication strength based on the sensitivity of the requested action: ```csharp // Allow basic actions with OTP if (User.FindFirst("amr")?.Value == "otp") { // Basic authenticated actions } // Require TOTP for sensitive actions if (User.FindFirst("amr")?.Value == "mfa") { // High-security actions } ``` ## Security Considerations [Section titled “Security Considerations”](#security-considerations) Security considerations for each flow are covered on their respective pages. For cross-cutting topics (data protection key persistence, password hashing parameters, and throttling configuration) see the dedicated page: [Security Considerations](/identityserver/identity/user-management/fundamentals/security)Data protection, password hashing parameters, throttling configuration, and passkey option reference. ----- # Passkey Authentication > How to implement passkey (WebAuthn/FIDO2) authentication using Duende User Management, including credential management, web endpoint configuration, second-factor support, and the JavaScript helper. Passkeys provide phishing-resistant, passwordless authentication using the WebAuthn/FIDO2 standard. Authentication can use biometrics (fingerprint, face recognition), a device PIN, or a hardware security key. Credentials are cryptographically bound to the origin, so they cannot be used on a different site. **Learn more:** [passkeys.dev](https://passkeys.dev/) (FIDO Alliance resource site) · [WebAuthn specification](https://www.w3.org/TR/webauthn-3/) (W3C) · [FIDO Alliance](https://fidoalliance.org/) (standards body) ## When to Use Passkeys [Section titled “When to Use Passkeys”](#when-to-use-passkeys) **Strongly recommended for:** * High-security applications (financial services, healthcare, government) * Any application where phishing resistance is a priority * Applications targeting modern devices and browsers FAPI Conformance For applications requiring FAPI (Financial-grade API) compliance, Duende offers [FAPI 2.0 support](/identityserver/tokens/fapi-2-0-specification/) for IdentityServer, which extends security with additional requirements aligned to financial-grade standards. See also the [conformance report](/identityserver/diagnostics/conformance-report/). **Good for:** * Consumer applications (passkeys are natively supported on modern iOS, Android, macOS, and Windows) * Enterprise applications replacing hardware tokens * Applications looking to eliminate password management overhead **Considerations:** * Requires browser and device support for WebAuthn (broadly available in all modern browsers and operating systems) * Users need a fallback authentication method if they lose access to their device * Discoverable credentials require authenticator support for resident keys ## Passkeys vs Other Authentication Methods [Section titled “Passkeys vs Other Authentication Methods”](#passkeys-vs-other-authentication-methods) | Aspect | Passkeys | Time-Based One-Time Password (TOTP) | Password | | ----------------------- | -------------- | ----------------------------------- | ---------- | | **Phishing resistance** | Excellent | High | None | | **User experience** | Excellent | Moderate | Moderate | | **Device dependency** | Yes | Yes (app) | No | | **Offline support** | Yes | Yes | Yes | | **Shared secret** | No | Yes | Yes | | **Replay attacks** | Not vulnerable | Not vulnerable | Vulnerable | | **Setup complexity** | Low | Moderate | Low | ## How It Works [Section titled “How It Works”](#how-it-works) WebAuthn calls its two main protocol flows *ceremonies*. The **registration ceremony** creates a new credential on the user’s device and registers the public key with your server. The **authentication ceremony** proves the user still controls the device by signing a server challenge with the stored private key. ### Registration Ceremony [Section titled “Registration Ceremony”](#registration-ceremony) 1. **User initiates passkey creation** - From account settings or during sign-up 2. **Challenge generation** - The server generates a cryptographic challenge 3. **Authenticator interaction** - The user’s device creates a public/private key pair 4. **Credential storage** - The public key and credential ID are stored server-side 5. **Private key retention** - The private key never leaves the user’s device ### Authentication Ceremony [Section titled “Authentication Ceremony”](#authentication-ceremony) 1. **Challenge generation** - The server generates a cryptographic challenge 2. **Authenticator interaction** - The user’s device signs the challenge with the private key 3. **Signature verification** - The server verifies the signature using the stored public key 4. **Session establishment** - Authentication is complete ### Discoverable Credentials [Section titled “Discoverable Credentials”](#discoverable-credentials) Discoverable credentials (also called resident keys) allow passwordless login without entering a username first. The authenticator stores the credential and can present it automatically when the relying party requests authentication. ## Authenticator Types [Section titled “Authenticator Types”](#authenticator-types) * **Platform authenticators** - Built into the device (Touch ID, Face ID, Windows Hello). Convenient but tied to a specific device. * **Cross-platform authenticators** - Separate hardware security keys (YubiKey, Titan Security Key). Work across devices but require carrying the key. ## Security Properties [Section titled “Security Properties”](#security-properties) Passkeys are the strongest authentication option in User Management, and the one with the fewest caveats. The private key never leaves the device, credentials are bound to your specific origin so they cannot be phished, and every authentication uses a unique challenge so replaying a captured response does not work. If you can use passkeys, you should. ### What User Management Does for You [Section titled “What User Management Does for You”](#what-user-management-does-for-you) Credentials are cryptographically bound to the relying party ID and origin. A passkey registered at `auth.example.com` cannot be used at `evil.example.com`, even if an attacker controls a subdomain. The server stores only the public key, so a database breach gives an attacker nothing useful. Challenges are 32 bytes (256 bits) of random data, expire after 5 minutes, and are single-use. There is no shared secret to steal, no code to intercept, and no password to guess. ### What You Need to Think About [Section titled “What You Need to Think About”](#what-you-need-to-think-about) Most passkey security issues come from misconfiguration rather than protocol weaknesses. `AllowedOrigins` is required and must be set explicitly. An overly broad list weakens origin binding, which is the core security property of passkeys. List only the exact origins your application uses. If you want a passkey registered at `auth.example.com` to work at `app.example.com`, set `ServerDomain` to `"example.com"`. Without it, each subdomain is treated as a separate relying party and the passkey will not work across them. The default `UserVerificationRequirement` is `"preferred"`, which means authentication can succeed without a PIN or biometric if the authenticator does not support user verification. For high-assurance scenarios (financial applications, admin interfaces, anything sensitive) set it to `"required"`. For regulated environments where you need to know exactly what kind of authenticator your users are using, set `AttestationConveyancePreference` to `"direct"` and verify the attestation statement. This lets you enforce an allowlist of approved authenticator models. For cross-cutting security topics (data protection key persistence and throttling configuration) see [Security Considerations](/identityserver/identity/user-management/fundamentals/security/). ## Configuration [Section titled “Configuration”](#configuration) ### Service Registration [Section titled “Service Registration”](#service-registration) Passkey support involves two registration steps: 1. **`AddUserManagement()`** automatically registers the core passkey services: cryptographic signature verification (ES256, ES384, ES512, RS256, RS384, RS512, PS256, PS384, PS512, RS1), WebAuthn registration and authentication ceremonies, credential storage, and challenge management. Use `Authentication(configure => ...)` to configure options when you want to drive the passkey flow yourself without the built-in HTTP endpoints. 2. **`MapUserManagement()`** maps the web layer, including the passkey HTTP endpoints (`/passkeys/register/begin`, `/passkeys/register/complete`, `/passkeys/authenticate/begin`, `/passkeys/authenticate/complete`) and the JavaScript helper (`/passkeys/js`). Using `Authentication(configure => ...)` to configure passkey options: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.Configure(options => { options.Passkeys.RelyingPartyName = "My Application"; options.Passkeys.ServerDomain = "example.com"; }); }) ); ``` This gives you access to `IPasskeyCeremonies`, `IUserAuthenticatorsSelfService`, and the other core services via dependency injection, but does not expose any HTTP endpoints. This is useful if you want to build your own API layer or integrate passkeys into an existing controller/endpoint structure. If you are building a web application and want the ready-to-use passkey endpoints, call `MapUserManagement()` on your endpoint route builder: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(); // After building the app: var app = builder.Build(); app.MapUserManagement(); ``` Note `MapUserManagement()` maps all User Management HTTP endpoints, including passkey registration and authentication ceremonies. ### Passkey Options [Section titled “Passkey Options”](#passkey-options) Configure passkey behavior using `PasskeyOptions`, accessible via `UserAuthenticationOptions.Passkeys`. Use the `Configure` method on the authentication builder: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.Configure(options => { options.Passkeys.RelyingPartyName = "My Application"; options.Passkeys.ServerDomain = "example.com"; }); }) ); ``` All `PasskeyOptions` properties and their defaults: | Property | Type | Default | Description | | --------------------------------- | ------------------------ | ------------- | ------------------------------------------------------------------ | | `RelyingPartyName` | `string` | Assembly name | Human-readable name shown during registration | | `ServerDomain` | `string?` | `null` | Relying party ID (domain). Set to share passkeys across subdomains | | `AllowedOrigins` | `IReadOnlyList?` | `null` | Fully-qualified origins permitted to use passkeys | | `UserVerificationRequirement` | `string` | `"preferred"` | Whether user verification (PIN/biometric) is required | | `AttestationConveyancePreference` | `string` | `"none"` | Whether attestation statements are requested | | `AuthenticatorAttachment` | `string?` | `null` | Restrict to `"platform"` or `"cross-platform"` authenticators | | `ResidentKeyRequirement` | `string` | `"preferred"` | Whether discoverable credentials are required | | `ChallengeSize` | `int` | `32` | Challenge size in bytes | | `ChallengeTimeout` | `TimeSpan` | 5 minutes | How long a challenge remains valid | | `SupportedAlgorithms` | `IReadOnlyList` | `[]` (all) | COSE algorithm identifiers, in preference order | ## Core Passkey Management [Section titled “Core Passkey Management”](#core-passkey-management) Passkey credentials are managed through `IUserAuthenticatorsSelfService`. This interface handles the persistence of credentials after the WebAuthn ceremony completes. ### Adding a Passkey [Section titled “Adding a Passkey”](#adding-a-passkey) After a successful registration ceremony, persist the credential: ```csharp Task TryAddPasskeyAsync( UserSubjectId subjectId, PasskeyCredentialData credential, CancellationToken ct); ``` `PasskeyCredentialData` is returned from a completed registration ceremony and contains: * `CredentialId` - Strongly-typed `PasskeyCredentialId` (byte array, max 1023 bytes) * `PublicKeyCose` - The COSE-encoded public key * `Algorithm` - COSE algorithm identifier * `SignCount` - Initial signature counter value * `BackupEligible` - Whether the credential can be backed up * `BackedUp` - Whether the credential is currently backed up * `Aaguid` - Authenticator AAGUID (identifies the authenticator model) * `CreatedAt` - Registration timestamp * `Name` - Display name for the credential Returns `true` if the credential was stored successfully, `false` if the user was not found or the credential already exists. ### Removing a Passkey [Section titled “Removing a Passkey”](#removing-a-passkey) Remove a specific passkey by its credential ID: ```csharp Task TryRemovePasskeyAsync( UserSubjectId subjectId, PasskeyCredentialId credentialId, CancellationToken ct); ``` Returns `true` if the credential was removed, `false` if the user or credential was not found. ### Listing Registered Passkeys [Section titled “Listing Registered Passkeys”](#listing-registered-passkeys) Retrieve a user’s registered passkeys via `TryGetAsync`: ```csharp var authenticators = await selfService.TryGetAsync(userId, ct); // authenticators.Passkeys is IReadOnlyCollection foreach (var passkey in authenticators?.Passkeys ?? []) { Console.WriteLine($"Credential: {passkey.Name}, registered: {passkey.CreatedAt}"); Console.WriteLine($"Credential ID: {passkey.CredentialId}"); } ``` `UserPasskey` exposes: * `CredentialId` - The `PasskeyCredentialId` for removal operations * `Name` - Display name for the credential * `CreatedAt` - When the credential was registered ## Web Endpoints [Section titled “Web Endpoints”](#web-endpoints) When `AddUserManagement()` is called, the following HTTP endpoints are registered automatically to handle the WebAuthn ceremony protocol: | Endpoint | Method | Default Path | Description | | --------------------------------- | ------ | ------------------------------------------- | ------------------------------------------------------------------ | | Begin Registration | `POST` | `/passkeys/register/begin` | Starts a registration ceremony for the authenticated user | | Complete Registration | `POST` | `/passkeys/register/complete` | Validates the attestation response and stores the credential | | Begin Authentication (2nd factor) | `POST` | `/passkeys/authenticate/begin` | Starts an authentication ceremony for a known user (second-factor) | | Begin Discoverable Authentication | `POST` | `/passkeys/authenticate/discoverable/begin` | Starts a usernameless authentication ceremony | | Complete Authentication | `POST` | `/passkeys/authenticate/complete` | Validates the assertion response and signs the user in | | JavaScript Helper | `GET` | `/passkeys/js` | Serves the built-in passkeys JavaScript helper | The Begin Registration and Complete Registration endpoints require an authenticated user (they use `RequireAuthorization()`). The authentication endpoints are unauthenticated; they establish the session. ### Configuring Endpoint Routes [Section titled “Configuring Endpoint Routes”](#configuring-endpoint-routes) Customize endpoint paths using `UserAuthenticationEndpointOptions` and `PasskeysRouteOptions`: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.ConfigureEndpoints(options => { // Base route prefix for all passkey endpoints (default: "/passkeys") options.Passkeys.Route = "/passkeys"; // Registration endpoints (relative to Route) options.Passkeys.BeginRegistration = "/register/begin"; options.Passkeys.CompleteRegistration = "/register/complete"; // Authentication endpoints (relative to Route) options.Passkeys.BeginAuthentication = "/authenticate/begin"; options.Passkeys.BeginDiscoverableAuthentication = "/authenticate/discoverable/begin"; options.Passkeys.CompleteAuthentication = "/authenticate/complete"; // JavaScript helper endpoint (relative to Route) options.Passkeys.PasskeysJavaScript = "/js"; }); }) ); ``` Configuration can also be loaded from `appsettings.json`: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.ConfigureEndpoints( builder.Configuration.GetSection("UserAuthentication:Endpoints")); }) ); ``` `PasskeysRouteOptions` properties: | Property | Default | Description | | --------------------------------- | ---------------------------------- | ---------------------------------------------------------- | | `Route` | `/passkeys` | Base route prefix for all passkey endpoints | | `BeginRegistration` | `/register/begin` | Path for the begin registration endpoint | | `CompleteRegistration` | `/register/complete` | Path for the complete registration endpoint | | `BeginAuthentication` | `/authenticate/begin` | Path for the begin authentication endpoint (second-factor) | | `BeginDiscoverableAuthentication` | `/authenticate/discoverable/begin` | Path for the begin discoverable authentication endpoint | | `CompleteAuthentication` | `/authenticate/complete` | Path for the complete authentication endpoint | | `PasskeysJavaScript` | `/js` | Path for the JavaScript helper endpoint | ### Ceremony Protocol [Section titled “Ceremony Protocol”](#ceremony-protocol) The endpoints implement the WebAuthn ceremony protocol: **Registration:** 1. Client calls `POST /passkeys/register/begin` - receives a `challengeId` and `PublicKeyCredentialCreationOptions` 2. Client passes the options to `navigator.credentials.create()` 3. Client calls `POST /passkeys/register/complete` with the `challengeId` and the authenticator’s attestation response 4. Server validates the attestation and stores the credential Here’s how the registration ceremony flows between the browser, your application, and User Management: ``` sequenceDiagram actor User participant Browser participant App participant UserManagement as User Management User->>App: Initiate passkey registration App->>UserManagement: POST /passkeys/register/begin UserManagement-->>App: challengeId + PublicKeyCredentialCreationOptions App->>Browser: navigator.credentials.create(options) Browser->>User: Prompt (biometric / PIN) User->>Browser: Authenticate Browser-->>App: Attestation response App->>UserManagement: POST /passkeys/register/complete (challengeId + attestation) UserManagement-->>App: Credential stored App-->>User: Passkey registered ``` **Authentication (discoverable):** 1. Client calls `POST /passkeys/authenticate/discoverable/begin` - receives a `challengeId` and `PublicKeyCredentialRequestOptions` 2. Client passes the options to `navigator.credentials.get()` 3. Client calls `POST /passkeys/authenticate/complete` with the `challengeId` and the authenticator’s assertion response 4. Server validates the assertion, looks up the user, and signs them in The discoverable authentication ceremony lets users sign in without entering a username first: ``` sequenceDiagram actor User participant Browser participant App participant UserManagement as User Management User->>App: Initiate passwordless sign-in App->>UserManagement: POST /passkeys/authenticate/discoverable/begin UserManagement-->>App: challengeId + PublicKeyCredentialRequestOptions App->>Browser: navigator.credentials.get(options) Browser->>User: Prompt (biometric / PIN) User->>Browser: Authenticate Browser-->>App: Assertion response App->>UserManagement: POST /passkeys/authenticate/complete (challengeId + assertion) UserManagement-->>App: User identified + signed in App-->>User: Signed in ``` ## JavaScript Helper [Section titled “JavaScript Helper”](#javascript-helper) The built-in JavaScript helper is served at `/passkeys/js` (configurable via `PasskeysJavaScript`). It provides browser-side utilities for interacting with the WebAuthn API and the ceremony endpoints. Note Passkey routes (including `/passkeys/js`) are only available when `AddUserManagement()` has been called during service registration and `app.MapUserManagement()` has been called on the endpoint route builder. Include it in your HTML: ```html ``` The default path is `/passkeys/js`, but this can be customized via `PasskeysRouteOptions`. If you have overridden the route, update the `src` attribute accordingly or read the path from the options. Tip The `asp-append-version="true"` attribute appends a content-based version hash to the URL (e.g., `/passkeys/js?v=abc123`), ensuring browsers fetch the latest version after updates. This is an ASP.NET Core [Tag Helper](https://learn.microsoft.com/aspnet/core/mvc/views/tag-helpers/built-in/script-tag-helper) feature and requires the Razor view or its layout to include `@addTagHelper *, Microsoft.AspNetCore.Mvc.TagHelpers`. The JavaScript helper handles: * Calling the begin/complete endpoints * Invoking `navigator.credentials.create()` and `navigator.credentials.get()` * Encoding and decoding Base64URL values required by the WebAuthn API * Error handling with user-friendly messages for common WebAuthn errors (`NotAllowedError`, `NotSupportedError`) ### Functions [Section titled “Functions”](#functions) The helper exposes three functions: #### `registerPasskey(name, callbacks)` [Section titled “registerPasskey(name, callbacks)”](#registerpasskeyname-callbacks) Registers a new passkey for the current (authenticated) user. The user must already be signed in. | Parameter | Type | Description | | ------------------------------------- | ---------- | ---------------------------------------------------------------- | | `name` | `string` | Optional user-friendly name for the passkey (max 255 characters) | | `callbacks.onStart` | `Function` | Called when registration begins | | `callbacks.onWaitingForAuthenticator` | `Function` | Called when waiting for user interaction with the authenticator | | `callbacks.onSuccess` | `Function` | Called with the result object (`{credentialId}`) on success | | `callbacks.onError` | `Function` | Called with an error message string on failure | ```javascript registerPasskey("My laptop", { onStart: () => showSpinner(), onWaitingForAuthenticator: () => showMessage("Touch your security key or use biometrics..."), onSuccess: (result) => showMessage(`Passkey registered: ${result.credentialId}`), onError: (message) => showError(message) }); ``` Internally, `registerPasskey` calls `/passkeys/register/begin` to get the challenge and options, invokes `navigator.credentials.create()`, and posts the attestation to `/passkeys/register/complete`. #### `authenticateWithPasskey(callbacks)` [Section titled “authenticateWithPasskey(callbacks)”](#authenticatewithpasskeycallbacks) Authenticates with a second-factor passkey (for MFA flows where the user is already partially authenticated). Unlike `registerPasskey` and `authenticateWithDiscoverablePasskey`, this function does **not** call the completion endpoint itself. Instead, it passes the assertion payload to `onSuccess`, and your application is responsible for submitting it to your own completion endpoint and promoting the session. | Parameter | Type | Description | | ------------------------------------- | ---------- | ------------------------------------------------------------------- | | `callbacks.onStart` | `Function` | Called when authentication begins | | `callbacks.onWaitingForAuthenticator` | `Function` | Called when waiting for user interaction | | `callbacks.onSuccess` | `Function` | Called with the complete request payload to submit to your endpoint | | `callbacks.onError` | `Function` | Called with an error message string on failure | ```javascript authenticateWithPasskey({ onStart: () => showSpinner(), onWaitingForAuthenticator: () => showMessage("Touch your security key..."), onSuccess: async (payload) => { // Submit the assertion to your application's completion endpoint const response = await fetch('/account/mfa/passkey/complete', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify(payload) }); if (response.ok) { window.location.href = '/dashboard'; } }, onError: (message) => showError(message) }); ``` Internally, `authenticateWithPasskey` calls `/passkeys/authenticate/begin` to get the challenge, invokes `navigator.credentials.get()` scoped to the user’s registered credentials, and passes the raw assertion result to your callback. #### `authenticateWithDiscoverablePasskey(callbacks)` [Section titled “authenticateWithDiscoverablePasskey(callbacks)”](#authenticatewithdiscoverablepasskeycallbacks) Authenticates with a discoverable (usernameless) passkey. The user does not need to enter a username: the authenticator presents stored credentials for the relying party. | Parameter | Type | Description | | ------------------------------------- | ---------- | ------------------------------------------------------- | | `callbacks.onStart` | `Function` | Called when authentication begins | | `callbacks.onWaitingForAuthenticator` | `Function` | Called when waiting for user interaction | | `callbacks.onSuccess` | `Function` | Called with success result (`{userVerified, backedUp}`) | | `callbacks.onError` | `Function` | Called with an error message string on failure | ```javascript authenticateWithDiscoverablePasskey({ onStart: () => showSpinner(), onWaitingForAuthenticator: () => showMessage("Select a passkey..."), onSuccess: (result) => { // User is now authenticated - redirect or update UI window.location.href = '/dashboard'; }, onError: (message) => showError(message) }); ``` Internally, `authenticateWithDiscoverablePasskey` calls `/passkeys/authenticate/discoverable/begin`, invokes `navigator.credentials.get()` with an empty `allowCredentials` list, and posts the assertion to `/passkeys/authenticate/complete`. ## Second-Factor Passkey Authentication [Section titled “Second-Factor Passkey Authentication”](#second-factor-passkey-authentication) By default, passkeys are used as the **primary authenticator**: the user proves their identity with a passkey alone, and the ceremony establishes the session. This is the recommended flow for most applications because it is phishing-resistant and requires no password. However, some applications need a **layered authentication model** where a passkey is used as a *second factor* on top of an existing first factor (password, OTP, smart card, etc.). Common reasons include: * **Regulatory requirements**: certain compliance frameworks mandate two distinct authentication factors, each from a different category (knowledge, possession, inherence). * **Gradual migration**: you want to add passkey step-up to an existing password-based login without replacing it entirely. * **High-assurance flows**: admin consoles or financial transactions where you want both a password and a biometric confirmation. In this mode, the user first completes their primary authentication step. Your application stores the partially-authenticated user’s identity in a temporary store (session, distributed cache, etc.). The client then calls the second-factor begin endpoint, which scopes the WebAuthn challenge to that specific user’s registered passkeys. The user completes the passkey ceremony, and the server signs them in. The key difference from primary passkey auth is that the server already knows *who* is authenticating before the WebAuthn ceremony starts. This is why a resolver interface is required: User Management needs your application to hand it the user identity that was established in the first factor. The second-factor begin endpoint (`PasskeyBeginAuthenticationForSecondFactorEndpoint`) is only registered when you configure a resolver; it is not active by default. This prevents the endpoint from being called without a first-factor context in place. Passkeys can be used as a second factor after a primary authentication step (for example, after password or One-Time Password (OTP) verification). This requires implementing `ISecondFactorPasskeyAuthenticationResolver` to identify the partially-authenticated user. ### ISecondFactorPasskeyAuthenticationResolver [Section titled “ISecondFactorPasskeyAuthenticationResolver”](#isecondfactorpasskeyauthenticationresolver) ```csharp public interface ISecondFactorPasskeyAuthenticationResolver { /// /// Resolves the user that completed the first factor and is participating /// in the passkey second-factor ceremony. /// Task ResolveAsync(CancellationToken ct); } ``` Implement this interface to retrieve the user’s `UserSubjectId` from your intermediate authentication state (for example, from a session, cookie, or distributed cache): ```csharp public class CustomSecondFactorResolver : ISecondFactorPasskeyAuthenticationResolver { // Note: In a real application, use a distributed cache or database // to persist pending user IDs across requests. private static readonly ConcurrentDictionary PendingUsers = new(); private readonly IHttpContextAccessor _httpContextAccessor; public CustomSecondFactorResolver(IHttpContextAccessor httpContextAccessor) { _httpContextAccessor = httpContextAccessor; } public Task ResolveAsync(CancellationToken ct) { // Retrieve the user ID stored after the first factor completed var stateKey = _httpContextAccessor.HttpContext?.Session.GetString("PendingUserId"); if (stateKey is null || !PendingUsers.TryGetValue(stateKey, out var userId)) { return Task.FromResult(null); } return Task.FromResult(UserSubjectId.Create(userId)); } } ``` ### Enabling Second-Factor Passkeys [Section titled “Enabling Second-Factor Passkeys”](#enabling-second-factor-passkeys) Register the resolver using `EnablePasskeyForSecondFactor()`: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.EnablePasskeyForSecondFactor(); }) ); ``` When a resolver is registered, the `POST /passkeys/authenticate/begin` endpoint becomes active. It calls `ResolveAsync()` to identify the user and begins an authentication ceremony scoped to that user’s registered credentials. Without a resolver, this endpoint is not registered. An instance overload is also available for singleton resolvers: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.EnablePasskeyForSecondFactor(new CustomSecondFactorResolver(...)); }) ); ``` ### Second-Factor Flow [Section titled “Second-Factor Flow”](#second-factor-flow) 1. **Primary authentication** - User signs in with password, OTP, or another first factor 2. **State storage** - Store the user’s `UserSubjectId` in session or a temporary store 3. **Passkey challenge** - Client calls `POST /passkeys/authenticate/begin`; the resolver retrieves the user and the server returns a challenge scoped to that user’s passkeys 4. **Authenticator interaction** - Client calls `navigator.credentials.get()` with the challenge 5. **Completion** - Client calls `POST /passkeys/authenticate/complete`; the server validates the assertion and signs the user in When passkeys are used as a second factor, the flow looks like this: ``` sequenceDiagram actor User participant App participant UserManagement as User Management participant Browser User->>App: Complete primary authentication (e.g. OTP) App->>UserManagement: Begin passkey assertion UserManagement-->>App: Challenge App->>Browser: navigator.credentials.get() Browser->>User: Prompt for passkey (biometric/PIN) User->>Browser: Authenticate Browser-->>App: Assertion response App->>UserManagement: Complete passkey assertion UserManagement-->>App: Verified (2FA complete) App-->>User: Signed in ``` In code: (note an email address is used here to identify the user, this could also be a username or another attribute) ```csharp // After primary authentication succeeds, store the user ID for the second factor public async Task OnPostLogin(string email, string password, CancellationToken ct) { var result = await passwordAuth.TryAuthenticateAsync( AttributeCode.Create("email"), email, NonValidatedPassword.Create(password), ct); if (result is not PasswordAuthenticationResult.Success success) { return Error("Invalid credentials."); } var authenticators = await selfService.TryGetAsync(success.UserSubjectId, ct); if (authenticators?.Passkeys.Count > 0) { // Store the user ID for the second-factor resolver to retrieve HttpContext.Session.SetString("PendingUserId", success.UserSubjectId.Value); return RedirectToPage("/LoginWithPasskey"); } await CompleteSignIn(success.UserSubjectId); return Redirect(returnUrl ?? "/"); } ``` ## Customizing Sign-In Behavior [Section titled “Customizing Sign-In Behavior”](#customizing-sign-in-behavior) After a successful passkey authentication ceremony, User Management signs the user in and returns a JSON response to the client. The `IPasskeySignInHandler` interface controls how this sign-in happens. You can replace the default implementation to customize claims, session properties, or integrate with an external session system. ### IPasskeySignInHandler [Section titled “IPasskeySignInHandler”](#ipasskeysigninhandler) IPasskeySignInHandler.cs ```csharp public interface IPasskeySignInHandler { Task SignInAsync( HttpContext context, UserAuthenticators user, bool userVerified, bool backedUp, CancellationToken ct); } ``` The parameters provide everything you need to establish the session: * `context` - the current HTTP context for issuing cookies or interacting with authentication middleware. * `user` - the authenticated user’s authenticator information, including their `SubjectId` and registered OTP addresses. * `userVerified` - whether the authenticator performed user verification (biometric or PIN) during the ceremony. * `backedUp` - whether the passkey credential is backed up (synced across devices). * `ct` - cancellation token. Your implementation must return an `IResult` that writes the HTTP response. Use `PasskeyCompleteAuthenticationResult` to return the standard JSON response after signing in: ```csharp return new PasskeyCompleteAuthenticationResult(userVerified, backedUp); ``` ### Default behavior [Section titled “Default behavior”](#default-behavior) The built-in handler creates a `ClaimsPrincipal` with the user’s subject ID, authentication method (`passkey`), authentication time, and email (if available), then calls `HttpContext.SignInAsync` with an 8-hour persistent cookie. When using the IdentityServer integration, the sign-in handler is automatically configured for proper session management. You don’t need to register anything extra. ### Custom implementation [Section titled “Custom implementation”](#custom-implementation) Register your own handler to customize the sign-in behavior. Because the default is registered with `TryAddScoped`, your registration takes priority: ```csharp builder.Services.AddScoped(); ``` Example: adding custom claims and adjusting session lifetime: ```csharp using System.Security.Claims; using Duende.UserManagement.Authentication; using Duende.UserManagement.Authentication.Passkeys; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Http; public class MyPasskeySignInHandler : IPasskeySignInHandler { public async Task SignInAsync( HttpContext context, UserAuthenticators user, bool userVerified, bool backedUp, CancellationToken ct) { var claims = new List { new Claim("sub", user.SubjectId.Value), new Claim("amr", "passkey"), new Claim("auth_time", DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString()) }; // Add custom claims based on ceremony results if (userVerified) { claims.Add(new Claim("user_verified", "true")); } var identity = new ClaimsIdentity(claims, "passkey"); var principal = new ClaimsPrincipal(identity); var properties = new AuthenticationProperties { IsPersistent = true, ExpiresUtc = DateTimeOffset.UtcNow.AddHours(4), IssuedUtc = DateTimeOffset.UtcNow, AllowRefresh = true }; await context.SignInAsync(principal, properties); return new PasskeyCompleteAuthenticationResult(userVerified, backedUp); } } ``` ## Passkey Management UI [Section titled “Passkey Management UI”](#passkey-management-ui) ### Registering a New Passkey [Section titled “Registering a New Passkey”](#registering-a-new-passkey) The registration flow is driven by the browser’s WebAuthn API. The server endpoints handle the ceremony; your UI initiates it: ```csharp // Server-side: the begin and complete endpoints handle the ceremony. // Your page only needs to trigger the JavaScript helper. public class ManagePasskeysModel : PageModel { private readonly IUserAuthenticatorsSelfService _selfService; public ManagePasskeysModel(IUserAuthenticatorsSelfService selfService) { _selfService = selfService; } public IReadOnlyCollection RegisteredPasskeys { get; private set; } = []; public async Task OnGetAsync(CancellationToken ct) { var userId = GetCurrentUserId(); var authenticators = await _selfService.TryGetAsync(userId, ct); RegisteredPasskeys = authenticators?.Passkeys ?? []; } } ``` ### Removing a Passkey [Section titled “Removing a Passkey”](#removing-a-passkey-1) ```csharp public async Task OnPostRemovePasskey( string credentialIdBase64, CancellationToken ct) { var userId = GetCurrentUserId(); if (!Convert.TryFromBase64String(credentialIdBase64, out var credentialIdBytes)) { return Error("Invalid credential ID."); } var credentialId = PasskeyCredentialId.From(credentialIdBytes); var removed = await _selfService.TryRemovePasskeyAsync(userId, credentialId, ct); if (!removed) { return Error("Passkey not found."); } return RedirectToPage(); } ``` ## WebAuthn Enhancements [Section titled “WebAuthn Enhancements”](#webauthn-enhancements) User Management includes several improvements to the underlying WebAuthn implementation. These are active by default and no additional configuration is required, but understanding what they do helps you reason about the security posture of your passkey deployment. ### TPM Attestation Validation [Section titled “TPM Attestation Validation”](#tpm-attestation-validation) Attestation is the mechanism by which an authenticator proves its provenance: it cryptographically signs the new credential with a key that is specific to the authenticator model, allowing the server to verify that the credential was created by a genuine, known device. TPM (Trusted Platform Module) attestation is the format used by Windows Hello and other platform authenticators backed by a hardware security chip. User Management validates TPM attestation statements, which means that when `AttestationConveyancePreference` is set to `"direct"` or `"enterprise"`, TPM-attested credentials are fully verified rather than accepted without attestation checks. This matters when you need to enforce an allowlist of approved authenticator models, for example in enterprise environments where only corporate-managed devices with a TPM should be permitted to register passkeys. ### RS1 Signature Verifier Support [Section titled “RS1 Signature Verifier Support”](#rs1-signature-verifier-support) WebAuthn credentials can use different cryptographic algorithms, identified by COSE algorithm identifiers. RS1 (`RSASSA-PKCS1-v1_5` with SHA-1) is an older RSA signature algorithm that some legacy authenticators and security keys use. User Management includes a verifier for RS1 signatures. This broadens compatibility with older hardware security keys that do not support the preferred ES256 (ECDSA with P-256) or RS256 (RSASSA-PKCS1-v1\_5 with SHA-256) algorithms. Note RS1 uses SHA-1, which is considered cryptographically weak for new designs. If your security policy requires strong algorithms only, restrict `SupportedAlgorithms` to exclude RS1: ```csharp options.Passkeys.SupportedAlgorithms = [CoseAlgorithms.Es256, CoseAlgorithms.Rs256]; ``` Omitting RS1 from `SupportedAlgorithms` prevents registration of RS1-based credentials while still allowing RS1 verification for credentials already registered before the restriction was applied. ### Expired Challenge Cleanup [Section titled “Expired Challenge Cleanup”](#expired-challenge-cleanup) Each WebAuthn ceremony begins with the server issuing a cryptographic challenge. Challenges are stored server-side and expire after `ChallengeTimeout` (default: 5 minutes). Expired challenges that were never completed (for example, because the user abandoned the browser prompt) previously accumulated in the challenge store. User Management automatically removes expired challenges. This keeps the challenge store lean and prevents unbounded growth in long-running applications with high registration or authentication traffic. No configuration is required. Cleanup runs as part of normal challenge lifecycle management. ### Constant-Time Challenge Comparison [Section titled “Constant-Time Challenge Comparison”](#constant-time-challenge-comparison) When the server validates a completed WebAuthn ceremony, it compares the challenge returned by the authenticator against the challenge it originally issued. A naive string or byte comparison can leak timing information: an attacker who can measure response times precisely might infer how many bytes matched before the comparison failed. User Management uses a constant-time comparison for challenge validation. The comparison always takes the same amount of time regardless of how many bytes match, eliminating this timing side-channel. This is a defense-in-depth measure. WebAuthn challenges are already large (32 bytes of random data by default) and single-use, so the practical exploitability of a timing side-channel is very low. Constant-time comparison removes it entirely. ## Attestation Trust Policies [Section titled “Attestation Trust Policies”](#attestation-trust-policies) By default, User Management accepts any authenticator during passkey registration. If you need to restrict which authenticators are allowed, you can implement `IAttestationTrustPolicy`. Common reasons to do this include: * Enforcing enterprise security policies that permit only corporate-managed hardware * Requiring FIDO-certified authenticators for regulated environments * Allowlisting specific authenticator models by their AAGUID ### IAttestationTrustPolicy [Section titled “IAttestationTrustPolicy”](#iattestationtrustpolicy) Implement `IAttestationTrustPolicy` (in `Duende.UserManagement.Authentication.Passkeys`) to evaluate each authenticator at registration time: IAttestationTrustPolicy.cs ```csharp public interface IAttestationTrustPolicy { ValueTask EvaluateAsync(AttestationTrustContext context, CancellationToken ct); } ``` Your implementation receives an `AttestationTrustContext` and returns either `Accept()` or `Reject(reason)`. ### AttestationTrustContext [Section titled “AttestationTrustContext”](#attestationtrustcontext) `AttestationTrustContext` carries all the information you need to make a trust decision: AttestationTrustContext.cs ```csharp public sealed record AttestationTrustContext { public required UserSubjectId UserSubjectId { get; init; } public required Guid Aaguid { get; init; } public required string AttestationFormat { get; init; } public IReadOnlyList? CertificateChain { get; init; } } ``` * `UserSubjectId`: the user registering the credential, so you can make per-user trust decisions if needed * `Aaguid`: the authenticator model identifier from the attested credential data; use this to allowlist or blocklist specific models * `AttestationFormat`: the attestation format string (for example, `"none"`, `"packed"`, `"tpm"`) * `CertificateChain`: DER-encoded certificate bytes from the attestation statement, if present. Index 0 is the attestation certificate. This is `null` when the format carries no certificates (for example, `"none"` or self-attestation) ### AttestationTrustPolicyResult [Section titled “AttestationTrustPolicyResult”](#attestationtrustpolicyresult) Return `Accept()` to allow the registration to proceed, or `Reject(reason)` to block it: ```csharp // Accept the authenticator return AttestationTrustPolicyResult.Accept(); // Reject with a reason return AttestationTrustPolicyResult.Reject("Authenticator not in allowlist."); ``` ### Registering a Custom Policy [Section titled “Registering a Custom Policy”](#registering-a-custom-policy) Register your policy using `AddAttestationTrustPolicy()` on the authentication builder: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.AddAttestationTrustPolicy(); }) ); ``` ### Example: AAGUID Allowlist [Section titled “Example: AAGUID Allowlist”](#example-aaguid-allowlist) The following example shows a complete policy that only accepts authenticators whose AAGUID appears in a known-good list: MyAttestationTrustPolicy.cs ```csharp using Duende.UserManagement.Authentication.Passkeys; public class MyAttestationTrustPolicy : IAttestationTrustPolicy { // Known-good authenticator AAGUIDs (e.g. YubiKey 5 series) private static readonly HashSet AllowedAaguids = [ Guid.Parse("2fc0579f-8113-47ea-b116-bb5a8db9202a"), // YubiKey 5 NFC Guid.Parse("c1f9a0bc-1dd2-404a-b27f-8e29047a43fd"), // YubiKey 5C ]; public ValueTask EvaluateAsync( AttestationTrustContext context, CancellationToken ct) { if (AllowedAaguids.Contains(context.Aaguid)) return ValueTask.FromResult(AttestationTrustPolicyResult.Accept()); return ValueTask.FromResult( AttestationTrustPolicyResult.Reject($"Authenticator {context.Aaguid} is not in the allowlist.")); } } ``` Tip `AAGUID` values for well-known authenticators are published by the FIDO Alliance in the [FIDO Metadata Service](https://fidoalliance.org/metadata/). You can use the metadata service to look up AAGUIDs for specific authenticator models and build your allowlist from there. ----- # Password Authentication Flow > How to implement password-based authentication in Duende User Management, including the IPasswordAuthenticator interface, password lifecycle management, configuration options, and custom validation extensibility. Security warning Password-only authentication carries significant risks including phishing attacks, credential stuffing, and password reuse. Passkey-based authentication is strongly recommended for the strongest security posture. If you must use passwords, extend them with an additional factor such as [OTP](/identityserver/identity/user-management/authentication/otp/) or [TOTP](/identityserver/identity/user-management/authentication/totp/). Password authentication is the traditional credential-based flow where users create and manage a password tied to their account. Duende User Management supports passwords as one of several authentication flows, with built-in PBKDF2 hashing, timing-attack protection, configurable complexity rules, and extensible validation. ## Account Recovery Considerations [Section titled “Account Recovery Considerations”](#account-recovery-considerations) A fundamental challenge with standalone password authentication is self-service account recovery: there is no way to deliver a password reset token without first verifying that the user controls the delivery channel (email or SMS). User Management addresses this by design. Creating a user without first verifying a One-Time Password (OTP) channel is not possible via the `IUserSelfService` interface. By default, a user verifies ownership of their email address via OTP, then creates a password. Whether to allow password-only login or to always require an additional OTP factor during login is a decision left to your application. Password policy configuration Password policies (minimum/maximum length, complexity requirements) are configured via `PasswordOptions` in the service provider registration. See the [configuration reference](/identityserver/identity/user-management/reference/configuration/) for all available options. ## Key Interfaces [Section titled “Key Interfaces”](#key-interfaces) ### IPasswordAuthenticator [Section titled “IPasswordAuthenticator”](#ipasswordauthenticator) `IPasswordAuthenticator` is the primary interface for verifying a user’s password during login. Inject it into your login page or controller to authenticate a user by a unique attribute (like an email address, username, …) and password. Its single method returns a `PasswordAuthenticationResult` discriminated union indicating success or failure: ```csharp public interface IPasswordAuthenticator { Task TryAuthenticateAsync( AttributeCode code, object value, NonValidatedPassword password, CancellationToken ct); } ``` `TryAuthenticateAsync` returns a `PasswordAuthenticationResult`, which is a discriminated union with three subtypes: * `PasswordAuthenticationResult.Success` — contains the user’s `UserSubjectId`. The credentials are valid. * `PasswordAuthenticationResult.Failure` — the credentials are invalid. * `PasswordAuthenticationResult.Expired` — contains the user’s `UserSubjectId`. The credentials are correct, but the password has passed its maximum age and must be changed. The comparison runs in constant time to prevent account enumeration via timing attacks. ### Validating Passwords [Section titled “Validating Passwords”](#validating-passwords) `ValidatedPlainTextPassword` is a validated value type that cannot be constructed directly. To create one, use the factory methods on `IUserAuthenticatorsSelfService`. These methods validate the password string against the full set of rules configured in [`PasswordOptions`](#passwordoptions) and return a `ValidatedPlainTextPassword` on success. They do not store the password or modify the user’s account. You pass the resulting `ValidatedPlainTextPassword` to a lifecycle method like `TrySetPasswordAsync` or `TryChangePasswordAsync` to actually persist it. Both factory methods take the `UserSubjectId` of the user setting the password as their first argument, so custom validators can apply per-user policy if needed: IUserAuthenticatorsSelfService.cs ```csharp public interface IUserAuthenticatorsSelfService { // ... // Validates the password string and returns a ValidatedPlainTextPassword. Does NOT set the password. // Throws FormatException if the password does not meet requirements. Task ValidatePasswordAsync(UserSubjectId userId, string passwordString, CancellationToken ct); // Validates the password string and returns a PasswordCreationResult. Does NOT set the password. // Returns a PasswordCreationResult indicating success or failure. Task TryValidatePasswordAsync(UserSubjectId userId, string passwordString, CancellationToken ct); // ... } ``` `PasswordCreationResult` is a discriminated union with two cases: PasswordCreationResult.cs ```csharp public abstract record PasswordCreationResult { // The password passed all validation rules. public sealed record Success(ValidatedPlainTextPassword Password) : PasswordCreationResult; // The password failed one or more validation rules. // Errors contains human-readable reasons suitable for display to the user. public sealed record Failed(IReadOnlyList Errors) : PasswordCreationResult; } ``` Use `TryValidatePasswordAsync` in user-facing flows where you want to return a validation error rather than catch an exception. Pattern match on `PasswordCreationResult.Success` to extract the validated password, or on `PasswordCreationResult.Failed` to surface the specific failure reasons to the user, such as “Password must contain at least 2 uppercase letters.” The key distinction between `ValidatedPlainTextPassword` and `NonValidatedPassword` is *when* you use each one: * Use `IUserAuthenticatorsSelfService.ValidatePasswordAsync(userId, ...)` or `TryValidatePasswordAsync(userId, ...)` when the user is **setting or changing** a password. These are factory methods that validate the input and return a `ValidatedPlainTextPassword` object. They do not persist anything. Pass the result to a lifecycle method like `TrySetPasswordAsync` or `TryChangePasswordAsync` to store it. * Use `NonValidatedPassword.Create()` when the user is **logging in** with an existing password. Validation rules are intentionally skipped because the rules may have changed since the password was created. For example, if the minimum length increased from 8 to 16 characters after a user set a 12-character password, that password would never pass the new validation and the user could never log in. #### NonValidatedPassword [Section titled “NonValidatedPassword”](#nonvalidatedpassword) When logging in, use `NonValidatedPassword` rather than `ValidatedPlainTextPassword`. Validation rules are intentionally skipped at authentication time because those rules may have changed since the password was created. For example, a user whose password was valid when they set it should always be able to log in, even if the policy has since tightened. `NonValidatedPassword` still performs basic sanity checks (not null, not empty) to catch invalid input. Use `TryCreate` in user-facing flows where you want to return a validation error rather than catch an exception. ```csharp public record NonValidatedPassword { public static NonValidatedPassword Create(string passwordString); public static bool TryCreate(string? passwordString, [NotNullWhen(true)] out NonValidatedPassword? result); public static bool TryCreate(string? passwordString, [NotNullWhen(true)] out NonValidatedPassword? result, [NotNullWhen(false)] out IReadOnlyList? errors); } ``` ## Password Lifecycle Methods [Section titled “Password Lifecycle Methods”](#password-lifecycle-methods) Password lifecycle operations (setting, changing, and resetting passwords) are available on `IUserAuthenticatorsSelfService`. These methods manage the stored credential rather than verifying it. ```csharp public interface IUserAuthenticatorsSelfService { // Sets a new password for the specified user without requiring the current password. // Returns false if the user record does not exist. Task TrySetPasswordAsync( UserSubjectId subjectId, ValidatedPlainTextPassword password, CancellationToken ct); // Changes a password by verifying the current password first. Task TryChangePasswordAsync( UserSubjectId subjectId, NonValidatedPassword oldPassword, ValidatedPlainTextPassword newPassword, CancellationToken ct); // Resets a password without requiring the current password. // Use only after verifying the user's identity via another channel (e.g., OTP). Task TryResetPasswordAsync( UserSubjectId subjectId, ValidatedPlainTextPassword password, CancellationToken ct); // ... other authenticator management methods } ``` * `TrySetPasswordAsync` - Sets a new password without requiring the current password. Returns `false` if the user record does not exist. The user must already have an authenticator record before you can set a password. If the user has no other form of authentication, create an empty authenticator first: ```csharp // Create an empty authenticator record for the user await authenticatorsAdmin.TryAddAsync(profile.SubjectId, [], [], cancellationToken); ``` * `TryChangePasswordAsync` - Use for authenticated password changes; requires the current password to be provided and verified. * `TryResetPasswordAsync` - Use for password reset flows after the user’s identity has been verified via a separate channel such as OTP. Does not require the current password. All three methods return `true` on success and `false` if the operation could not be completed (for example, `TryChangePasswordAsync` returns `false` if the current password is incorrect). ## Configuration [Section titled “Configuration”](#configuration) ### PasswordOptions [Section titled “PasswordOptions”](#passwordoptions) Password complexity requirements are configured via `PasswordOptions`, accessible through the top-level options object when registering User Management services. Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.Configure(options => { options.Passwords.MinLength = 12; // other PasswordOptions... }); }) ); ``` | Property | Default | Description | | ------------------------ | ---------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MinLength` | `8` | Minimum password length in characters. | | `MaxLength` | `64` | Maximum password length; capped at 64 to avoid PBKDF2 pre-hashing vulnerabilities with SHA-512. | | `MinLower` | `2` | Minimum number of lowercase letters required. | | `MinUpper` | `2` | Minimum number of uppercase letters required. | | `MinDigits` | `2` | Minimum number of numeric digit characters required. | | `MinSymbols` | `2` | Minimum number of symbol (non-alphanumeric) characters required. | | `HistoryCount` | `0` | Number of previous passwords to remember and reject on change or reset; `0` disables history. | | `MaxAgeDays` | `null` | Maximum password age in days before the password is considered expired; `null` disables expiration. | | `PreferredHashAlgorithm` | `"pbkdf2"` | Algorithm ID used when hashing new passwords and when re-hashing on login. See [Password Hashing Algorithms](/identityserver/identity/user-management/reference/password-hashing/). | The `MaxLength` default of 64 comes from the PBKDF2/SHA-512 security limit. Passwords longer than 128 bytes (64 UTF-16 characters) can trigger pre-hashing behavior in PBKDF2 that weakens the key derivation. See the [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2-pre-hashing) for background. ## Password History [Section titled “Password History”](#password-history) When `HistoryCount` is set to a value greater than 0, password history validation is automatically enforced. The system retains the hashes of the user’s most recent passwords (up to `HistoryCount` entries) and rejects any new password that matches one of them. This prevents users from cycling back to a recently used password. Both `TryChangePasswordAsync` and `TryResetPasswordAsync` check the candidate password against the stored history and return `false` if it matches any of the retained entries. Program.cs ```csharp options.Passwords.HistoryCount = 5; ``` The default value of `0` disables history checking entirely, so no previous passwords are stored or compared. ## Password Expiration [Section titled “Password Expiration”](#password-expiration) When `MaxAgeDays` is set to a positive integer, `TryAuthenticateAsync` checks whether the user’s password is older than that many days. If it is, the method returns `PasswordAuthenticationResult.Expired` instead of `PasswordAuthenticationResult.Success`. The credentials are correct, but the user must change their password before continuing. If the system does not know when the password was set (for example, for accounts migrated from an external store without a creation timestamp), the password is treated as expired immediately. A value of `null` (the default) disables expiration entirely. Because `TryAuthenticateAsync` can return distinct results, your login handler should pattern-match on all three: LoginPage.cshtml.cs ```csharp public async Task OnPostLogin(string email, string password) { var result = await passwordAuth.TryAuthenticateAsync( AttributeCode.Create("email"), email, NonValidatedPassword.Create(password), ct); return result switch { PasswordAuthenticationResult.Success success => await CompleteSignIn(success.UserSubjectId), PasswordAuthenticationResult.Expired expired => RedirectToPage("/ChangePassword", new { userId = expired.UserSubjectId }), PasswordAuthenticationResult.Failure => Error("Invalid username or password"), _ => Error("Unexpected authentication result") }; } ``` ## Custom Password Validation [Section titled “Custom Password Validation”](#custom-password-validation) Beyond the built-in complexity rules, you can implement `IPasswordValidator` to add custom policy checks such as blocklist enforcement, breach database lookups (e.g., Have I Been Pwned), or dictionary word rejection. The `ValidateAsync` method receives the `UserSubjectId` of the user setting the password, the candidate password string, and a cancellation token. ```csharp public interface IPasswordValidator { Task ValidateAsync(UserSubjectId userId, string password, CancellationToken ct); } ``` `PasswordValidationResult` is a discriminated union with two cases: ```csharp public abstract record PasswordValidationResult { // The password passed validation. public sealed record Accepted : PasswordValidationResult; // The password failed validation. // Reason is a human-readable explanation suitable for display to the user. public sealed record Rejected(string Reason) : PasswordValidationResult; } ``` ### Implementing a Custom Validator [Section titled “Implementing a Custom Validator”](#implementing-a-custom-validator) You can implement a custom validator by inheriting from the `IPasswordValidator` interface. The `userId` parameter lets you apply per-user logic, such as rejecting passwords that contain the user’s own identifier. The following example rejects passwords found in a common-password blocklist: ```csharp using Duende.UserManagement.Authentication.Passwords; public class BlocklistPasswordValidator : IPasswordValidator { private static readonly HashSet CommonPasswords = [ "Password1!", "Welcome1!", "Summer2024!" ]; public Task ValidateAsync(UserSubjectId userId, string password, CancellationToken ct) { if (CommonPasswords.Contains(password)) { return Task.FromResult( new PasswordValidationResult.Rejected( "This password is too common. Please choose a more unique password.")); } return Task.FromResult( new PasswordValidationResult.Accepted()); } } ``` Register the custom validator with the service provider. Multiple `IPasswordValidator` implementations can be registered. You can register the validator directly with: ```csharp services.AddTransient(); ``` Or you can use the helper method when configuring user authentication: ```csharp using Duende.IdentityServer; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.AddPasswordValidator(); }) ); ``` ### When Password Validation Runs [Section titled “When Password Validation Runs”](#when-password-validation-runs) Validations run in registration order, and the first rejection stops further evaluation. Both the built-in complexity checks (length, character class requirements from [`PasswordOptions`](#passwordoptions)) and any registered `IPasswordValidator` implementations run inside `TryValidatePasswordAsync` at validation time. This means validation happens when you call `TryValidatePasswordAsync`, before the password is passed to any lifecycle method. The [password lifecycle methods](#password-lifecycle-methods) accept an already-validated `ValidatedPlainTextPassword` and do not re-run the validators. ## Security [Section titled “Security”](#security) Passwords are the most attacked credential type on the internet: reused, guessed, phished, and leaked constantly. User Management supports them because some applications need them, but the defaults are designed to make the worst outcomes less likely. ### What User Management Does for You [Section titled “What User Management Does for You”](#what-user-management-does-for-you) Passwords are hashed with PBKDF2-HMAC-SHA-512 at 210000 iterations, following the [OWASP recommendation](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2). Each password gets a unique salt, so two users with the same password have different hashes and rainbow table attacks are useless. The `MaxLength` is capped at 64 characters to avoid a PBKDF2 pre-hashing vulnerability that appears when passwords exceed the HMAC-SHA-512 block size. `TryAuthenticateAsync` uses constant-time comparison throughout, so an attacker cannot determine whether an account exists by measuring response times. `ValidatedPlainTextPassword` and `NonValidatedPassword` intentionally return the type name from `ToString()` to prevent accidental logging. ### What You Need to Think About [Section titled “What You Need to Think About”](#what-you-need-to-think-about) The default minimum password length of 8 characters is a floor, not a recommendation. For new applications, 12-16 characters is a more defensible baseline. Consider plugging in an `IPasswordValidator` that checks submitted passwords against the [Have I Been Pwned](https://haveibeenpwned.com/API/v3#searchingPwnedPasswordsByRange) k-anonymity API. Rejecting passwords that appear in known breach datasets is one of the highest-value things you can do to reduce credential stuffing risk. Never use passwords as the only factor. They are phishable, reused across services, and leaked regularly. Pair them with TOTP at minimum, or push users toward passkeys for sensitive operations. One thing that catches people out: `TryResetPasswordAsync` does not require the current password. That is intentional; it is for password reset flows where the user has already proved their identity via OTP. But it means your application is responsible for that identity verification step. Calling `TryResetPasswordAsync` without first confirming who the user is would be a serious security hole. For cross-cutting security topics (data protection key persistence, throttling configuration, and password hashing parameters) see [Security Considerations](/identityserver/identity/user-management/fundamentals/security/). ## Authentication Flow [Section titled “Authentication Flow”](#authentication-flow) The password authentication flow has two paths: a success path where valid credentials produce a subject ID, and a failure path where invalid credentials, throttling, or lockout prevent access. ### Success Path [Section titled “Success Path”](#success-path) The user submits valid credentials and is authenticated in constant time: ``` sequenceDiagram actor User participant App participant UserManagement as User Management User->>App: Submit credentials App->>UserManagement: TryAuthenticateAsync(attributeCode, value, password) Note over UserManagement: Constant-time PBKDF2 comparison UserManagement-->>App: PasswordAuthenticationResult.Success App-->>User: Signed in ✓ ``` ### Failure Path [Section titled “Failure Path”](#failure-path) When credentials are wrong, the system returns a `PasswordAuthenticationResult.Failure` without revealing whether an account exists. Repeated failures may trigger throttling or account lockout depending on your security configuration: ``` sequenceDiagram actor User participant App participant UserManagement as User Management User->>App: Submit credentials App->>UserManagement: TryAuthenticateAsync(attributeCode, value, password) Note over UserManagement: Constant-time comparison (prevents timing enumeration) UserManagement-->>App: PasswordAuthenticationResult.Failure App-->>User: Generic error message ✗ Note over App: Do not reveal whether an account exists ``` ### Login [Section titled “Login”](#login) Inject `IPasswordAuthenticator` into your login handler. Use `NonValidatedPassword.Create(password)` when calling `TryAuthenticateAsync`. This skips policy validation so that users whose passwords predate a rule change can still log in. Pattern-match on all three result types: redirect to a password-change page on `Expired`, and show a generic error on `Failure` to avoid revealing whether an account exists. After a successful password check, optionally redirect to a second-factor page if the user has TOTP configured: LoginPage.cshtml.cs ```csharp public async Task OnPostLogin(string email, string password) { var result = await passwordAuth.TryAuthenticateAsync( AttributeCode.Create("email"), email, NonValidatedPassword.Create(password), ct); if (result is PasswordAuthenticationResult.Failure) return Error("Invalid username or password"); if (result is PasswordAuthenticationResult.Expired expired) return RedirectToPage("/ChangePassword", new { userId = expired.UserSubjectId }); var success = (PasswordAuthenticationResult.Success)result; var user = await userAuthenticatorsSelfService.TryGetAsync(success.UserSubjectId, ct); // Optionally check for a second factor before completing sign-in. if (user?.TotpDeviceNames.Count > 0) { StoreAuthState(success.UserSubjectId); return RedirectToPage("/LoginWith2FA"); } await SignIn(user); return RedirectToPage("/Index"); } ``` ### Password Change (Authenticated User) [Section titled “Password Change (Authenticated User)”](#password-change-authenticated-user) Use `TryChangePasswordAsync` when the user is already signed in and wants to update their password. Use `NonValidatedPassword.Create` for the current password (authentication path) and `authenticatorsSelfService.TryValidatePasswordAsync` for the new password (applies full validation rules): ```csharp public async Task OnPostChangePassword( string currentPassword, string newPassword, string confirmNewPassword, CancellationToken ct) { if (newPassword != confirmNewPassword) return Error("New passwords do not match"); var creationResult = await authenticatorsSelfService.TryValidatePasswordAsync(GetCurrentUserId(), newPassword, ct); if (creationResult is not PasswordCreationResult.Success { Password: var validatedNewPassword }) return Error("New password does not meet requirements"); var success = await authenticatorsSelfService.TryChangePasswordAsync( GetCurrentUserId(), NonValidatedPassword.Create(currentPassword), validatedNewPassword, ct); if (!success) return Error("Current password is incorrect"); return Success("Password changed successfully"); } ``` ### Password Reset (After OTP Verification) [Section titled “Password Reset (After OTP Verification)”](#password-reset-after-otp-verification) Use `TryResetPasswordAsync` at the end of a forgot-password flow, after the user’s identity has been confirmed via OTP. This method does not require the current password: ```csharp // Step 1: Verify user identity via OTP (see OTP flow documentation). // Step 2: Once identity is confirmed, reset the password directly. public async Task OnPostCompleteReset(string newPassword, CancellationToken ct) { var creationResult = await authenticatorsSelfService.TryValidatePasswordAsync(GetVerifiedUserId(), newPassword, ct); if (creationResult is not PasswordCreationResult.Success { Password: var validatedPassword }) return Error("Password does not meet requirements"); var success = await authenticatorsSelfService.TryResetPasswordAsync( GetVerifiedUserId(), validatedPassword, ct); if (!success) return Error("Password reset failed"); return RedirectToPage("/Login"); } ``` ## Combining Passwords with a Second Factor [Section titled “Combining Passwords with a Second Factor”](#combining-passwords-with-a-second-factor) The recommended pattern when using passwords is to combine them with Time-Based One-Time Password (TOTP) for two-factor authentication: * **First Factor** — Password (something you know) * **Second Factor** — TOTP code (something you have) * **Backup** — Recovery codes (something you saved) See [TOTP Authentication Flow](/identityserver/identity/user-management/authentication/totp/) for the second-factor implementation, [Passkeys as Second Factor](/identityserver/identity/user-management/authentication/passkeys#second-factor-passkey-authentication) for using passkeys as a second step, and [Recovery Codes](/identityserver/identity/user-management/authentication/recovery-codes/) for backup access. ## Multi-Algorithm Password Hashing [Section titled “Multi-Algorithm Password Hashing”](#multi-algorithm-password-hashing) User Management supports multiple password hashing algorithms simultaneously, enabling transparent migration from legacy hashes to stronger algorithms. Each stored hash carries an algorithm identifier, and the system automatically re-hashes passwords on successful login when a stronger algorithm is available. The `PreferredHashAlgorithm` option in `PasswordOptions` controls which algorithm is used for new password hashes and for re-hashing on login. See [Password Hashing Algorithms](/identityserver/identity/user-management/reference/password-hashing/) for the full list of built-in algorithms and how to register custom ones. ----- # Recovery Code Authentication > How to implement recovery code authentication as a backup two-factor authentication method using Duende User Management, including code generation, authentication, and regeneration. Recovery codes are a backup authentication method for when the primary two-factor mechanism (typically Time-Based One-Time Password (TOTP)) is unavailable. Each code is single-use, so a user who loses their device or breaks their authenticator app can still get back in. ## Comparison With Other Authentication Methods [Section titled “Comparison With Other Authentication Methods”](#comparison-with-other-authentication-methods) | Aspect | Recovery Codes | TOTP | Password Reset | | ------------------- | ---------------- | ----------------- | --------------- | | **Use Case** | Emergency backup | Primary 2FA | Lost password | | **Frequency** | Rare | Every login | Occasional | | **Device Required** | None | Authenticator app | Email access | | **Security Model** | Single-use | Time-based | Email dependent | | **User Burden** | Must save codes | Install app | Email access | ## How It Works [Section titled “How It Works”](#how-it-works) ### Generation Flow [Section titled “Generation Flow”](#generation-flow) When a user enables 2FA, the system generates a set of recovery codes (typically 10), displays them once for the user to save securely, and stores only their hashes. Codes remain valid until used or regenerated. ``` sequenceDiagram actor User participant App participant UserManagement as User Management User->>App: Enable TOTP authenticator App->>UserManagement: TryAddTotpDeviceAsync() UserManagement-->>App: Success App->>UserManagement: TryCreateRecoveryCodesAsync() UserManagement-->>App: Plain-text codes (shown once) App-->>User: Display codes (save these securely) Note over UserManagement: Codes stored as hashes only ``` ### Authentication Flow [Section titled “Authentication Flow”](#authentication-flow) When a user can’t access their authenticator app, they enter a saved recovery code instead. The system verifies it against stored hashes, consumes it immediately (single-use), and issues an MFA claim. The user is warned if their remaining code count is low. ``` sequenceDiagram actor User participant App participant UserManagement as User Management User->>App: Complete primary authentication App-->>User: 2FA required User->>App: Select "Use recovery code" User->>App: Enter recovery code App->>UserManagement: TryAuthenticateAsync(subjectId, code) UserManagement-->>App: Valid (code consumed) App-->>User: Signed in + warning if codes running low ``` ## Key Interfaces [Section titled “Key Interfaces”](#key-interfaces) ### IRecoveryCodeAuthenticator [Section titled “IRecoveryCodeAuthenticator”](#irecoverycodeauthenticator) `IRecoveryCodeAuthenticator` is the primary interface for verifying and consuming recovery codes during authentication: ```csharp public interface IRecoveryCodeAuthenticator { Task TryAuthenticateAsync( UserSubjectId subjectId, PlainTextRecoveryCode recoveryCode, CancellationToken ct); } ``` The method verifies the supplied code against stored hashes and, if valid, marks it as consumed so it cannot be reused. ### IUserAuthenticatorsSelfService [Section titled “IUserAuthenticatorsSelfService”](#iuserauthenticatorsselfservice) `IUserAuthenticatorsSelfService` exposes recovery code management for authenticated users: ```csharp // Generate new recovery codes. Invalidates all existing codes Task?> TryCreateRecoveryCodesAsync( UserSubjectId subjectId, CancellationToken ct); ``` Returns `null` if code generation fails (for example, when 2FA is not enabled for the user). ### PlainTextRecoveryCode [Section titled “PlainTextRecoveryCode”](#plaintextrecoverycode) `PlainTextRecoveryCode` represents a recovery code value and provides parsing and display helpers: ```csharp public record PlainTextRecoveryCode { // Create from a user-supplied string into a recovery code public static bool TryCreate(string? input, [NotNullWhen(true)] out PlainTextRecoveryCode? result); // Create or throw on invalid input public static PlainTextRecoveryCode Create(string input); // Format into groups for display, e.g. ["7e8a", "9b2c", "4d1f"] public IReadOnlyCollection ToTextGroups(); } ``` ### User Properties [Section titled “User Properties”](#user-properties) The `User` object exposes the count of remaining unused recovery codes: ```csharp var user = await userSelfService.TryGetUserAsync(subjectId, ct); // Number of unused recovery codes remaining int remaining = user.RecoveryCodeCount; ``` ## Implementation Patterns [Section titled “Implementation Patterns”](#implementation-patterns) ### Generating Recovery Codes [Section titled “Generating Recovery Codes”](#generating-recovery-codes) Recovery codes are typically generated when the user enables TOTP. Call `TryCreateRecoveryCodesAsync` after successfully activating the authenticator: ```csharp public async Task OnPostEnableTotpAsync(string verificationCode) { var subjectId = GetCurrentUserId(); var success = await userAuthenticatorsSelfService.TryAddTotpDeviceAsync( subjectId, TotpDeviceName.Default, totpKey, PlainTextTotp.Create(verificationCode), ct); if (!success) { return Error("Invalid verification code"); } // Generate recovery codes after enabling TOTP var recoveryCodes = await userAuthenticatorsSelfService.TryCreateRecoveryCodesAsync( subjectId, ct); if (recoveryCodes == null) { return Error("Failed to generate recovery codes"); } // Format codes for display var formattedCodes = recoveryCodes .Select(code => string.Join("-", code.ToTextGroups())) .ToArray(); TempData["RecoveryCodes"] = formattedCodes; return RedirectToPage("/ShowRecoveryCodes"); } ``` ### Authenticating With a Recovery Code [Section titled “Authenticating With a Recovery Code”](#authenticating-with-a-recovery-code) After the user completes primary authentication and selects the recovery code option, verify and consume the code: ```csharp public async Task OnPostLoginWithRecoveryCodeAsync(string recoveryCode) { var authState = GetAuthState(); if (authState == null) { return RedirectToPage("/Login"); } // Strip spaces and dashes. Accept flexible input formats var cleanCode = recoveryCode .Replace(" ", string.Empty) .Replace("-", string.Empty); if (!PlainTextRecoveryCode.TryCreate(cleanCode, out var code)) { return Error("Invalid recovery code format"); } // Verify and consume the recovery code var success = await recoveryCodeAuth.TryAuthenticateAsync( authState.UserId, code, ct); if (!success) { return Error("Invalid recovery code"); } ClearAuthState(); var user = await userSelfService.TryGetUserAsync(authState.UserId, ct); await SignInWithMfaAsync(user, authState.RememberMe); // Warn the user if they are running low on codes if (user.RecoveryCodeCount < 3) { TempData["Warning"] = $"You have {user.RecoveryCodeCount} recovery codes remaining. " + "Consider generating new ones."; } return RedirectToPage("/Index"); } ``` ### Regenerating Recovery Codes [Section titled “Regenerating Recovery Codes”](#regenerating-recovery-codes) Users can regenerate codes at any time from their account settings. Regeneration invalidates all existing codes: ```csharp public async Task OnPostRegenerateCodesAsync() { var subjectId = GetCurrentUserId(); var user = await userSelfService.TryGetUserAsync(subjectId, ct); // Verify 2FA is enabled before generating codes if (user.TotpDeviceNames.Count == 0) { return Error("Two-factor authentication is not enabled"); } // Generate new codes. All old codes are immediately invalidated var recoveryCodes = await userAuthenticatorsSelfService.TryCreateRecoveryCodesAsync( subjectId, ct); if (recoveryCodes == null) { return Error("Failed to generate recovery codes"); } var formattedCodes = recoveryCodes .Select(code => string.Join("-", code.ToTextGroups())) .ToArray(); TempData["RecoveryCodes"] = formattedCodes; TempData["Message"] = "New recovery codes generated. Old codes are no longer valid."; return RedirectToPage("/ShowRecoveryCodes"); } ``` ### Displaying Recovery Code Status [Section titled “Displaying Recovery Code Status”](#displaying-recovery-code-status) Show the user how many codes they have remaining and prompt them to regenerate when running low: ```csharp public async Task OnGetAsync() { var subjectId = GetCurrentUserId(); var user = await userSelfService.TryGetUserAsync(subjectId, ct); if (user.TotpDeviceNames.Count == 0) { return RedirectToPage("/EnableAuthenticator"); } ViewData["RecoveryCodesRemaining"] = user.RecoveryCodeCount; return Page(); } ``` ## Configuration [Section titled “Configuration”](#configuration) You can control how recovery codes are generated and whether they are enabled at all. ### RecoveryCodeOptions [Section titled “RecoveryCodeOptions”](#recoverycodeoptions) Recovery code behavior is configured via `RecoveryCodeOptions`, accessible through the top-level options object when registering User Management services. Program.cs ```csharp using Duende.IdentityServer; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.Configure(options => { options.RecoveryCodes.Count = 8; options.RecoveryCodes.Enabled = true; }); }) ); ``` | Property | Type | Default | Description | | --------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Count` | `int` | `10` | Number of recovery codes generated per call to `TryCreateRecoveryCodesAsync`. Valid range is 1 to 50. | | `Enabled` | `bool` | `true` | When set to `false`, recovery codes are disabled entirely. `TryCreateRecoveryCodesAsync` returns `null` and `TryAuthenticateAsync` returns `false` for all recovery code attempts. | ### Disabling recovery codes [Section titled “Disabling recovery codes”](#disabling-recovery-codes) You may want to set `Enabled = false` if your application only supports TOTP or passkeys as second factors and you don’t want recovery codes as a fallback. For example, some high-security applications prefer to require users to contact support for account recovery rather than relying on stored codes. ```csharp // Program.cs - disable recovery codes entirely auth.Configure(options => { options.RecoveryCodes.Enabled = false; }); ``` When `Enabled` is `false`, any call to `TryCreateRecoveryCodesAsync` returns `null`, and any call to `TryAuthenticateAsync` returns `false`, regardless of what codes the user may have stored. ## Security [Section titled “Security”](#security) Recovery codes are the safety net for two-factor authentication. They exist for the moment when a user loses their phone, breaks their authenticator app, or otherwise cannot use their primary second factor. That makes them valuable, and it makes them a target. ### What User Management Does for You [Section titled “What User Management Does for You”](#what-user-management-does-for-you) Codes are hashed with PBKDF2 before storage, so a stolen database row cannot be used to recover the plaintext. Each code is single-use and invalidated immediately on successful verification. Codes are generated with a cryptographically secure random number generator using Base32 Crockford encoding for readability and case-insensitivity. ### What You Need to Think About [Section titled “What You Need to Think About”](#what-you-need-to-think-about) The security of recovery codes depends almost entirely on what users do with them after generation. A code stored in a plain text file on the desktop, or in an unencrypted notes app, is effectively public. Your UI should tell users clearly where to store them. A password manager or printed and kept somewhere physically secure are the standard recommendations. Show a warning when a user is running low. A user who uses their last recovery code and does not generate new ones has no fallback if they lose their second factor. Prompt regeneration when fewer than 2–3 codes remain. Treat the code display screen as a sensitive operation. Never show recovery codes without first confirming the user’s session is authenticated. And make it easy to regenerate. A user who has used a code should be encouraged to generate a fresh set immediately, so the old set is fully invalidated. For cross-cutting security topics (data protection key persistence and throttling configuration) see [Security Considerations](/identityserver/identity/user-management/fundamentals/security/). ## Post-Recovery Guidance [Section titled “Post-Recovery Guidance”](#post-recovery-guidance) After a user authenticates with a recovery code, guide them to restore their normal 2FA setup: ```csharp if (usedRecoveryCode) { TempData["PostRecoveryMessage"] = "You signed in with a recovery code. " + "Set up your authenticator app and generate new recovery codes " + "to keep your account secure."; } ``` ----- # TOTP Authentication Flow > How to implement TOTP (Time-Based One-Time Password) two-factor authentication using Duende User Management, including setup, verification, and recovery code management. TOTP (Time-Based One-Time Password) adds a second factor using time-synchronized codes from an authenticator app. It strengthens security by requiring both something the user knows (password or OTP) and something the user has (an authenticator device). ## When to Use TOTP [Section titled “When to Use TOTP”](#when-to-use-totp) **Strongly recommended for:** * Financial applications, banking, and payments * Healthcare systems handling protected health information * Administrative and privileged access * Regulated industries with compliance requirements (PCI-DSS, HIPAA, etc.) **Good for:** * Enterprise applications with corporate security policies * Developer tools and cloud platforms * Any application offering optional enhanced security **Consider alternatives for:** * Low-risk applications with non-sensitive data * High-frequency access scenarios where the additional step creates significant friction For a comparison of all authentication methods, see [Choosing an Authentication Method](/identityserver/identity/user-management/authentication/overview#choosing-an-authentication-method). ## How It Works [Section titled “How It Works”](#how-it-works) TOTP authentication operates as a two-phase flow. ### Phase 1: Setup (One-Time) [Section titled “Phase 1: Setup (One-Time)”](#phase-1-setup-one-time) 1. **User initiates Two-Factor Authentication (2FA)** - After signing in, the user chooses to enable two-factor authentication 2. **Secret generation** - The system generates a cryptographic secret key (160-bit random value) 3. **Secret sharing** - The secret is shared with the user via QR code or manual entry 4. **App configuration** - The user scans the QR code or enters the secret in an authenticator app 5. **Verification** - The user enters the current TOTP code from the app to confirm setup 6. **Activation** - If the code is valid, 2FA is enabled for the account 7. **Recovery codes** - The system generates single-use recovery codes as a backup ### Phase 2: Authentication (Every Login) [Section titled “Phase 2: Authentication (Every Login)”](#phase-2-authentication-every-login) 1. **Primary authentication** - The user signs in with their password or OTP 2. **2FA check** - The system detects that the user has TOTP enabled 3. **Code request** - The user is prompted for the current TOTP code 4. **Code verification** - The system verifies the 6-digit code using the shared secret and current time 5. **Session establishment** - If the codes match, full authentication is granted ## Key Interfaces [Section titled “Key Interfaces”](#key-interfaces) ### ITotpAuthenticator [Section titled “ITotpAuthenticator”](#itotpauthenticator) `ITotpAuthenticator` is the primary interface for verifying TOTP codes during login: ```csharp public interface ITotpAuthenticator { Task TryAuthenticateAsync( UserSubjectId subjectId, TotpDeviceName deviceName, PlainTextTotp totp, Ct ct); } ``` ### IUserAuthenticatorsSelfService TOTP Methods [Section titled “IUserAuthenticatorsSelfService TOTP Methods”](#iuserauthenticatorsselfservice-totp-methods) `IUserAuthenticatorsSelfService` manages TOTP device registration and recovery codes: ```csharp // Add a TOTP device (enables 2FA) Task TryAddTotpDeviceAsync( UserSubjectId subjectId, TotpDeviceName deviceName, PlainBytesTotpKey key, PlainTextTotp totp, Ct ct); // Remove a TOTP device (disables 2FA) Task TryRemoveTotpDeviceAsync( UserSubjectId subjectId, TotpDeviceName deviceName, Ct ct); // Generate recovery codes (invalidates any existing codes) Task?> TryCreateRecoveryCodesAsync( UserSubjectId subjectId, Ct ct); ``` ## TOTP Types [Section titled “TOTP Types”](#totp-types) ### PlainBytesTotpKey [Section titled “PlainBytesTotpKey”](#plainbytestotpkey) Represents the shared secret key (160-bit): ```csharp public record PlainBytesTotpKey { // Generate a new cryptographically secure random key public static PlainBytesTotpKey New(); // Encode to Base32 for display or QR code generation public string EncodeToBase32(); // Encode to Base32 as grouped strings (e.g. for manual entry display) public IReadOnlyCollection EncodeToBase32Groups(); // Decode from a Base32 string public static PlainBytesTotpKey DecodeFromBase32(string input); // Try to decode from a Base32 string without throwing public static bool TryDecodeFromBase32(string input, [NotNullWhen(true)] out PlainBytesTotpKey? result); } ``` ### PlainTextTotp [Section titled “PlainTextTotp”](#plaintexttotp) Represents the 6-digit verification code entered by the user: ```csharp public record PlainTextTotp { // Create a TOTP code, throwing on invalid input public static PlainTextTotp Create(string input); // Try to parse a TOTP code without throwing public static bool TryCreate(string? input, [NotNullWhen(true)] out PlainTextTotp? result); } ``` ### TotpDeviceName [Section titled “TotpDeviceName”](#totpdevicename) Identifies a specific TOTP device registered to a user. Multiple devices per user are supported: ```csharp public record TotpDeviceName { // The default device name ("Default") public static TotpDeviceName Default { get; } // Create a device name, throwing on invalid input public static TotpDeviceName Create(string input); // Try to parse a device name without throwing public static bool TryCreate(string? input, [NotNullWhen(true)] out TotpDeviceName? result); public static bool TryCreate(string? input, [NotNullWhen(true)] out TotpDeviceName? result, [NotNullWhen(false)] out IReadOnlyList? errors); } ``` ### TotpAuthenticatorUri [Section titled “TotpAuthenticatorUri”](#totpauthenticatoruri) Generates `otpauth://` URIs for use with authenticator apps and QR code libraries: ```csharp public static class TotpAuthenticatorUri { // Generate an otpauth:// URI for QR code generation // Format: otpauth://totp/{issuer}:{account}?secret={secret}&issuer={issuer}&digits=6 public static string Generate(string issuer, string accountIdentifier, PlainBytesTotpKey key); } ``` ## The TOTP Algorithm [Section titled “The TOTP Algorithm”](#the-totp-algorithm) TOTP is defined in [RFC 6238](https://tools.ietf.org/html/rfc6238). Codes are generated using: * A shared secret key (160-bit) * The current Unix time divided by a 30-second step * HMAC-SHA1 to produce a 6-digit code To account for clock drift, the system accepts codes from the current time step and one step in each direction (±30 seconds), giving a 90-second acceptance window. ### QR Code URI Format [Section titled “QR Code URI Format”](#qr-code-uri-format) TOTP secrets are shared via the `otpauth://` URI scheme: ```text otpauth://totp/MyApp:user@example.com?secret=JBSWY3DPEHPK3PXP&issuer=MyApp&digits=6 ``` ## Implementation Patterns [Section titled “Implementation Patterns”](#implementation-patterns) ### Enabling TOTP (Setup Flow) [Section titled “Enabling TOTP (Setup Flow)”](#enabling-totp-setup-flow) The setup flow has two steps: generating and displaying the secret, then verifying the user has configured their authenticator app correctly. Here’s the full setup flow, from the user requesting to enable TOTP to having a verified authenticator: ``` sequenceDiagram actor User participant App participant UserManagement as User Management User->>App: Request to enable TOTP App->>UserManagement: Generate TOTP secret key UserManagement-->>App: Secret key + QR code URI App-->>User: Display QR code User->>User: Scan QR code with authenticator app User->>App: Enter verification code App->>UserManagement: TryAddTotpDeviceAsync(subjectId, name, key, code) UserManagement-->>App: Success App->>UserManagement: TryCreateRecoveryCodesAsync() UserManagement-->>App: Recovery codes App-->>User: Show recovery codes ``` ```csharp // Step 1: Generate and display the secret public async Task OnGetSetup(CancellationToken ct) { var userId = GetCurrentUserId(); var authenticators = await userAuthenticatorsSelfService.TryGetAsync(userId, ct); // Redirect if TOTP is already enabled if (authenticators?.TotpDeviceNames.Count > 0) { return RedirectToPage("/Manage2FA"); } // Generate a new secret key var key = PlainBytesTotpKey.New(); // Store the key temporarily (e.g., in TempData or session) for the verification step TempData["PendingTotpKey"] = key.EncodeToBase32(); // Generate the otpauth:// URI for QR code display var email = GetCurrentUserEmail(); var qrUri = TotpAuthenticatorUri.Generate("MyApp", email, key); ViewData["QRCodeUri"] = qrUri; ViewData["ManualKey"] = key.EncodeToBase32Groups(); // Grouped for manual entry return Page(); } // Step 2: Verify the user has configured their authenticator app public async Task OnPostVerify(string code, CancellationToken ct) { var userId = GetCurrentUserId(); // Retrieve the temporarily stored key var keyBase32 = TempData["PendingTotpKey"] as string; if (keyBase32 == null) { return RedirectToPage("/Setup2FA"); } if (!PlainBytesTotpKey.TryDecodeFromBase32(keyBase32, out var key)) { return Error("Invalid key."); } if (!PlainTextTotp.TryCreate(code, out var totp)) { return Error("Invalid code format."); } // Register the TOTP device (this also verifies the code) var success = await userAuthenticatorsSelfService.TryAddTotpDeviceAsync( userId, TotpDeviceName.Default, key, totp, ct); if (!success) { return Error("Invalid code. Please try again."); } // Generate recovery codes and show them to the user var recoveryCodes = await userAuthenticatorsSelfService.TryCreateRecoveryCodesAsync(userId, ct); TempData["RecoveryCodes"] = recoveryCodes? .Select(c => string.Join("-", c.ToTextGroups())) .ToArray(); return RedirectToPage("/ShowRecoveryCodes"); } ``` ### TOTP Verification During Login [Section titled “TOTP Verification During Login”](#totp-verification-during-login) After primary authentication (password or OTP), check whether the user has TOTP enabled and redirect to a second-factor page if so: ```csharp // After primary authentication public async Task OnPostLogin(string email, string password, CancellationToken ct) { // Step 1: Verify primary credentials var result = await passwordAuth.TryAuthenticateAsync( AttributeCode.Create("email"), email, NonValidatedPassword.Create(password), ct); if (result is not PasswordAuthenticationResult.Success success) { return Error("Invalid credentials."); } // Step 2: Check if TOTP is enabled var authenticators = await userAuthenticatorsSelfService.TryGetAsync(success.UserSubjectId, ct); if (authenticators?.TotpDeviceNames.Count > 0) { // Store intermediate authentication state authenticationStateService.Store(new AuthenticationState { UserId = success.UserSubjectId, RememberMe = rememberMe, ReturnUrl = returnUrl }); return RedirectToPage("/LoginWith2FA"); } // No 2FA required. Complete sign-in await CompleteSignIn(authenticators, rememberMe); return Redirect(returnUrl ?? "/"); } // TOTP verification page handler public async Task OnPostVerifyTotp(string code, CancellationToken ct) { // Retrieve intermediate authentication state if (!authenticationStateService.TryRetrieve(out var authState)) { return RedirectToPage("/Login"); } if (!PlainTextTotp.TryCreate(code, out var totp)) { return Error("Invalid code format."); } var success = await totpAuth.TryAuthenticateAsync( authState.UserId, TotpDeviceName.Default, totp, ct); if (!success) { return Error("Invalid code."); } // Clear intermediate state and complete sign-in with MFA claim authenticationStateService.Clear(); var authenticators = await userAuthenticatorsSelfService.TryGetAsync(authState.UserId, ct); await CompleteSignIn(authenticators, authState.RememberMe, isMfa: true); return Redirect(authState.ReturnUrl ?? "/"); } ``` ### Disabling TOTP [Section titled “Disabling TOTP”](#disabling-totp) ```csharp public async Task OnPostDisable2FA(CancellationToken ct) { var userId = GetCurrentUserId(); var authenticators = await userAuthenticatorsSelfService.TryGetAsync(userId, ct); if (authenticators == null) { return Error("User not found."); } // Remove all registered TOTP devices foreach (var deviceName in authenticators.TotpDeviceNames) { await userAuthenticatorsSelfService.TryRemoveTotpDeviceAsync( userId, deviceName, ct); } return Success("Two-factor authentication has been disabled."); } ``` ### Regenerating Recovery Codes [Section titled “Regenerating Recovery Codes”](#regenerating-recovery-codes) ```csharp public async Task OnPostRegenerateRecoveryCodes(CancellationToken ct) { var userId = GetCurrentUserId(); // Generate new recovery codes (this invalidates any existing codes) var recoveryCodes = await userAuthenticatorsSelfService.TryCreateRecoveryCodesAsync(userId, ct); if (recoveryCodes == null) { return Error("Failed to generate recovery codes."); } TempData["RecoveryCodes"] = recoveryCodes .Select(c => string.Join("-", c.ToTextGroups())) .ToArray(); return RedirectToPage("/ShowRecoveryCodes"); } ``` ### Checking TOTP Status [Section titled “Checking TOTP Status”](#checking-totp-status) Use `IUserAuthenticatorsSelfService.TryGetAsync` to inspect a user’s TOTP configuration: ```csharp var authenticators = await userAuthenticatorsSelfService.TryGetAsync(userId, ct); // Check if TOTP is enabled bool has2FA = authenticators?.TotpDeviceNames.Count > 0; // List registered authenticator names foreach (var name in authenticators?.TotpDeviceNames ?? []) { Console.WriteLine($"Authenticator: {name}"); } // Check how many recovery codes remain int codesRemaining = authenticators?.RecoveryCodeCount ?? 0; ``` ## Recommended Practices [Section titled “Recommended Practices”](#recommended-practices) ### QR Code Display [Section titled “QR Code Display”](#qr-code-display) Generate QR codes from the `otpauth://` URI to make setup straightforward for users. Any standard QR code library can encode the URI: ```csharp var qrUri = TotpAuthenticatorUri.Generate("MyApp", userEmail, key); // Pass qrUri to your preferred QR code rendering library ``` ### Manual Entry Formatting [Section titled “Manual Entry Formatting”](#manual-entry-formatting) Display the Base32 key in groups for users who cannot scan a QR code: ```csharp // EncodeToBase32Groups() returns the key split into 4-character groups // e.g. ["JBSW", "Y3DP", "EHPK", "3PXP"] var groups = key.EncodeToBase32Groups(); var formatted = string.Join(" ", groups).ToLowerInvariant(); // Result: "jbsw y3dp ehpk 3pxp" ``` ### Recovery Code Guidance [Section titled “Recovery Code Guidance”](#recovery-code-guidance) Advise users to store their recovery codes securely: * Save codes in a password manager * Print and store in a secure location * Store in an encrypted note-taking app * Do not store in email or unencrypted cloud storage ### Authenticator App Recommendations [Section titled “Authenticator App Recommendations”](#authenticator-app-recommendations) Any RFC 6238-compliant authenticator app works with TOTP. Common options include: * **Microsoft Authenticator** - Cross-platform, supports cloud backup * **Google Authenticator** - Simple and widely used * **Authy** - Multi-device sync * **1Password** - Integrated with password management * **Bitwarden** - Open source ## Security [Section titled “Security”](#security) TOTP is the right choice when you want a second factor that works offline and does not depend on a delivery channel. The authenticator app generates codes locally from a shared secret, so there is no email or SMS to intercept. The catch is that the shared secret lives on both the server and the device. If either is compromised, an attacker can generate valid codes. ### What User Management Does for You [Section titled “What User Management Does for You”](#what-user-management-does-for-you) TOTP secrets are generated with a cryptographically secure random number generator and encrypted at rest using [ASP.NET Core Data Protection](/general/data-protection/) before being stored. The verification window accepts codes from ±30 seconds around the current window to handle minor clock drift without meaningfully widening the attack surface. Failed attempts are subject to the same throttling policy as other flows, which makes brute-forcing the 1,000,000 possible 6-digit codes per window impractical. ### What You Need to Think About [Section titled “What You Need to Think About”](#what-you-need-to-think-about) The single most common production mistake with TOTP is not configuring Data Protection key persistence. Without it, TOTP secrets become unreadable after an application restart, and every user with TOTP enrolled is locked out. Configure key persistence before you go to production. This is not optional. TOTP does not protect against real-time phishing. A phishing proxy can sit between the user and your site, relay the TOTP code in real time, and complete the login before the 30-second window expires. If phishing resistance is a hard requirement, passkeys are the answer. Always generate recovery codes when a user enrolls TOTP. A user who loses their authenticator device with no recovery codes has no way back in. For cross-cutting security topics (data protection key persistence, throttling configuration, and password hashing) see [Security Considerations](/identityserver/identity/user-management/fundamentals/security/). ----- # Attribute Groups and Ordering > How to organize user profile attributes into groups and control their display order using IUserProfileSchemaAdmin in Duende User Management. When a schema contains many attributes, displaying them as a flat list quickly becomes hard to navigate. Attribute groups let you organize attributes into named sections and control the order in which both groups and individual attributes appear. This is especially useful when building your own admin UIs or profile editors that need to present attributes in a structured, logical way. ## Types [Section titled “Types”](#types) ### `AttributeGroup` [Section titled “AttributeGroup”](#attributegroup) An `AttributeGroup` represents a named section that attributes can be assigned to. attribute-group-type.cs ```csharp public sealed record AttributeGroup( AttributeGroupCode Code, AttributeDisplayName? DisplayName, AttributeDescription? Description, int Order); ``` * `Code`: The unique identifier for the group. See [`AttributeGroupCode`](#attributegroupcode) below. * `DisplayName`: Optional human-readable label you can show in UIs instead of the raw code. * `Description`: Optional description of what the group contains. * `Order`: Sort weight controlling the position of this group relative to other groups. Lower values appear first. ### `AttributeGroupCode` [Section titled “AttributeGroupCode”](#attributegroupcode) `AttributeGroupCode` is a string-based identifier for a group. Valid characters are alphanumeric, dashes, and underscores. Comparison is case-insensitive. Create an `AttributeGroupCode` using the static `Create` method: attribute-group-code.cs ```csharp var code = AttributeGroupCode.Create("personal-info"); ``` ### `AttributeDefinition` group properties [Section titled “AttributeDefinition group properties”](#attributedefinition-group-properties) Two properties on `AttributeDefinition` control how an attribute is placed within the group structure: * `AttributeGroupCode? GroupCode`: The group this attribute belongs to. `null` means the attribute is ungrouped and appears outside any group section. * `int Order`: Sort weight controlling the display position of this attribute within its group (or among ungrouped attributes). Lower values appear first. These properties are set when constructing an `AttributeDefinition` and can be updated by removing and re-adding the definition, or by calling `ReorderAttributesAsync` to adjust ordering without recreating definitions. ## Managing Groups with `IUserProfileSchemaAdmin` [Section titled “Managing Groups with IUserProfileSchemaAdmin”](#managing-groups-with-iuserprofileschemaadmin) `IUserProfileSchemaAdmin` exposes five methods for working with groups and ordering. IUserProfileSchemaAdmin.cs ```csharp // Get all groups Task> GetAllGroupsAsync(Ct ct); // Add a group Task TryAddGroupAsync(AttributeGroup group, Ct ct); // Remove a group Task TryRemoveGroupAsync(AttributeGroupCode name, Ct ct); // Reorder attributes within a group (pass null for ungrouped attributes) Task ReorderAttributesAsync(AttributeGroupCode? group, IReadOnlyList orderedCodes, Ct ct); // Reorder groups Task ReorderGroupsAsync(IReadOnlyList orderedGroups, Ct ct); ``` * `GetAllGroupsAsync`: Returns all registered groups as a dictionary keyed by `AttributeGroupCode`. Returns an empty dictionary when no groups have been defined. * `TryAddGroupAsync`: Registers a new group. Returns `true` on success and `false` if a group with the same code already exists. * `TryRemoveGroupAsync`: Removes a group by code. Attributes that belonged to the removed group become ungrouped. Returns `true` whether or not the group existed. * `ReorderAttributesAsync`: Reassigns the `Order` values of attributes within the specified group based on the supplied list. Pass `null` as the group to reorder ungrouped attributes. Attributes not included in the list keep their current order and are appended after the listed ones. * `ReorderGroupsAsync`: Reassigns the `Order` values of groups based on the supplied list. Groups not included in the list keep their current order and are appended after the listed ones. ## Setting Up Groups [Section titled “Setting Up Groups”](#setting-up-groups) The following example creates a group, adds attributes to it, and then reorders those attributes. attribute-groups-setup.cs ```csharp using Duende.Storage.EntityAttributeValue; using Duende.UserManagement.Profiles; // Create a group var group = new AttributeGroup( Code: AttributeGroupCode.Create("personal-info"), DisplayName: AttributeDisplayName.Create("Personal Information"), Description: null, Order: 0); await schemaAdmin.TryAddGroupAsync(group, ct); // Add attributes to the group var givenName = new AttributeDefinition { Code = AttributeCode.Create("given_name"), AttributeType = new ScalarAttributeType(ScalarDataType.String), GroupCode = AttributeGroupCode.Create("personal-info"), Order = 0 }; var familyName = new AttributeDefinition { Code = AttributeCode.Create("family_name"), AttributeType = new ScalarAttributeType(ScalarDataType.String), GroupCode = AttributeGroupCode.Create("personal-info"), Order = 1 }; await schemaAdmin.TryAddAttributeDefinitionAsync(givenName, ct); await schemaAdmin.TryAddAttributeDefinitionAsync(familyName, ct); // Reorder attributes within the group await schemaAdmin.ReorderAttributesAsync( AttributeGroupCode.Create("personal-info"), [AttributeCode.Create("family_name"), AttributeCode.Create("given_name")], ct); ``` After the `ReorderAttributesAsync` call, `family_name` will have `Order: 0` and `given_name` will have `Order: 1`, so when you build a UI you can use the field ordering and have family name appear before given name. ## Notes on Ordering [Section titled “Notes on Ordering”](#notes-on-ordering) `Order` values do not need to be unique. When two attributes share the same `Order` value, the system applies a stable secondary sort to produce a consistent result. `ReorderAttributesAsync` reassigns order values starting from `0` based on the position of each code in the supplied list. Attributes not included in the list keep their existing order values and are placed after all listed attributes. Passing `null` as the group to `ReorderAttributesAsync` targets ungrouped attributes, that is, attributes whose `GroupCode` is `null`. The same rules apply to `ReorderGroupsAsync`: groups not in the supplied list are appended after the listed ones in their existing relative order. ----- # User Profiles and Attributes > How to store, retrieve, and manage user profile attributes in Duende User Management using IUserProfileSelfService, IUserProfileAdmin, and IUserProfileSchemaAdmin. User Management provides a flexible, schema-driven profile system that lets you attach typed attributes to every user. The design follows an **Entity-Attribute-Value (EAV)** model: instead of extending a base class or adding columns to a table, you define attributes in a schema at runtime and store values against individual user profiles. `UserProfile` is a **sealed record** that cannot be subclassed. There is exactly one `UserProfile` type in the system: ```csharp public sealed record UserProfile { public UserSubjectId SubjectId { get; } public IReadOnlyDictionary Attributes { get; } } ``` All extensibility happens through the `Attributes` dictionary. You define which attributes exist by registering `AttributeDefinition` entries in the schema; the system then validates values against those definitions at write time. The system exposes three interfaces covering different access levels: self-service operations performed by the authenticated user, administrative operations performed by back-end code, and schema management for defining which attributes exist. ### Where to use these interfaces [Section titled “Where to use these interfaces”](#where-to-use-these-interfaces) All three interfaces are registered with the service provider by `AddUserManagement()` and can be injected anywhere in your application: * **Razor Pages**: inject into page models to read or update the current user’s profile. * **MVC controllers**: inject into controllers for profile endpoints. * **Backend services / hosted services**: inject into `IHostedService` implementations for background provisioning or migration tasks. * **Seed scripts / startup code**: inject `IUserProfileSchemaAdmin` into an `IHostedService` or a startup filter to initialize the schema before the application starts serving requests. ## Registration [Section titled “Registration”](#registration) Call `AddUserManagement()` on the IdentityServer builder to register all profile services: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(); ``` This makes `IUserProfileSelfService`, `IUserProfileAdmin`, and `IUserProfileSchemaAdmin` available for injection. You can also access them as properties on `IUserSelfService.Profiles` and `IUserAdmin.Profiles` respectively (see [User Lifecycle](/identityserver/identity/user-management/fundamentals/user-lifecycle/)). Automatic profile provisioning When a user signs in via OTP for the first time, User Management automatically creates a profile for them and sets the email attribute from their OTP address. You do not need to call `IUserProfileSelfService.TryCreateAsync` manually for OTP-authenticated users. If you want to skip automatic profile provisioning, you can provide a custom `IOtpAuthenticator` implementation. See [OTP Authentication](/identityserver/identity/user-management/authentication/otp/) for details. ## Schema Management [Section titled “Schema Management”](#schema-management) Before storing attributes you must define them in the schema. The schema is a dictionary of `AttributeCode` to `AttributeDefinition` pairs that describes every attribute the system accepts, its data type, and optional uniqueness constraints. ### `IUserProfileSchemaAdmin` [Section titled “IUserProfileSchemaAdmin”](#iuserprofileschemaadmin) `IUserProfileSchemaAdmin` is the interface for managing attribute definitions at runtime. ```csharp public interface IUserProfileSchemaAdmin { Task> GetAllAttributeDefinitionsAsync(Ct ct); Task TryAddAttributeDefinitionAsync(AttributeDefinition definition, Ct ct); Task TryRemoveAttributeDefinitionAsync(AttributeCode code, Ct ct); } ``` * `GetAllAttributeDefinitionsAsync`: Returns all currently registered attribute definitions keyed by code. Returns an empty dictionary when no schema has been configured yet. * `TryAddAttributeDefinitionAsync`: Adds a new attribute definition to the schema. Returns `true` on success and `false` if the definition could not be added (for example, a definition with the same code already exists). * `TryRemoveAttributeDefinitionAsync`: Removes an attribute definition by code. Returns `true` whether or not the definition existed. To organize attributes into groups and control their display order, see [Attribute groups and ordering](/identityserver/identity/user-management/fundamentals/attribute-groups/). ### `AttributeDefinition` [Section titled “AttributeDefinition”](#attributedefinition) An `AttributeDefinition` describes a single attribute in the schema. ```csharp public sealed class AttributeDefinition { public required AttributeCode Code { get; init; } public required AttributeType AttributeType { get; init; } public AttributeDescription? Description { get; init; } public AttributeDisplayName? DisplayName { get; init; } public ScalarDataType DataType { get; } // convenience; throws for non-scalar types public bool IsUnique { get; init; } public bool IsQueryable { get; init; } = true; public bool IsRequired { get; init; } public IReadOnlyCollection Tags { get; init; } public AttributeGroupCode? GroupCode { get; init; } public int Order { get; init; } } ``` * `Code`: The attribute’s identifier. Must start with an ASCII letter, must not end with an underscore, and may only contain ASCII letters, digits, or underscores. * `AttributeType`: The full type descriptor. Use `ScalarAttributeType`, `ComplexAttributeType`, or `ListAttributeType`. * `Description`: Human-readable description of the attribute. * `DisplayName`: Optional human-readable display name for the attribute. When set, UIs can show this instead of the raw code. * `DataType`: Convenience accessor for scalar types. Throws `InvalidOperationException` for complex or list types. * `IsUnique`: When `true`, the system enforces that no two profiles share the same value for this attribute. Not supported for complex or list types. * `IsQueryable`: When `true` (the default), the attribute is indexed and can be searched and filtered. Set to `false` for attributes that are stored but never queried, reducing storage overhead. * `IsRequired`: When `true`, the attribute must be present in the `AttributeValueCollection` before `Validate()` succeeds. Defaults to `false`. * `Tags`: Optional string tags for grouping or filtering definitions. * `GroupCode`: The code of the group this attribute belongs to. `null` means the attribute is ungrouped. * `Order`: Sort weight within the group. Lower values appear first. ### Attribute Types [Section titled “Attribute Types”](#attribute-types) Three attribute type descriptors are available: * **`ScalarAttributeType`**: A single primitive value. Wraps a `ScalarDataType` value. * **`ComplexAttributeType`**: A nested object with named sub-properties, each with its own `AttributeType`. All sub-properties are optional at write time; unknown sub-properties are rejected. * **`ListAttributeType`**: An ordered list of elements, each sharing the same `AttributeType`. Lists cannot be nested inside other lists. ### `ScalarDataType` [Section titled “ScalarDataType”](#scalardatatype) The `ScalarDataType` enum defines the supported primitive types: ```csharp public enum ScalarDataType { Boolean, Date, DateTime, Decimal, Integer, String, } ``` ### Defining Custom Attributes [Section titled “Defining Custom Attributes”](#defining-custom-attributes) Implicit conversions Value objects like `AttributeCode` and `AttributeGroupCode` support implicit conversion from `string`, so you can write `AttributeCode code = "department"` instead of `AttributeCode.Create("department")`. The examples in this documentation use the explicit `Create` method for clarity. The following example adds a custom `department` string attribute and a unique `employee_id` integer attribute to the schema: ```csharp using Duende.Storage.EntityAttributeValue; using Duende.UserManagement.Profiles; public class ProfileSchemaInitializer(IUserProfileSchemaAdmin schemaAdmin) { public async Task InitializeAsync(CancellationToken ct) { var department = new AttributeDefinition { Code = AttributeCode.Create("department"), AttributeType = new ScalarAttributeType(ScalarDataType.String), Description = AttributeDescription.Create("The department the user belongs to.") }; var employeeId = new AttributeDefinition { Code = AttributeCode.Create("employee_id"), AttributeType = new ScalarAttributeType(ScalarDataType.Integer), Description = AttributeDescription.Create("The unique employee identifier."), IsUnique = true }; await schemaAdmin.TryAddAttributeDefinitionAsync(department, ct); await schemaAdmin.TryAddAttributeDefinitionAsync(employeeId, ct); } } ``` ### Defining Complex Attributes [Section titled “Defining Complex Attributes”](#defining-complex-attributes) Use `ComplexAttributeType` to model structured values such as an address: ```csharp var addressType = new ComplexAttributeType( new Dictionary { [AttributeCode.Create("street")] = ComplexAttributeProperty.Of(ScalarDataType.String), [AttributeCode.Create("city")] = ComplexAttributeProperty.Of(ScalarDataType.String), [AttributeCode.Create("country")] = ComplexAttributeProperty.Of(ScalarDataType.String), }); var address = new AttributeDefinition { Code = AttributeCode.Create("address"), AttributeType = addressType, Description = AttributeDescription.Create("The user's postal address.") }; await schemaAdmin.TryAddAttributeDefinitionAsync(address, ct); ``` Complex types can be nested. For example, an address with a geo-location sub-object: ```csharp var addressWithGeo = new ComplexAttributeType( new Dictionary { [AttributeCode.Create("city")] = ComplexAttributeProperty.Of(ScalarDataType.String), [AttributeCode.Create("geo")] = ComplexAttributeProperty.Of( new ComplexAttributeType(new Dictionary { [AttributeCode.Create("lat")] = ComplexAttributeProperty.Of(ScalarDataType.Decimal), [AttributeCode.Create("lng")] = ComplexAttributeProperty.Of(ScalarDataType.Decimal), })), }); ``` ### Defining List Attributes [Section titled “Defining List Attributes”](#defining-list-attributes) Use `ListAttributeType` to model multi-value attributes. The element type can be a scalar or a complex type. A list of strings (e.g., tags): ```csharp var tags = new AttributeDefinition { Code = AttributeCode.Create("tags"), AttributeType = new ListAttributeType(new ScalarAttributeType(ScalarDataType.String)), Description = AttributeDescription.Create("User tags.") }; await schemaAdmin.TryAddAttributeDefinitionAsync(tags, ct); ``` A list of complex objects (e.g., phone numbers with type and number): ```csharp var phoneNumbers = new AttributeDefinition { Code = AttributeCode.Create("phone_numbers"), AttributeType = new ListAttributeType(new ComplexAttributeType( new Dictionary { [AttributeCode.Create("type")] = ComplexAttributeProperty.Of(ScalarDataType.String), [AttributeCode.Create("number")] = ComplexAttributeProperty.Of(ScalarDataType.String), })), Description = AttributeDescription.Create("Phone numbers for the user.") }; await schemaAdmin.TryAddAttributeDefinitionAsync(phoneNumbers, ct); ``` ### Setting Complex and List Values [Section titled “Setting Complex and List Values”](#setting-complex-and-list-values) Once the schema is defined, use the `Set` overloads on `AttributeValueCollection` that accept `IReadOnlyDictionary` (for complex) or `IReadOnlyList` (for list) values. #### Complex attribute [Section titled “Complex attribute”](#complex-attribute) ```csharp var schema = await selfService.GetSchemaAsync(ct); var attributes = new AttributeValueCollection(schema); attributes.Set( AttributeCode.Create("address"), (IReadOnlyDictionary)new Dictionary { ["street"] = "123 Main St", ["city"] = "Seattle", ["country"] = "US" }); var profile = await selfService.TryCreateAsync(subjectId, attributes.Validate(), ct); ``` For nested complex types, nest dictionaries: ```csharp attributes.Set( AttributeCode.Create("address"), (IReadOnlyDictionary)new Dictionary { ["city"] = "Seattle", ["geo"] = new Dictionary { ["lat"] = 47.6m, ["lng"] = -122.3m } }); ``` #### List of scalars [Section titled “List of scalars”](#list-of-scalars) ```csharp attributes.Set( AttributeCode.Create("tags"), (IReadOnlyList)new List { "admin", "power-user" }); ``` #### List of complex objects [Section titled “List of complex objects”](#list-of-complex-objects) ```csharp attributes.Set( AttributeCode.Create("phone_numbers"), (IReadOnlyList)new List { new Dictionary { ["type"] = "mobile", ["number"] = "555-0001" }, new Dictionary { ["type"] = "home", ["number"] = "555-0002" }, }); ``` ### Reading Complex and List Values [Section titled “Reading Complex and List Values”](#reading-complex-and-list-values) When you read a profile back, complex attributes are returned as `IReadOnlyDictionary` and list attributes as `IReadOnlyList`. Cast the value from the `Attributes` dictionary: ```csharp var profile = await selfService.TryGetAsync(subjectId, ct); // Complex attribute var address = (IReadOnlyDictionary)profile!.Attributes[AttributeCode.Create("address")].UntypedValue; Console.WriteLine(address["city"]); // "Seattle" // List attribute var phones = (IReadOnlyList)profile.Attributes[AttributeCode.Create("phone_numbers")].UntypedValue; foreach (var item in phones) { var phone = (IReadOnlyDictionary)item; Console.WriteLine($"{phone["type"]}: {phone["number"]}"); } ``` ### Updating an Attribute [Section titled “Updating an Attribute”](#updating-an-attribute) To update an existing attribute value, you must read the profile back first. You can then create a new `AttributeValueCollection` from the `profile.Attributes.Values` collection, and make updates atributes. Make sure to store the updated attribute values using `IProfileSelfService.TryUpdateAsync()`. ```csharp if (await profileSelfService.TryGetAsync(subjectId, HttpContext.RequestAborted) != null) { // Pass in profile.Attributes.Values to build an updated AttributeValueCollection var updatedAttributes = new AttributeValueCollection(schema, profile.Attributes.Values); updatedAttributes.Set(UserAttributes.Name, Name); updatedAttributes.Set(UserAttributes.FavoriteDinosaur, FavoriteDinosaur); // Validate AttributeValueCollection if (!updatedAttributes.TryValidate(out var validatedUpdatedAttributes, out var errors)) { // handle validation errors } // Store AttributeValueCollection if (await profileSelfService.TryUpdateAsync(profile.SubjectId, validatedUpdatedAttributes, HttpContext.RequestAborted) is null) { // handle errors updating profile } } ``` ### Removing an Attribute Definition [Section titled “Removing an Attribute Definition”](#removing-an-attribute-definition) ```csharp await schemaAdmin.TryRemoveAttributeDefinitionAsync( AttributeCode.Create("department"), ct); ``` ### Inspecting the Schema [Section titled “Inspecting the Schema”](#inspecting-the-schema) ```csharp var definitions = await schemaAdmin.GetAllAttributeDefinitionsAsync(ct); foreach (var (name, definition) in definitions) { Console.WriteLine($"{name}: {definition.Description}"); } ``` ## OIDC Standard Attributes [Section titled “OIDC Standard Attributes”](#oidc-standard-attributes) `OidcStandardAttributes` is a static class that provides pre-built `AttributeDefinition` instances for the standard OpenID Connect profile claims. Use these to add well-known claims to the schema without constructing definitions by hand. ```csharp public static class OidcStandardAttributes { public static readonly AttributeDefinition Name; public static readonly AttributeDefinition GivenName; public static readonly AttributeDefinition FamilyName; public static readonly AttributeDefinition MiddleName; public static readonly AttributeDefinition Nickname; public static readonly AttributeDefinition PreferredUserName; public static readonly AttributeDefinition Profile; public static readonly AttributeDefinition Picture; public static readonly AttributeDefinition Website; public static readonly AttributeDefinition Email; public static readonly AttributeDefinition EmailVerified; public static readonly AttributeDefinition Gender; public static readonly AttributeDefinition Birthdate; public static readonly AttributeDefinition Zoneinfo; public static readonly AttributeDefinition Locale; public static readonly AttributeDefinition PhoneNumber; public static readonly AttributeDefinition PhoneNumberVerified; public static readonly AttributeDefinition Address; } ``` Each member maps to the corresponding OpenID Connect (OIDC) claim name (for example `given_name`, `family_name`, `email_verified`) and carries the description from the OpenID Connect Core specification. ### Adding OIDC Standard Attributes to the Schema [Section titled “Adding OIDC Standard Attributes to the Schema”](#adding-oidc-standard-attributes-to-the-schema) ```csharp await schemaAdmin.TryAddAttributeDefinitionAsync(OidcStandardAttributes.GivenName, ct); await schemaAdmin.TryAddAttributeDefinitionAsync(OidcStandardAttributes.FamilyName, ct); await schemaAdmin.TryAddAttributeDefinitionAsync(OidcStandardAttributes.Email, ct); await schemaAdmin.TryAddAttributeDefinitionAsync(OidcStandardAttributes.EmailVerified, ct); ``` ## Data Types [Section titled “Data Types”](#data-types) ### `UserProfile` [Section titled “UserProfile”](#userprofile) `UserProfile` is the primary read model returned by all profile lookup and mutation operations. ```csharp public sealed record UserProfile { public UserSubjectId SubjectId { get; } public IReadOnlyDictionary Attributes { get; } } ``` * `SubjectId`: The unique subject identifier for the user. * `Attributes`: All stored attribute values keyed by `AttributeCode`. ### `UserProfileListItem` [Section titled “UserProfileListItem”](#userprofilelistitem) `UserProfileListItem` is a lightweight projection used in list query results. It carries the subject identifier and all schema attribute values as a plain string-keyed dictionary. ```csharp public sealed record UserProfileListItem { public UserSubjectId SubjectId { get; } public IReadOnlyDictionary Attributes { get; } } ``` ### `UserProfileAttributeProjection` [Section titled “UserProfileAttributeProjection”](#userprofileattributeprojection) `UserProfileAttributeProjection` is the result type returned by the `QueryAsync` overload that accepts a `HashSet`. It contains only the attributes you requested, making it more efficient than fetching full `UserProfile` records when you need a subset of data. ```csharp public sealed record UserProfileAttributeProjection { public UserSubjectId SubjectId { get; } public IReadOnlyDictionary Attributes { get; } public AttributeValue this[AttributeCode code] { get; } public bool Contains(AttributeCode code); public bool TryGet(AttributeCode code, out AttributeValue? value); } ``` * `SubjectId`: The user’s subject identifier. * `Attributes`: The projected attributes as a dictionary keyed by `AttributeCode`. Only the attributes requested in the query are present. * `this[AttributeCode]`: Gets an attribute value by code. Throws when the attribute is not present in the projection. * `Contains(AttributeCode)`: Returns `true` when the named attribute is present in the projection. * `TryGet(AttributeCode, out AttributeValue?)`: Tries to retrieve an attribute value by code. Returns `false` when the attribute is not present. ### `AttributeValueCollection` [Section titled “AttributeValueCollection”](#attributevaluecollection) `AttributeValueCollection` is a mutable, schema-aware collection of `AttributeValue` instances used when building profile data. It validates attribute values against the schema on every mutation. ```csharp public sealed class AttributeValueCollection : IEnumerable { public AttributeValueCollection(IReadOnlyAttributeSchema schema); public int Count { get; } // Typed setters - validate code exists in schema and value matches declared type public void Set(AttributeCode code, string value); public void Set(AttributeCode code, bool value); public void Set(AttributeCode code, int value); public void Set(AttributeCode code, decimal value); public void Set(AttributeCode code, DateOnly value); public void Set(AttributeCode code, DateTimeOffset value); public void Set(AttributeCode code, IReadOnlyDictionary value); public void Set(AttributeCode code, IReadOnlyList value); // Try variants - return false with error list instead of throwing public bool TrySet(AttributeCode code, string value, out IReadOnlyList? errors); // ... (overloads for bool, int, decimal, DateOnly, DateTimeOffset, complex, list) // Low-level setter (validates against schema if present) public void Set(AttributeValue attribute); public bool Remove(AttributeCode code); public bool Contains(AttributeCode code); public bool TryGet(AttributeCode code, out AttributeValue attribute); public AttributeValue this[AttributeCode code] { get; } // Validation - produces the immutable type required by persist methods public ValidatedAttributeValueCollection Validate(); public bool TryValidate(out ValidatedAttributeValueCollection? validated, out IReadOnlyList? errors); } ``` ### `ValidatedAttributeValueCollection` [Section titled “ValidatedAttributeValueCollection”](#validatedattributevaluecollection) `ValidatedAttributeValueCollection` is an immutable collection that guarantees all required attributes are present and all values conform to the schema. Persist methods (`TryAddAsync`, `TryUpdateAsync`, `TryCreateAsync`) accept only this type, enforcing correctness at compile time. Obtain an instance by calling `Validate()` or `TryValidate()` on an `AttributeValueCollection`. Use `ValidatedAttributeValueCollection.Empty` when no attributes are needed. Build an `AttributeValueCollection` from the schema so that attribute values are validated against their declared types: ```csharp var schema = await selfService.GetSchemaAsync(ct); var attributes = new AttributeValueCollection(schema); attributes.Set(AttributeCode.Create("given_name"), "Jane"); attributes.Set(AttributeCode.Create("family_name"), "Smith"); attributes.Set(AttributeCode.Create("email_verified"), true); ``` ## Self-Service Profile Operations [Section titled “Self-Service Profile Operations”](#self-service-profile-operations) `IUserProfileSelfService` exposes the operations that an authenticated user performs on their own profile. You can inject it directly or access it via `IUserSelfService.Profiles`. ### `IUserProfileSelfService` [Section titled “IUserProfileSelfService”](#iuserprofileselfservice) ```csharp public interface IUserProfileSelfService { Task GetSchemaAsync(Ct ct); Task TryCreateAsync(UserSubjectId subjectId, ValidatedAttributeValueCollection attributes, Ct ct); Task TryGetAsync(UserSubjectId subjectId, Ct ct); Task TryUpdateAsync(UserSubjectId subjectId, ValidatedAttributeValueCollection attributes, Ct ct); } ``` * `GetSchemaAsync`: Returns the current attribute schema. Pass the returned `IReadOnlyAttributeSchema` to the `AttributeValueCollection` constructor so attribute values are validated against their declared types. * `TryCreateAsync`: Creates a new profile for the given subject with the supplied attributes. Returns the created `UserProfile` on success, or `null` if a profile already exists for that subject. * `TryGetAsync`: Retrieves the profile for the given subject. Returns `null` when no profile exists. * `TryUpdateAsync`: Replaces the attributes of an existing profile. Returns the updated `UserProfile` on success, or `null` when the profile does not exist or a concurrent update conflict occurs. ### Registering a Profile [Section titled “Registering a Profile”](#registering-a-profile) ```csharp using Duende.Storage.EntityAttributeValue; using Duende.UserManagement; using Duende.UserManagement.Profiles; public class RegistrationService(IUserProfileSelfService profileService) { public async Task RegisterAsync( string subjectId, string givenName, string familyName, string email, CancellationToken ct) { var schema = await profileService.GetSchemaAsync(ct); var attributes = new AttributeValueCollection(schema); attributes.Set(AttributeCode.Create("given_name"), givenName); attributes.Set(AttributeCode.Create("family_name"), familyName); attributes.Set(AttributeCode.Create("email"), email); return await profileService.TryCreateAsync( UserSubjectId.Create(subjectId), attributes.Validate(), ct); } } ``` ### Retrieving a Profile [Section titled “Retrieving a Profile”](#retrieving-a-profile) ```csharp var profile = await profileService.TryGetAsync(UserSubjectId.Create(subjectId), ct); if (profile is null) { // No profile exists for this subject. return; } if (profile.Attributes.TryGetValue(AttributeCode.Create("given_name"), out var givenName)) { Console.WriteLine($"Hello, {givenName}"); } ``` ### Updating a Profile [Section titled “Updating a Profile”](#updating-a-profile) Build a new `AttributeValueCollection` with the updated values and call `TryUpdateAsync`: ```csharp var profile = await profileService.TryGetAsync(UserSubjectId.Create(subjectId), ct); if (profile is null) { return; } var schema = await profileService.GetSchemaAsync(ct); var attributes = new AttributeValueCollection(schema); attributes.Set(AttributeCode.Create("given_name"), "Janet"); var updated = await profileService.TryUpdateAsync( UserSubjectId.Create(subjectId), attributes.Validate(), ct); ``` ## Administrative Profile Operations [Section titled “Administrative Profile Operations”](#administrative-profile-operations) `IUserProfileAdmin` provides the same read and create operations as the self-service interface, intended for back-end administrative code that manages profiles on behalf of users. You can inject it directly or access it via `IUserAdmin.Profiles`. ### `IUserProfileAdmin` [Section titled “IUserProfileAdmin”](#iuserprofileadmin) ```csharp public interface IUserProfileAdmin { Task GetSchemaAsync(Ct ct); Task TryAddAsync(UserSubjectId subjectId, ValidatedAttributeValueCollection attributes, Ct ct); Task TryGetAsync(UserSubjectId subjectId, Ct ct); Task TryGetAsync(AttributeCode uniqueAttributeCode, object value, Ct ct); } ``` * `GetSchemaAsync`: Returns the current attribute schema, identical to the self-service variant. * `TryAddAsync`: Creates a new profile for the given subject. Returns the created `UserProfile` on success, or `null` if a profile already exists. * `TryGetAsync(UserSubjectId, Ct)`: Retrieves a profile by subject identifier. * `TryGetAsync(AttributeCode, object, Ct)`: Retrieves a profile by matching a unique attribute value. The attribute must have `IsUnique` set to `true` in its `AttributeDefinition`, because the lookup relies on the unique index for efficient matching. Returns `null` when no matching profile is found. ### Creating a Profile (Admin) [Section titled “Creating a Profile (Admin)”](#creating-a-profile-admin) ```csharp using Duende.Storage.EntityAttributeValue; using Duende.UserManagement; using Duende.UserManagement.Profiles; public class AdminProvisioningService(IUserProfileAdmin profileAdmin) { public async Task ProvisionAsync( string subjectId, string email, int employeeId, CancellationToken ct) { var schema = await profileAdmin.GetSchemaAsync(ct); var attributes = new AttributeValueCollection(schema); attributes.Set(AttributeCode.Create("email"), email); attributes.Set(AttributeCode.Create("employee_id"), employeeId); return await profileAdmin.TryAddAsync( UserSubjectId.Create(subjectId), attributes.Validate(), ct); } } ``` ### Looking Up a Profile by Attribute Value [Section titled “Looking Up a Profile by Attribute Value”](#looking-up-a-profile-by-attribute-value) ```csharp var profile = await profileAdmin.TryGetAsync( AttributeCode.Create("employee_id"), 42, ct); if (profile is not null) { Console.WriteLine($"Found profile for subject {profile.SubjectId}"); } ``` ## Querying Profiles [Section titled “Querying Profiles”](#querying-profiles) `IUserProfileAdmin` provides query methods for searching and filtering user profiles. This is useful for admin operations such as finding all profiles with a specific attribute value, exporting profile data, or generating reports. ### QueryAsync Methods [Section titled “QueryAsync Methods”](#queryasync-methods) ```csharp public interface IUserProfileAdmin { // ... other methods ... Task> QueryAsync( QueryRequest request, CancellationToken ct); Task> QueryAsync( QueryRequest request, HashSet attributes, CancellationToken ct); } ``` Filtering and sorting are not supported for profile queries; only pagination via `Range` is available. Passing a filter or sort field will throw `NotSupportedException`. Use `QueryRequest.Create(new DataRange(...))` to construct the request. * **`QueryAsync(QueryRequest, CancellationToken)`**: Returns a paged list of `UserProfile` records. Use `QueryRequest.Create(new DataRange(offset, limit))` to control pagination. * **`QueryAsync(QueryRequest, HashSet, CancellationToken)`**: Returns a paged list of `UserProfileAttributeProjection` records with only the specified attributes. This overload is useful for performance optimization when you only need a subset of attributes. The projection includes `SubjectId` and the requested attributes. ### Querying All Profiles [Section titled “Querying All Profiles”](#querying-all-profiles) ```csharp using Duende.Storage.Querying; using Duende.UserManagement.Profiles; var request = QueryRequest.Create(new DataRange(0, 50)); var result = await userProfileAdmin.QueryAsync(request, ct); foreach (var profile in result.Items) { Console.WriteLine($"Subject: {profile.SubjectId}"); } ``` ### Querying Profiles with Attribute Projection [Section titled “Querying Profiles with Attribute Projection”](#querying-profiles-with-attribute-projection) ```csharp using Duende.Storage.EntityAttributeValue; using Duende.Storage.Querying; using Duende.UserManagement.Profiles; // Only retrieve email and department attributes for performance var attributes = new HashSet { AttributeCode.Create("email"), AttributeCode.Create("department") }; var request = QueryRequest.Create(new DataRange(0, 50)); var projections = await userProfileAdmin.QueryAsync(request, attributes, ct); foreach (var projection in projections.Items) { Console.WriteLine($"Subject: {projection.SubjectId}"); foreach (var (name, value) in projection.Attributes) { Console.WriteLine($" {name} = {value}"); } } ``` `IReadOnlyAttributeSchema` is returned by `GetSchemaAsync` on both `IUserProfileSelfService` and `IUserProfileAdmin`. It exposes the full set of attribute definitions and their groupings. Pass the schema to the `AttributeValueCollection` constructor so the collection validates attribute values against their declared types. ```csharp public interface IReadOnlyAttributeSchema { IReadOnlyDictionary AttributeDefinitions { get; } IReadOnlyDictionary Groups { get; } } ``` * `AttributeDefinitions`: The full schema as a read-only dictionary. Each `AttributeDefinition` includes an `IsRequired` property (defaults to `false`). * `Groups`: The attribute groups defined in the schema. ## End-To-End Example [Section titled “End-To-End Example”](#end-to-end-example) The following example shows a complete flow: initialising the schema on startup, registering a user profile, and then reading it back. ```csharp using Duende.Storage.EntityAttributeValue; using Duende.UserManagement; using Duende.UserManagement.Profiles; // 1. Add OIDC standard attributes and a custom attribute to the schema. public class SchemaSetup(IUserProfileSchemaAdmin schemaAdmin) { public async Task RunAsync(CancellationToken ct) { await schemaAdmin.TryAddAttributeDefinitionAsync(OidcStandardAttributes.GivenName, ct); await schemaAdmin.TryAddAttributeDefinitionAsync(OidcStandardAttributes.FamilyName, ct); await schemaAdmin.TryAddAttributeDefinitionAsync(OidcStandardAttributes.Email, ct); await schemaAdmin.TryAddAttributeDefinitionAsync(OidcStandardAttributes.EmailVerified, ct); var department = new AttributeDefinition { Code = AttributeCode.Create("department"), AttributeType = new ScalarAttributeType(ScalarDataType.String), Description = AttributeDescription.Create("The department the user belongs to.") }; await schemaAdmin.TryAddAttributeDefinitionAsync(department, ct); } } // 2. Register a new user profile (self-service, called after authentication). public class OnboardingHandler(IUserProfileSelfService profileService) { public async Task OnboardAsync( string subjectId, string givenName, string familyName, string email, CancellationToken ct) { var schema = await profileService.GetSchemaAsync(ct); var attributes = new AttributeValueCollection(schema); attributes.Set(AttributeCode.Create("given_name"), givenName); attributes.Set(AttributeCode.Create("family_name"), familyName); attributes.Set(AttributeCode.Create("email"), email); attributes.Set(AttributeCode.Create("email_verified"), false); return await profileService.TryCreateAsync( UserSubjectId.Create(subjectId), attributes.Validate(), ct); } } // 3. Read the profile back and surface claims. public class ProfileReader(IUserProfileSelfService profileService) { public async Task PrintAsync(string subjectId, CancellationToken ct) { var profile = await profileService.TryGetAsync(UserSubjectId.Create(subjectId), ct); if (profile is null) { Console.WriteLine("No profile found."); return; } foreach (var (name, value) in profile.Attributes) { Console.WriteLine($"{name} = {value}"); } } } ``` ----- # Roles and Groups > How to manage roles and groups in Duende User Management, including direct and transitive role assignment, CRUD operations, and membership queries. Roles and groups provide a flexible authorization model. A role represents a named permission or capability. A group is a named collection of users. Roles can be assigned to users directly, or transitively by assigning a role to a group and then adding users to that group. ## End-to-End Example [Section titled “End-to-End Example”](#end-to-end-example) The following example creates a role, creates a group, assigns the role to the group, adds a user to the group, and then queries the user’s effective roles (direct and transitive). It uses three services (`IRoleAdmin`, `IGroupAdmin`, and `IMembershipAdmin`) which are registered automatically when you call `AddUserManagement()` (see [Configuration](/identityserver/identity/user-management/reference/configuration/#membership-module)) and can be injected via constructor injection: ```csharp using Duende.UserManagement.Membership; public class RoleSetupService( IRoleAdmin roleAdmin, IGroupAdmin groupAdmin, IMembershipAdmin membershipAdmin) { public async Task SetupEditorRoleAsync(UserSubjectId subjectId, CancellationToken ct) { // 1. Create a role. var roleResult = await roleAdmin.CreateAsync( new Role { Name = RoleName.Create("content-editor") }, ct); var roleId = roleResult.Value; // 2. Create a group. var groupResult = await groupAdmin.CreateAsync( new Group { Name = GroupName.Create("editors") }, ct); var groupId = groupResult.Value; // 3. Assign the role to the group (transitive path). await membershipAdmin.AssignRoleToGroupAsync(roleId, groupId, ct); // 4. Add the user to the group. Membership is auto-created when assigning roles/groups. await membershipAdmin.AssignGroupAsync(subjectId, groupId, ct); // 5. Query effective roles (direct + transitive, merged in application code). var directRoles = await membershipAdmin.GetDirectRolesAsync(subjectId, range: null, ct); var transitiveRoles = await membershipAdmin.GetTransitiveRolesAsync(subjectId, range: null, ct); var effectiveRoles = directRoles.Items .Concat(transitiveRoles.Items) .DistinctBy(r => r.Id) .ToList(); // effectiveRoles now contains "content-editor" via the group. } } ``` ### Where this code typically lives [Section titled “Where this code typically lives”](#where-this-code-typically-lives) This kind of programmatic role and group management is used in several common scenarios: * **Admin application**: A back-office UI where administrators create and manage roles and groups, assign users to groups, and review effective permissions. The admin app calls these APIs in response to user actions. * **Automation or background service**: A service that synchronizes roles or group membership from an external system (for example, an HR directory or an identity provider). The service runs on a schedule or reacts to events, calling these APIs to keep the local state in sync. * **Seed script**: A startup routine that ensures required roles and groups exist before the application accepts traffic. Typically runs once on first deployment or after a database reset. * **Integration tests**: Test setup code that creates known roles, groups, and memberships so that tests run against a predictable, isolated state. ## Data Model [Section titled “Data Model”](#data-model) The core types in the `Duende.UserManagement.Membership` namespace are: ### Role Types [Section titled “Role Types”](#role-types) * **`RoleId`**: A strongly-typed, string-based identifier for a role. Use `RoleId.Create(string)` to create one. Valid characters are alphanumeric, dashes, underscores, forward slashes, and backslashes. * **`RoleName`**: A validated role name. Maximum 200 characters, leading and trailing whitespace is trimmed. Use `RoleName.Create(string)` to construct. * **`RoleDescription`**: An optional description for a role. Maximum 500 characters. Use `RoleDescription.Create(string)` to construct. * **`Role`**: The record used when creating or updating a role. Contains a required `Name` and an optional `Description`. * **`RoleListItem`**: The summary record returned by list and query operations. Contains `Id`, `Name`, and `Description`. * **`RoleFilter`**: Filter criteria for role queries. Supports contains-match filtering on `Name` and `Description`. * **`RoleSortField`**: Enum with values `Name` and `Description` for sorting role query results. ### Group Types [Section titled “Group Types”](#group-types) * **`GroupId`**: A strongly-typed, string-based identifier for a group. Use `GroupId.Create(string)` to create one. Valid characters are alphanumeric, dashes, underscores, forward slashes, and backslashes. * **`GroupName`**: A validated group name. Maximum 200 characters, leading and trailing whitespace is trimmed. Use `GroupName.Create(string)` to construct. * **`GroupDescription`**: An optional description for a group. Maximum 500 characters. Use `GroupDescription.Create(string)` to construct. * **`Group`**: The record used when creating or updating a group. Contains a required `Name` and an optional `Description`. * **`GroupListItem`**: The summary record returned by list and query operations. Contains `Id`, `Name`, and `Description`. * **`GroupFilter`**: Filter criteria for group queries. Supports contains-match filtering on `Name` and `Description`, plus an optional `SearchExpression` for filter expressions (e.g., `displayName eq "Engineers"`). * **`GroupSortField`**: Enum with values `Name` and `Description` for sorting group query results. ### Membership Types [Section titled “Membership Types”](#membership-types) * **`MembershipRoleMemberListItem`**: Returned when listing users directly assigned to a role. Contains `SubjectId`. * **`RoleGroupMemberListItem`**: Returned when listing groups assigned to a role. Contains `Id` and `Name`. * **`MembershipGroupMemberListItem`**: Returned when listing users in a group. Contains `SubjectId`. ### Direct vs. Transitive Role Assignment [Section titled “Direct vs. Transitive Role Assignment”](#direct-vs-transitive-role-assignment) A user can hold a role in two ways: * **Direct assignment**: The role is assigned directly to the user’s profile via `IMembershipAdmin.AssignRoleAsync`. The user holds the role regardless of group membership. * **Transitive assignment**: The role is assigned to a group via `IMembershipAdmin.AssignRoleToGroupAsync`, and the user is a member of that group. The effective role path is: `Role <- GroupRole <- Group <- UserProfileGroup <- UserProfile`. Because the storage layer does not support union operations, direct and transitive roles cannot be combined in a single query. Use `GetDirectRolesAsync` and `GetTransitiveRolesAsync` separately and merge the results in application code. ## `IRoleAdmin` [Section titled “IRoleAdmin”](#iroleadmin) `IRoleAdmin` provides full CRUD operations for roles. It is registered automatically when you call `AddUserManagement()` and is typically injected into admin controllers, background services, or seed scripts. Use it whenever you need to create, read, update, delete, or search roles independently of membership, for example to populate a role picker in an admin UI or to ensure a set of well-known roles exists at startup. ```csharp public interface IRoleAdmin { Task> CreateAsync(Role role, CancellationToken ct); Task> GetAsync(RoleId id, CancellationToken ct); Task> UpdateAsync(RoleId id, Role role, Version expectedVersion, CancellationToken ct); Task> DeleteAsync(RoleId id, CancellationToken ct); Task> QueryAsync( QueryRequest request, CancellationToken ct); } ``` * **`CreateAsync`**: Creates a new role. Returns a `SaveResult` containing the new role’s identifier and version on success, or an error if the role name already exists. * **`GetAsync`**: Retrieves a single role by its `RoleId`. Returns a `GetResult` that is either found or not found. * **`UpdateAsync`**: Updates an existing role. Requires the current `Version` for optimistic concurrency. Returns an error on version conflict or if the role is not found. * **`DeleteAsync`**: Deletes a role by its `RoleId`. Returns an error if deletion fails. * **`QueryAsync`**: Returns a paged list of `RoleListItem` records. Use `QueryRequest.Create(filter, sort, range)` to construct the request. All parameters are optional: omit `filter` to return all roles, omit `sort` to use the default ordering, and omit `range` to return the first page with the default page size. ### Creating a Role [Section titled “Creating a Role”](#creating-a-role) ```csharp using Duende.UserManagement.Membership; var role = new Role { Name = RoleName.Create("content-editor"), Description = RoleDescription.Create("Can create and edit content.") }; var result = await roleAdmin.CreateAsync(role, ct); if (result.IsSuccess) { var roleId = result.Value; Console.WriteLine($"Created role: {roleId}"); } ``` ### Querying Roles [Section titled “Querying Roles”](#querying-roles) ```csharp using Duende.Storage.Querying; using Duende.UserManagement.Membership; var filter = new RoleFilter { Name = "editor" }; var sort = SortBy.Ascending(RoleSortField.Name); var range = new DataRange(Offset: 0, Limit: 20); var roles = await roleAdmin.QueryAsync(QueryRequest.Create(filter, sort, range), ct); foreach (var r in roles.Items) { Console.WriteLine($"{r.Id}: {r.Name}"); } ``` ### Updating a Role [Section titled “Updating a Role”](#updating-a-role) ```csharp var existing = await roleAdmin.GetAsync(roleId, ct); if (existing.IsFound) { var updated = new Role { Name = existing.Value.Name, Description = RoleDescription.Create("Updated description.") }; var result = await roleAdmin.UpdateAsync(roleId, updated, existing.Version, ct); } ``` ## `IGroupAdmin` [Section titled “IGroupAdmin”](#igroupadmin) `IGroupAdmin` provides full CRUD operations for groups. Like `IRoleAdmin`, it is registered by `AddUserManagement()` and is injected wherever group lifecycle management is needed, for example in an admin UI that lets administrators create and rename groups, or in a synchronization service that mirrors groups from an external directory. Use it to manage the group catalog independently of membership. ```csharp public interface IGroupAdmin { Task> CreateAsync(Group group, CancellationToken ct); Task> GetAsync(GroupId id, CancellationToken ct); Task> UpdateAsync(GroupId id, Group group, Version expectedVersion, CancellationToken ct); Task> DeleteAsync(GroupId id, CancellationToken ct); Task> QueryAsync( QueryRequest request, CancellationToken ct); } ``` * **`CreateAsync`**: Creates a new group. Returns a `SaveResult` on success, or an error if the group name already exists. * **`GetAsync`**: Retrieves a single group by its `GroupId`. * **`UpdateAsync`**: Updates an existing group with optimistic concurrency via `expectedVersion`. * **`DeleteAsync`**: Deletes a group by its `GroupId`. * **`QueryAsync`**: Returns a paged list of `GroupListItem` records. Use `QueryRequest.Create(filter, sort, range)` to construct the request. `GroupFilter` also supports a `SearchExpression` (e.g., `displayName eq "Engineers"`) that is combined with the other filter properties using AND logic. ### Creating a Group [Section titled “Creating a Group”](#creating-a-group) ```csharp using Duende.UserManagement.Membership; var group = new Group { Name = GroupName.Create("editors"), Description = GroupDescription.Create("All content editors.") }; var result = await groupAdmin.CreateAsync(group, ct); if (result.IsSuccess) { var groupId = result.Value; Console.WriteLine($"Created group: {groupId}"); } ``` ### Querying Groups with a Filter Expression [Section titled “Querying Groups with a Filter Expression”](#querying-groups-with-a-filter-expression) ```csharp using Duende.UserManagement.Membership; using Duende.Storage.Querying; var filter = new GroupFilter { SearchExpression = new SearchExpression("displayName eq \"editors\"") }; var groups = await groupAdmin.QueryAsync(QueryRequest.Create(filter, sort: null, range: null), ct); ``` ## `IMembershipAdmin` [Section titled “IMembershipAdmin”](#imembershipadmin) `IMembershipAdmin` is the single interface for all membership operations. It replaces the former `IRoleMembershipAdmin` and `IGroupMembershipAdmin` interfaces, which no longer exist. It is registered by `AddUserManagement()` alongside `IRoleAdmin` and `IGroupAdmin` and is injected wherever you need to assign roles or groups to users, or query a user’s effective roles. A user’s membership record is automatically created when a role or group is first assigned to them. There is no need to explicitly create or manage the membership lifecycle. ```csharp public interface IMembershipAdmin { // Direct role assignment Task> AssignRoleAsync(UserSubjectId subjectId, RoleId roleId, CancellationToken ct); Task> RemoveRoleAsync(UserSubjectId subjectId, RoleId roleId, CancellationToken ct); // Group role assignment Task> AssignRoleToGroupAsync(RoleId roleId, GroupId groupId, CancellationToken ct); Task> RemoveRoleFromGroupAsync(RoleId roleId, GroupId groupId, CancellationToken ct); // Group membership Task> AssignGroupAsync(UserSubjectId subjectId, GroupId groupId, CancellationToken ct); Task> RemoveGroupAsync(UserSubjectId subjectId, GroupId groupId, CancellationToken ct); // Query operations Task> GetDirectRolesAsync(UserSubjectId subjectId, DataRange? range, CancellationToken ct); Task> GetTransitiveRolesAsync(UserSubjectId subjectId, DataRange? range, CancellationToken ct); Task> GetRolesForGroupAsync(GroupId groupId, DataRange? range, CancellationToken ct); Task> GetGroupsAsync(UserSubjectId subjectId, DataRange? range, CancellationToken ct); Task> GetMembersInRoleAsync(RoleId roleId, DataRange? range, CancellationToken ct); Task> GetGroupsInRoleAsync(RoleId roleId, DataRange? range, CancellationToken ct); Task> GetMembersInGroupAsync(GroupId groupId, DataRange? range, CancellationToken ct); } ``` ### Direct role assignment [Section titled “Direct role assignment”](#direct-role-assignment) * **`AssignRoleAsync(UserSubjectId, RoleId, CancellationToken)`**: Directly assigns a role to a user. Automatically creates the user’s membership record if it does not exist. Idempotent; succeeds if the assignment already exists. * **`RemoveRoleAsync(UserSubjectId, RoleId, CancellationToken)`**: Removes a direct role assignment from a user. Idempotent; succeeds if the assignment does not exist. ### Group role assignment [Section titled “Group role assignment”](#group-role-assignment) * **`AssignRoleToGroupAsync(RoleId, GroupId, CancellationToken)`**: Assigns a role to a group. All members of the group transitively hold the role. Idempotent. * **`RemoveRoleFromGroupAsync(RoleId, GroupId, CancellationToken)`**: Removes a role assignment from a group. Idempotent. ### Group membership [Section titled “Group membership”](#group-membership) * **`AssignGroupAsync(UserSubjectId, GroupId, CancellationToken)`**: Adds a user to a group. Idempotent; succeeds if the user is already a member. * **`RemoveGroupAsync(UserSubjectId, GroupId, CancellationToken)`**: Removes a user from a group. Idempotent; succeeds if the user is not a member. ### Query operations [Section titled “Query operations”](#query-operations) * **`GetDirectRolesAsync(UserSubjectId, DataRange?, CancellationToken)`**: Returns roles directly assigned to a user (single-hop query). * **`GetTransitiveRolesAsync(UserSubjectId, DataRange?, CancellationToken)`**: Returns roles a user holds via group membership (multi-hop query: `Role <- GroupRole <- Group <- UserProfileGroup <- UserProfile`). * **`GetRolesForGroupAsync(GroupId, DataRange?, CancellationToken)`**: Returns roles assigned to a group. * **`GetGroupsAsync(UserSubjectId, DataRange?, CancellationToken)`**: Returns the groups a user belongs to. * **`GetMembersInRoleAsync(RoleId, DataRange?, CancellationToken)`**: Returns the users directly assigned to a role. * **`GetGroupsInRoleAsync(RoleId, DataRange?, CancellationToken)`**: Returns the groups assigned to a role. * **`GetMembersInGroupAsync(GroupId, DataRange?, CancellationToken)`**: Returns the users who are members of a group. ### Assigning a Role Directly to a User [Section titled “Assigning a Role Directly to a User”](#assigning-a-role-directly-to-a-user) ```csharp using Duende.UserManagement.Membership; var result = await membershipAdmin.AssignRoleAsync(subjectId, roleId, ct); if (result.IsSuccess) { Console.WriteLine("Role assigned to user."); } ``` ### Assigning a Role to a Group [Section titled “Assigning a Role to a Group”](#assigning-a-role-to-a-group) ```csharp var result = await membershipAdmin.AssignRoleToGroupAsync(roleId, groupId, ct); ``` ### Adding a User to a Group [Section titled “Adding a User to a Group”](#adding-a-user-to-a-group) ```csharp using Duende.UserManagement.Membership; var result = await membershipAdmin.AssignGroupAsync(subjectId, groupId, ct); if (result.IsSuccess) { Console.WriteLine("User added to group."); } ``` ### Removing a User from a Group [Section titled “Removing a User from a Group”](#removing-a-user-from-a-group) ```csharp var result = await membershipAdmin.RemoveGroupAsync(subjectId, groupId, ct); ``` ### Querying a User’s Effective Roles [Section titled “Querying a User’s Effective Roles”](#querying-a-users-effective-roles) Because direct and transitive roles cannot be combined in a single query, retrieve both sets separately and merge them: ```csharp using Duende.UserManagement.Membership; var directRoles = await membershipAdmin.GetDirectRolesAsync(subjectId, range: null, ct); var transitiveRoles = await membershipAdmin.GetTransitiveRolesAsync(subjectId, range: null, ct); var effectiveRoles = directRoles.Items .Concat(transitiveRoles.Items) .DistinctBy(r => r.Id) .ToList(); foreach (var role in effectiveRoles) { Console.WriteLine(role.Name); } ``` ### Querying Groups for a User [Section titled “Querying Groups for a User”](#querying-groups-for-a-user) ```csharp var groups = await membershipAdmin.GetGroupsAsync(subjectId, range: null, ct); foreach (var group in groups.Items) { Console.WriteLine($"{group.Id}: {group.Name}"); } ``` ### Listing Members of a Group [Section titled “Listing Members of a Group”](#listing-members-of-a-group) ```csharp using Duende.UserManagement.Membership; var range = new DataRange(Offset: 0, Limit: 50); var members = await membershipAdmin.GetMembersInGroupAsync(groupId, range, ct); foreach (var member in members.Items) { Console.WriteLine(member.SubjectId); } ``` ### Listing Members of a Role [Section titled “Listing Members of a Role”](#listing-members-of-a-role) ```csharp var members = await membershipAdmin.GetMembersInRoleAsync(roleId, range: null, ct); foreach (var member in members.Items) { Console.WriteLine(member.SubjectId); } ``` ### Deprovisioning a User [Section titled “Deprovisioning a User”](#deprovisioning-a-user) When a user is removed from the system, use `IUserAdmin.TryRemoveAsync()` to delete the user and clean up all role and group assignments: ```csharp using Duende.UserManagement; var removed = await userAdmin.TryRemoveAsync(subjectId, ct); if (removed) { Console.WriteLine("User and all role/group assignments removed."); } ``` ----- # Security Considerations > Security best practices, rate limiting values, authentication throttling, passkey properties, and data protection guidance for Duende User Management. This page covers security best practices and design decisions in User Management’s authentication system, including verified rate limiting values, authentication throttling configuration, and passkey security properties. ## Data Protection [Section titled “Data Protection”](#data-protection) ### Encryption at Rest [Section titled “Encryption at Rest”](#encryption-at-rest) User Management encrypts sensitive user data using [ASP.NET Core Data Protection](/general/data-protection/): * **One-Time Password (OTP) codes**: Encrypted before storage, decrypted only during verification * **Time-Based One-Time Password (TOTP) secrets**: Stored encrypted, decrypted only for code generation and verification * **Recovery codes**: Hashed (not encrypted) using PBKDF2. They cannot be retrieved, only verified. ### Data Protection Configuration [Section titled “Data Protection Configuration”](#data-protection-configuration) Ensure [ASP.NET Core Data Protection](/general/data-protection/) is configured for key persistence: Program.cs ```csharp builder.Services.AddDataProtection() .SetApplicationName("MyApp") .PersistKeysToFileSystem(new DirectoryInfo("/keys")); ``` Caution Without persistent data protection keys, encrypted data such as OTP tokens and TOTP secrets becomes unreadable after an application restart. Always configure key persistence in production. ## Password Security [Section titled “Password Security”](#password-security) ### Hashing [Section titled “Hashing”](#hashing) User Management uses **PBKDF2** (RFC 2898) with the following parameters, verified from source: * **Pseudorandom function**: HMAC-SHA-512 * **Iteration count**: 210,000 (following the [OWASP recommendation](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) for PBKDF2-HMAC-SHA512) * **Salt**: Unique per password, generated using a cryptographically secure random number generator ### Password Validation Defaults [Section titled “Password Validation Defaults”](#password-validation-defaults) The default `PasswordOptions` enforces the following constraints: | Property | Default | Description | | ------------ | ------------ | ------------------------------------------------ | | `MinLength` | `8` | Minimum password length | | `MinLower` | `2` | Minimum lowercase characters | | `MinUpper` | `2` | Minimum uppercase characters | | `MinDigits` | `2` | Minimum numeric digit characters | | `MinSymbols` | `2` | Minimum symbol characters | | `MaxLength` | PBKDF2 limit | Maximum length based on HMAC-SHA-512 digest size | Override these defaults during registration: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => auth.Configure(options => { options.Passwords.MinLength = 12; options.Passwords.MinSymbols = 1; })) ); ``` ### ASP.NET Identity Password Hash Compatibility [Section titled “ASP.NET Identity Password Hash Compatibility”](#aspnet-identity-password-hash-compatibility) The ASP.NET Identity import job preserves existing password hashes. Users migrated from ASP.NET Identity can continue using their existing passwords without forced resets. ### Timing Attack Protection [Section titled “Timing Attack Protection”](#timing-attack-protection) Password verification uses constant-time comparison, preventing attackers from determining whether a username exists by measuring response times. ## Authentication Throttling [Section titled “Authentication Throttling”](#authentication-throttling) User Management includes a per-authenticator throttling policy that limits repeated failed authentication attempts. The policy is controlled by `AuthenticationThrottlingOptions`, accessible via `UserAuthenticationOptions.Throttling`. ### `AuthenticationThrottlingOptions` [Section titled “AuthenticationThrottlingOptions”](#authenticationthrottlingoptions) | Property | Type | Default | Description | | ----------------------------- | -------------------------- | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MaxFailedAttempts` | `int` | `5` | Maximum number of failed attempts before throttling activates. | | `FailureWindow` | `TimeSpan` | `15 minutes` | Window after the last failure during which the failure count is relevant. If `LastFailedAtUtc + FailureWindow` has elapsed, the count resets to zero. | | `ThrottleDuration` | `TimeSpan` | `5 minutes` | How long to block after exceeding the threshold, measured from `LastFailedAtUtc`. | | `EscalatingThrottleDurations` | `IReadOnlyList?` | `null` | Per-lockout durations for escalating lockout behavior. When set, each successive lockout uses the next duration in the list. When `null` or empty, `ThrottleDuration` applies for all lockouts. | Configure throttling during registration: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => auth.Configure(options => { options.Throttling.MaxFailedAttempts = 3; options.Throttling.FailureWindow = TimeSpan.FromMinutes(30); options.Throttling.ThrottleDuration = TimeSpan.FromMinutes(10); })) ); ``` The default policy allows an attempt when: * The failure count is below `MaxFailedAttempts`, or * `LastFailedAtUtc + FailureWindow` has elapsed (the window has expired), or * `LastFailedAtUtc + ThrottleDuration` has elapsed (the block period has ended) Implement `IAuthenticationAttemptPolicy` to replace the default policy with custom logic. ### Velocity-Based Throttling [Section titled “Velocity-Based Throttling”](#velocity-based-throttling) In addition to failure-based throttling, User Management can limit the total rate of authentication attempts (both successful and failed) within a sliding window. This protects against high-frequency automated attacks that might not trigger failure-based throttling because they occasionally succeed. The following properties on `AuthenticationThrottlingOptions` control velocity-based throttling: | Property | Type | Default | Description | | -------------------------- | ---------- | ---------- | ------------------------------------------------------------------------------------------------- | | `MaxAttemptsPerWindow` | `int` | `5` | Maximum total authentication attempts (successful and failed) allowed within the `VelocityWindow` | | `VelocityWindow` | `TimeSpan` | `00:00:10` | Sliding window duration for counting total attempts | | `VelocityThrottleDuration` | `TimeSpan` | `00:00:30` | How long to block further attempts after the velocity threshold is exceeded | Configure velocity-based throttling during registration: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => auth.Configure(options => { options.Throttling.MaxAttemptsPerWindow = 3; options.Throttling.VelocityWindow = TimeSpan.FromSeconds(15); options.Throttling.VelocityThrottleDuration = TimeSpan.FromMinutes(1); })) ); ``` The `AuthenticatorAttemptInfo` record now includes a `RecentAttemptTimestamps` property (`IReadOnlyList`) that stores the timestamps of recent attempts. The velocity policy uses this list to count attempts within the sliding window and determine whether to block further attempts. ### Escalating Lockout [Section titled “Escalating Lockout”](#escalating-lockout) By default, every lockout applies the same flat `ThrottleDuration`. You can make repeated lockouts progressively longer by setting `EscalatingThrottleDurations` to a list of `TimeSpan` values. When `EscalatingThrottleDurations` is set, the lockout duration is chosen by indexing into the list using the user’s current lockout count: * The first lockout uses the first duration in the list. * The second lockout uses the second duration, and so on. * Once the list is exhausted, the last duration is reused for all subsequent lockouts. `AuthenticatorAttemptInfo.LockoutCount` tracks how many times the user has been locked out for a given authenticator since their last successful authentication. The throttling policy reads this value to select the appropriate duration from the list. When `EscalatingThrottleDurations` is `null` or empty, the flat `ThrottleDuration` applies as before. Program.cs ```csharp options.Throttling.EscalatingThrottleDurations = [ TimeSpan.FromMinutes(5), TimeSpan.FromMinutes(15), TimeSpan.FromHours(1) ]; ``` With this configuration, the first lockout blocks for 5 minutes, the second for 15 minutes, and every subsequent lockout for 1 hour. ## OTP Security [Section titled “OTP Security”](#otp-security) ### Rate Limiting [Section titled “Rate Limiting”](#rate-limiting) User Management includes built-in rate limiting for OTP operations. The following values are verified from source (`OtpWorkflow.cs`): | Protection | Value | Purpose | | ------------------------- | ------------- | --------------------------- | | Max verification attempts | `5` per token | Prevents code brute-forcing | | Min time between sends | `1 minute` | Prevents request flooding | | Code expiration | `5 minutes` | Limits the attack window | These values are fixed in the OTP workflow and are not configurable. The OTP code is hashed using PBKDF2 before storage and verified using constant-time comparison. ### Delivery Channel Risks [Section titled “Delivery Channel Risks”](#delivery-channel-risks) **Email:** * Codes are sent in plain text via email * Email may be stored or forwarded by mail servers * Consider adding “do not forward” warnings in email templates **SMS:** * Subject to SIM swapping attacks * SMS may be intercepted or stored by carriers * Less secure than email for sensitive applications ## TOTP Security [Section titled “TOTP Security”](#totp-security) * **Secret key generation** uses a cryptographically secure random number generator * **Time window tolerance** accepts codes from the current 30-second step and one step in each direction (±30 seconds) * **Rate limiting** for TOTP verification is enforced by the authentication throttling policy described above. Apply `AuthenticationThrottlingOptions` to limit brute-force attempts against the 1,000,000 possible 6-digit codes per 30-second window. ## Passkey Security [Section titled “Passkey Security”](#passkey-security) Passkeys (WebAuthn/FIDO2) provide the strongest authentication guarantees available in User Management. ### Security Properties [Section titled “Security Properties”](#security-properties) * **Origin-bound**: Credentials are cryptographically bound to the specific relying party domain and cannot be phished or reused on a different origin. * **No shared secrets**: The private key never leaves the authenticator device; only a public key is stored server-side. * **Replay-resistant**: Each authentication uses a unique server-generated challenge signed by the device; replaying a captured response is rejected. * **Challenge expiry**: Challenges expire after 5 minutes (300 seconds) by default and are single-use. ### `PasskeyOptions` [Section titled “PasskeyOptions”](#passkeyoptions) Passkey behavior is controlled by `PasskeyOptions`, accessible via `UserAuthenticationOptions.Passkeys`. The following defaults are verified from source: | Property | Default | Description | | --------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------ | | `ChallengeSize` | `32` bytes (256 bits) | Size of the server-generated challenge | | `ChallengeTimeout` | `300` seconds (5 minutes) | Maximum validity period for a passkey challenge | | `UserVerificationRequirement` | `"preferred"` | Whether user verification (PIN, biometric) is required during authentication | | `AttestationConveyancePreference` | `"none"` | Whether the authenticator must provide an attestation statement during registration | | `ResidentKeyRequirement` | `"preferred"` | Whether a discoverable (resident) credential is required | | `AuthenticatorAttachment` | `null` (any) | Restricts authenticator type: `"platform"` (built-in), `"cross-platform"` (roaming), or `null` for any | | `SupportedAlgorithms` | `[]` (all) | COSE algorithm identifiers to accept, in preference order | | `ServerDomain` | `null` | Explicit relying party ID; set when sharing passkeys across subdomains | | `AllowedOrigins` | Required | Fully-qualified origins permitted to use passkeys with this relying party | Configure passkey options during registration: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => auth.Configure(options => { options.Passkeys.UserVerificationRequirement = "required"; options.Passkeys.ResidentKeyRequirement = "required"; options.Passkeys.AllowedOrigins = ["https://auth.example.com"]; options.Passkeys.ServerDomain = "example.com"; })) ); ``` ### User Verification Requirement Values [Section titled “User Verification Requirement Values”](#user-verification-requirement-values) | Value | Meaning | | --------------- | --------------------------------------------------------- | | `"required"` | User verification (PIN, biometric) must be performed | | `"preferred"` | User verification is preferred but not required (default) | | `"discouraged"` | User verification should not be performed | ### Resident Key Requirement Values [Section titled “Resident Key Requirement Values”](#resident-key-requirement-values) | Value | Meaning | | --------------- | -------------------------------------------------------- | | `"required"` | Authenticator must create a discoverable credential | | `"preferred"` | Discoverable credential preferred if supported (default) | | `"discouraged"` | Non-discoverable credential preferred | ### Attestation Conveyance Values [Section titled “Attestation Conveyance Values”](#attestation-conveyance-values) | Value | Meaning | | -------------- | ------------------------------------------------------------ | | `"none"` | No attestation statement required (default) | | `"indirect"` | Attestation statement may be anonymized | | `"direct"` | Attestation statement provided directly by the authenticator | | `"enterprise"` | Enterprise attestation for managed authenticators | ## Encryption Algorithms [Section titled “Encryption Algorithms”](#encryption-algorithms) Detailed coverage of the encryption algorithms used for password hashing, recovery code storage, and data protection keys, including their strengths and configuration options, is planned for a future update. ## Recovery Code Security [Section titled “Recovery Code Security”](#recovery-code-security) * Codes are **hashed** (not encrypted) in storage using PBKDF2. They cannot be retrieved, only verified. * Each code is **single-use**, consumed on successful verification. * **Generate new codes** to invalidate all previous codes. * Warn users when the remaining code count drops below a safe threshold. ## Recommendations [Section titled “Recommendations”](#recommendations) * **Prefer passkeys** for the strongest security. They are phishing-resistant and require no shared secrets. * **Use OTP** as the default passwordless method for consumer applications. * **Add TOTP** as an optional second factor for users who prefer authenticator apps. * **Always generate recovery codes** when enabling two-factor authentication. * **Configure [Data Protection](/general/data-protection/) key persistence** in production to prevent data loss on restart. * **Set `AllowedOrigins`** explicitly in `PasskeyOptions` to restrict which origins can use passkeys. * **Set `ServerDomain`** when sharing passkeys across subdomains (for example, `"example.com"` for `auth.example.com` and `app.example.com`). * **Set `UserVerificationRequirement` to `"required"`** for high-assurance scenarios. * **Never store passwords** in logs, error messages, or telemetry. ----- # Storage Configuration > How to configure PostgreSQL or SQL Server storage for Duende User Management, including package installation, connection strings, schema names, schema initialization, and version checks. Duende User Management uses a document-based storage engine that stores entities as complete documents inside a relational database. Adding or removing properties on a document does not require a schema change, which eliminates the need for database migrations. Two production-ready storage adapters are available: PostgreSQL and SQL Server. ## Document-Based Storage [Section titled “Document-Based Storage”](#document-based-storage) The storage engine uses a document-oriented approach within a relational database: * **No Database Migrations**: Add or remove properties without schema changes. * **In-Place Schema Upgrades**: Documents evolve automatically with your application. * **Transaction Support**: Full ACID compliance for data integrity. ## Available Storage Adapters [Section titled “Available Storage Adapters”](#available-storage-adapters) * **[In-Memory](#in-memory-storage)**: An in-memory (optionally file-backed) implementation for local development and testing. * **[PostgreSQL](#postgresql-storage)**: Production-ready storage using PostgreSQL’s native JSONB format (recommended). * **[SQL Server](#sql-server-storage)**: Production-ready storage using SQL Server’s JSON support. The adapter pattern means you can switch databases without changing your application code. ## Storage Options Comparison [Section titled “Storage Options Comparison”](#storage-options-comparison) | Feature | In-Memory | PostgreSQL | SQL Server | | ---------------------- | --------------------------- | --------------------------------------------- | ------------------------------------------------- | | **Setup** | Zero setup required | Requires PostgreSQL infrastructure | Requires SQL Server infrastructure | | **Best for** | Tests and local development | Production workloads (recommended) | Production workloads in .NET/Windows environments | | **Data persistence** | Lost on restart | Durable | Durable | | **JSON support** | N/A | Native JSONB with excellent query performance | JSON support (less native than PostgreSQL JSONB) | | **Enterprise support** | None | Community + commercial options | Full Microsoft enterprise support | | **Production use** | ❌ Not recommended | ✅ Recommended | ✅ Supported | Tip PostgreSQL is the recommended production adapter due to its native JSONB support and excellent JSON query performance. SQL Server is a strong choice for teams already invested in the Microsoft/Windows ecosystem. ## In-Memory Storage [Section titled “In-Memory Storage”](#in-memory-storage) The in-memory adapter stores data in process memory and is intended exclusively for local development and automated testing. No installation or infrastructure is required; it uses SQLite with an in-memory connection string. Program.cs ```csharp using Duende.Storage.Sqlite; builder.Services.AddSqliteStore(options => { options.ConnectionString = "Data Source=:memory:"; }); ``` Not for production The in-memory adapter loses all data when the application restarts. Do not use it in production environments. ## PostgreSQL Storage [Section titled “PostgreSQL Storage”](#postgresql-storage) PostgreSQL is the recommended production storage adapter. It uses PostgreSQL’s native JSONB support to provide flexible document-based storage with relational database reliability. ### Installation [Section titled “Installation”](#installation) Install the PostgreSQL storage package: ```bash dotnet add package Duende.Storage.PostgreSQL ``` ### Basic Setup [Section titled “Basic Setup”](#basic-setup) Configure User Management to use PostgreSQL storage: Program.cs ```csharp using Duende.Storage.Schema; using Duende.Storage.PostgreSql; using Npgsql; var builder = WebApplication.CreateBuilder(args); // Register the NpgsqlDataSource and PostgreSQL store builder.Services .AddSingleton(new NpgsqlDataSourceBuilder( builder.Configuration.GetConnectionString("pgsql")!).Build()) .AddPostgreSqlStore(); var app = builder.Build(); // Initialize the database schema on startup. using (var scope = app.Services.CreateScope()) { await scope.ServiceProvider .GetRequiredService() .CreateIfNotExistsAsync(CancellationToken.None); } app.Run(); ``` Development convenience only Calling `CreateIfNotExistsAsync()` at application startup is convenient for development but not recommended for production. In production, run schema initialization as a separate migration step in your CI/CD pipeline or deployment process. ### Connection String [Section titled “Connection String”](#connection-string) Configure your connection string in `appsettings.json`: appsettings.json ```json { "ConnectionStrings": { "pgsql": "Host=localhost;Database=usermanagement;Username=postgres;Password=yourpassword" } } ``` Connection string parameters: * `Host`: PostgreSQL server hostname. * `Database`: Database name. * `Username`: Database user. * `Password`: Database password. * `Port`: Optional port (default: `5432`). * `SSL Mode`: Optional SSL configuration. Production connection string example: appsettings.json ```json { "ConnectionStrings": { "pgsql": "Host=db.example.com;Database=usermanagement_prod;Username=app_user;Password=secure_password;SSL Mode=Require;Timeout=30" } } ``` ### Schema Configuration [Section titled “Schema Configuration”](#schema-configuration) Customize the database schema name using `PostgreSqlStoreOptions`: Program.cs ```csharp builder.Services.AddPostgreSqlStore(options => { options.SchemaName = "usermanagement"; }); ``` Using a custom schema name helps: * Organize database objects. * Isolate User Management tables from other application data. * Support multi-tenant deployments. Multiple stores with keyed services If your application needs more than one store instance (for example, in a multi-tenant setup where each tenant has its own database), see [Multiple Store Instances](#multiple-store-instances) below. ### Schema Initialization [Section titled “Schema Initialization”](#schema-initialization) Call `CreateIfNotExistsAsync` once on startup to create the schema, tables, and indexes. The operation is idempotent and uses advisory locks to prevent concurrent initialization: Program.cs ```csharp using Duende.Storage.Schema; using var scope = app.Services.CreateScope(); await scope.ServiceProvider .GetRequiredService() .CreateIfNotExistsAsync(CancellationToken.None); ``` ### Schema Version Check [Section titled “Schema Version Check”](#schema-version-check) Check schema compatibility before the application starts accepting traffic: Program.cs ```csharp using Duende.Storage.Schema; using var scope = app.Services.CreateScope(); var schema = scope.ServiceProvider.GetRequiredService(); var result = await schema.CheckVersionAsync(CancellationToken.None); if (!result.IsCompatible) { throw new InvalidOperationException( $"Schema version mismatch. Current: {result.CurrentVersion}, Required: {result.RequiredVersion}"); } ``` `CheckSchemaVersionResult` properties: * `IsCompatible`: `true` when the current schema version matches the required version. * `CurrentVersion`: The schema version found in the database. * `RequiredVersion`: The schema version required by the current package. ## SQL Server Storage [Section titled “SQL Server Storage”](#sql-server-storage) SQL Server is a production-ready storage adapter that uses SQL Server’s JSON support to provide flexible document-based storage with enterprise-grade database reliability. ### Installation [Section titled “Installation”](#installation-1) Install the SQL Server storage package: ```bash dotnet add package Duende.Storage.SqlServer ``` ### Basic Setup [Section titled “Basic Setup”](#basic-setup-1) Configure User Management to use SQL Server storage: Program.cs ```csharp using Duende.Storage.Schema; using Duende.Storage.MsSql; using Microsoft.Data.SqlClient; var builder = WebApplication.CreateBuilder(args); // Register the connection factory and SQL Server store var connectionString = builder.Configuration.GetConnectionString("mssql")!; builder.Services .AddSingleton(() => new SqlConnection(connectionString)) .AddMsSqlStore(options => { }); var app = builder.Build(); // Initialize the database schema on startup. using (var scope = app.Services.CreateScope()) { await scope.ServiceProvider .GetRequiredService() .CreateIfNotExistsAsync(CancellationToken.None); } app.Run(); ``` Development convenience only Calling `CreateIfNotExistsAsync()` at application startup is convenient for development but not recommended for production. In production, run schema initialization as a separate migration step in your CI/CD pipeline or deployment process. ### Connection String [Section titled “Connection String”](#connection-string-1) Configure your connection string in `appsettings.json`: appsettings.json ```json { "ConnectionStrings": { "mssql": "Server=localhost;Database=usermanagement;User Id=sa;Password=yourpassword;TrustServerCertificate=True" } } ``` Connection string parameters: * `Server`: SQL Server hostname. Supports instance notation, for example `localhost\SQLEXPRESS`. * `Database`: Database name. * `User Id`: Database user. * `Password`: Database password. * `TrustServerCertificate`: Set to `True` for development environments. * `Encrypt`: Optional encryption setting (default: `True` in modern drivers). * `Connection Timeout`: Optional connection timeout in seconds (default: `30`). Production connection string example: appsettings.json ```json { "ConnectionStrings": { "mssql": "Server=db.example.com;Database=usermanagement_prod;User Id=app_user;Password=secure_password;Encrypt=True;TrustServerCertificate=False;Connection Timeout=30;Min Pool Size=5;Max Pool Size=100" } } ``` Windows Authentication example: appsettings.json ```json { "ConnectionStrings": { "mssql": "Server=localhost;Database=usermanagement;Integrated Security=True;TrustServerCertificate=True" } } ``` ### Schema Configuration [Section titled “Schema Configuration”](#schema-configuration-1) Customize the database schema name using `MsSqlStoreOptions`: Program.cs ```csharp builder.Services.AddMsSqlStore(options => { options.SchemaName = "usermanagement"; }); ``` Using a custom schema name helps: * Organize database objects. * Isolate User Management tables from other application data. * Support multi-tenant deployments. * Manage permissions at the schema level. Multiple stores with keyed services If your application needs more than one store instance (for example, in a multi-tenant setup where each tenant has its own database), see [Multiple Store Instances](#multiple-store-instances) below. ### Schema Initialization [Section titled “Schema Initialization”](#schema-initialization-1) Call `CreateIfNotExistsAsync` once on startup to create the schema, tables, and indexes. The operation is idempotent and uses application locks to prevent concurrent initialization: Program.cs ```csharp using Duende.Storage.Schema; using var scope = app.Services.CreateScope(); await scope.ServiceProvider .GetRequiredService() .CreateIfNotExistsAsync(CancellationToken.None); ``` ### Schema Version Check [Section titled “Schema Version Check”](#schema-version-check-1) Check schema compatibility before the application starts accepting traffic: Program.cs ```csharp using Duende.Storage.Schema; using var scope = app.Services.CreateScope(); var schema = scope.ServiceProvider.GetRequiredService(); var result = await schema.CheckVersionAsync(CancellationToken.None); if (!result.IsCompatible) { throw new InvalidOperationException( $"Schema version mismatch. Current: {result.CurrentVersion}, Required: {result.RequiredVersion}"); } ``` ### Supported SQL Server Editions [Section titled “Supported SQL Server Editions”](#supported-sql-server-editions) The SQL Server storage adapter is compatible with: * SQL Server 2019 and later (recommended). * SQL Server 2017 (requires compatibility level 140 or higher). * Azure SQL Database (all tiers). * Azure SQL Managed Instance. ## Deployment Best Practices [Section titled “Deployment Best Practices”](#deployment-best-practices) ### Run Schema Initialization as a Separate Step [Section titled “Run Schema Initialization as a Separate Step”](#run-schema-initialization-as-a-separate-step) Avoid calling `CreateIfNotExistsAsync()` at application startup in production. Instead, run schema initialization as a dedicated step in your CI/CD pipeline or deployment process before the application starts: ```bash # Example: run schema init as a pre-deployment job dotnet run --project tools/SchemaInit -- --connection-string "$DB_CONNECTION_STRING" ``` This approach ensures: * Schema changes are applied before new application instances start. * Rollback is possible if schema initialization fails. * Multiple application instances starting simultaneously do not race to initialize the schema. ### Manage Connection String Secrets [Section titled “Manage Connection String Secrets”](#manage-connection-string-secrets) Never store production credentials in `appsettings.json` or source control. Use a secrets management solution appropriate for your environment: * **Environment variables**: Set `ConnectionStrings__pgsql` or `ConnectionStrings__mssql` as environment variables at the OS or container level. * **Azure Key Vault**: Use `builder.Configuration.AddAzureKeyVault(...)` to pull secrets at startup. * **AWS Secrets Manager / HashiCorp Vault**: Integrate via the appropriate .NET configuration provider. * **.NET User Secrets**: Use `dotnet user-secrets` for local development to keep credentials out of source control. ### Configure Connection Pooling [Section titled “Configure Connection Pooling”](#configure-connection-pooling) Both the Npgsql (PostgreSQL) and Microsoft.Data.SqlClient (SQL Server) drivers maintain connection pools automatically. Tune pool size to match your expected concurrency: appsettings.Production.json ```json { "ConnectionStrings": { "pgsql": "Host=db.example.com;Database=usermanagement_prod;Username=app_user;Password=...;Minimum Pool Size=5;Maximum Pool Size=100", "mssql": "Server=db.example.com;Database=usermanagement_prod;User Id=app_user;Password=...;Min Pool Size=5;Max Pool Size=100" } } ``` General guidelines: * Set minimum pool size to avoid cold-start latency under burst traffic. * Set maximum pool size to prevent overwhelming the database server. * Monitor pool exhaustion (timeout errors) and adjust accordingly. ### Use Read Replicas for Query-Heavy Workloads [Section titled “Use Read Replicas for Query-Heavy Workloads”](#use-read-replicas-for-query-heavy-workloads) If your workload is read-heavy, consider routing read operations to a read replica: * **PostgreSQL**: Configure a secondary connection string pointing to a read replica and use it for query-only operations. * **SQL Server**: Use the `ApplicationIntent=ReadOnly` connection string parameter to route reads to an Always On availability group secondary. * **Azure SQL / Azure Database for PostgreSQL**: Enable read replicas in the Azure portal and configure a separate connection string for read traffic. ## Multiple Store Instances [Section titled “Multiple Store Instances”](#multiple-store-instances) If your application needs more than one store instance (for example, in a multi-tenant setup where each tenant has its own database), you can register named stores using [.NET’s keyed services](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/dependency-injection#keyed-services). Both the PostgreSQL and SQL Server adapters accept a service key as their first parameter. When you provide a key, the adapter registers itself and resolves its dependencies (the `NpgsqlDataSource` or `CreateSqlConnection` delegate) as keyed services under that same key. This lets you run multiple isolated stores side-by-side in a single application. ### PostgreSQL [Section titled “PostgreSQL”](#postgresql) Register each tenant’s `NpgsqlDataSource` as a keyed singleton, then pass the same key to `AddPostgreSqlStore`. Each store gets its own connection pool and can target a different database or schema: Program.cs ```csharp using Duende.Storage.PostgreSql; using Npgsql; // Tenant A builder.Services .AddKeyedSingleton("tenant-a", new NpgsqlDataSourceBuilder(builder.Configuration.GetConnectionString("pgsql-tenant-a")!).Build()) .AddPostgreSqlStore("tenant-a", options => { options.SchemaName = "tenant_a"; }); // Tenant B builder.Services .AddKeyedSingleton("tenant-b", new NpgsqlDataSourceBuilder(builder.Configuration.GetConnectionString("pgsql-tenant-b")!).Build()) .AddPostgreSqlStore("tenant-b", options => { options.SchemaName = "tenant_b"; }); ``` ### SQL Server [Section titled “SQL Server”](#sql-server) The same pattern applies to SQL Server. Register a keyed `CreateSqlConnection` delegate for each tenant, then pass the key to `AddMsSqlStore`: Program.cs ```csharp using Duende.Storage.MsSql; using Microsoft.Data.SqlClient; // Tenant A var tenantAConnectionString = builder.Configuration.GetConnectionString("mssql-tenant-a")!; builder.Services .AddKeyedSingleton("tenant-a", () => new SqlConnection(tenantAConnectionString)) .AddMsSqlStore("tenant-a", options => { options.SchemaName = "tenant_a"; }); ``` ### Resolving Keyed Stores [Section titled “Resolving Keyed Stores”](#resolving-keyed-stores) Once registered, you can inject a specific store instance using the `[FromKeyedServices]` attribute on constructor parameters: Example.cs ```csharp public class TenantAService([FromKeyedServices("tenant-a")] IPooledStore store) { // Use the tenant-a store instance } ``` You can also resolve keyed services programmatically via `IServiceProvider.GetRequiredKeyedService("tenant-a")`, which is useful when the tenant key is determined at runtime (for example, from a request header or route value). ----- # User Management Operations > Programmatic interfaces for managing user accounts in Duende User Management, including IUserSelfService, IUserAdmin, UserAuthenticators, and value objects. User Management exposes two service interfaces for managing user accounts: `IUserSelfService` for operations users perform on their own accounts, and `IUserAdmin` for administrative operations. Both interfaces work with strongly-typed value objects and return `bool` results to indicate success or failure. ## `IUserSelfService` [Section titled “IUserSelfService”](#iuserselfservice) `IUserSelfService` is what you inject when a user needs to manage their own account. It handles deregistration directly and exposes profile and authenticator operations as sub-service properties, so you don’t need to inject each one separately. ```csharp public interface IUserSelfService { Task TryDeleteAsync(UserSubjectId subjectId, Ct ct); IUserProfileSelfService Profiles { get; } IUserAuthenticatorsSelfService Authenticators { get; } } ``` ### Methods [Section titled “Methods”](#methods) * **`TryDeleteAsync`**: Permanently removes the user identified by `subjectId` and all associated data. Returns `false` if the user does not exist. ### Properties [Section titled “Properties”](#properties) * **`Profiles`**: Provides access to `IUserProfileSelfService` for reading and updating the user’s profile. * **`Authenticators`**: Provides access to `IUserAuthenticatorsSelfService` for managing OTP, TOTP, passkeys, passwords, and recovery codes. ### Usage [Section titled “Usage”](#usage) ```csharp // Deregister the user var deregistered = await userSelfService.TryDeleteAsync(subjectId, ct); // Access sub-services through the parent interface var profile = await userSelfService.Profiles.TryGetAsync(subjectId, ct); var authenticators = await userSelfService.Authenticators.TryGetAsync(subjectId, ct); ``` ## `IUserAdmin` [Section titled “IUserAdmin”](#iuseradmin) `IUserAdmin` is the administrative counterpart. Inject it into admin interfaces or background jobs that manage users on their behalf. ```csharp public interface IUserAdmin { Task TryRemoveAsync(UserSubjectId subjectId, Ct ct); IMembershipAdmin Membership { get; } IUserProfileAdmin Profiles { get; } IUserAuthenticatorsAdmin Authenticators { get; } } ``` ### Methods [Section titled “Methods”](#methods-1) * **`TryRemoveAsync`**: Permanently removes the user identified by `subjectId` and all associated data. Returns `false` if the user does not exist. ### Properties [Section titled “Properties”](#properties-1) * **`Membership`**: Provides access to `IMembershipAdmin` for role and group assignment. * **`Profiles`**: Provides access to `IUserProfileAdmin` for reading, creating, and querying profiles. * **`Authenticators`**: Provides access to `IUserAuthenticatorsAdmin` for managing authenticators on behalf of users. ### Difference from `IUserSelfService` [Section titled “Difference from IUserSelfService”](#difference-from-iuserselfservice) `IUserAdmin` and `IUserSelfService` expose similar capabilities. The difference is who the actor is: admin code managing users on their behalf vs. users managing their own accounts. Apply appropriate authorization to each in your application. ## `UserAuthenticators` Record [Section titled “UserAuthenticators Record”](#userauthenticators-record) `UserAuthenticators` is a read-only snapshot of all authenticators registered for a user. It is returned by the `Authenticators` sub-service on `IUserSelfService` and `IUserAdmin` (i.e., `IUserAuthenticatorsSelfService` and `IUserAuthenticatorsAdmin`). ```csharp public sealed record UserAuthenticators { public UserSubjectId SubjectId { get; } public IReadOnlyCollection OtpAddresses { get; } public IReadOnlyCollection ExternalAuthenticatorAddresses { get; } public IReadOnlyCollection TotpDeviceNames { get; } public IReadOnlyCollection Passkeys { get; } public int RecoveryCodeCount { get; } public bool HasPassword { get; } } ``` ### Properties [Section titled “Properties”](#properties-2) | Property | Type | Description | | -------------------------------- | --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------- | | `SubjectId` | `UserSubjectId` | The unique identifier of the user | | `OtpAddresses` | `IReadOnlyCollection` | Email addresses and phone numbers registered for One-Time Password (OTP) delivery | | `ExternalAuthenticatorAddresses` | `IReadOnlyCollection` | External identity providers linked to this account (for example, Google, GitHub) | | `TotpDeviceNames` | `IReadOnlyCollection` | Names of registered Time-Based One-Time Password (TOTP) authenticators; a non-empty collection indicates two-factor authentication is enabled | | `Passkeys` | `IReadOnlyCollection` | Registered passkeys, each with a credential ID, display name, and creation timestamp | | `RecoveryCodeCount` | `int` | Number of unused recovery codes remaining | | `HasPassword` | `bool` | Whether the user has a password set | ### Usage [Section titled “Usage”](#usage-1) ```csharp var authenticators = await userAuthenticatorsSelfService.TryGetAsync(subjectId, ct); if (authenticators is not null) { // Check whether two-factor authentication is enabled var hasTwoFactor = authenticators.TotpDeviceNames.Count > 0 || authenticators.Passkeys.Count > 0; // Check remaining recovery codes if (authenticators.RecoveryCodeCount < 3) { // Prompt user to regenerate recovery codes } } ``` ## Value Objects [Section titled “Value Objects”](#value-objects) User Management uses strongly-typed value objects for all identifiers. These types prevent mixing up different kinds of identifiers at compile time and enforce format constraints at parse time. ### `UserSubjectId` [Section titled “UserSubjectId”](#usersubjectid) The unique identifier for a user. Stored as a string value (maximum 200 characters), compliant with [RFC 9493](https://www.rfc-editor.org/rfc/rfc9493.html). ```csharp // Create from an existing string identifier var subjectId = UserSubjectId.Create("some-existing-id"); // Generate a new unique identifier (creates a GUID string internally) var newId = UserSubjectId.New(); // Access the underlying string value string value = subjectId.Value; ``` ### `OtpAddress` [Section titled “OtpAddress”](#otpaddress) A combination of an `OtpChannel` (email or phone) and a `SubjectId` (the address itself). Represents a delivery channel for one-time passwords. ```csharp // Construct from a channel and address var emailAddress = EmailAddress.Create("jane@example.com"); var otpAddress = new OtpAddress(OtpChannel.Email, emailAddress); var phoneNumber = PhoneNumber.Create("+12025550100"); var otpPhone = new OtpAddress(OtpChannel.Sms, phoneNumber); ``` ### `EmailAddress` [Section titled “EmailAddress”](#emailaddress) A validated email address. Whitespace is trimmed automatically. Minimum length is 3 characters; maximum length is 320 characters. ```csharp // Create: throws FormatException on invalid input var email = EmailAddress.Create("jane@example.com"); // TryCreate: returns false on invalid input if (EmailAddress.TryCreate("jane@example.com", out var result)) { // result is valid here } ``` ### `PhoneNumber` [Section titled “PhoneNumber”](#phonenumber) A validated phone number. Leading `+` and `0` characters are stripped, whitespace is removed, and only digit characters are accepted. Maximum length is 15 digits (per ITU-T E.164). ```csharp // Create: throws FormatException on invalid input var phone = PhoneNumber.Create("+12025550100"); // TryCreate: returns false on invalid input if (PhoneNumber.TryCreate("+12025550100", out var result)) { // result is valid here } ``` ### `ExternalAuthenticatorName` [Section titled “ExternalAuthenticatorName”](#externalauthenticatorname) The name of an external identity provider (for example, `"Google"` or `"GitHub"`). Whitespace is trimmed automatically. Maximum length is 255 characters. ```csharp // Create: throws FormatException on invalid input var name = ExternalAuthenticatorName.Create("Google"); // TryCreate: returns false on invalid input if (ExternalAuthenticatorName.TryCreate("Google", out var result)) { // result is valid here } ``` ### `OpaqueSubjectId` [Section titled “OpaqueSubjectId”](#opaquesubjectid) An opaque string identifier, used as the subject ID issued by an external identity provider. Whitespace is trimmed automatically. Maximum length is 255 characters. ```csharp // Create: throws FormatException on invalid input var id = OpaqueSubjectId.Create("1234567890"); // TryCreate: returns false on invalid input if (OpaqueSubjectId.TryCreate("1234567890", out var result)) { // result is valid here } ``` `UserSubjectId` and `OpaqueSubjectId` both implement the `ISubjectId` interface but are independent types. Use `UserSubjectId` when referring to users within User Management, and `OpaqueSubjectId` when working with external provider subject IDs. ## Extensibility and Maintenance Boundaries [Section titled “Extensibility and Maintenance Boundaries”](#extensibility-and-maintenance-boundaries) Knowing what Duende maintains versus what you can customize helps you build integrations correctly and avoid reimplementing things that already exist. ### Maintained by Duende (internal) [Section titled “Maintained by Duende (internal)”](#maintained-by-duende-internal) These are implemented and maintained by Duende and updated with each release. You call these interfaces but don’t implement them: * **`IUserSelfService`**: lifecycle operations users perform on their own accounts (`TryDeleteAsync`), with sub-services for profiles (`Profiles`) and authenticators (`Authenticators`) * **`IUserAdmin`**: administrative lifecycle operations (`TryRemoveAsync`), with sub-services for membership (`Membership`), profiles (`Profiles`), and authenticators (`Authenticators`) * **`IUserAuthenticatorsSelfService` / `IUserAuthenticatorsAdmin`**: authenticator management (OTP addresses, TOTP, passkeys, recovery codes), accessible directly or via the parent interfaces * **Core storage**: the underlying user store, credential storage, and session state are internal to Duende and not designed for replacement or override * **Authentication logic and lifecycle state machine**: the rules governing registration, login, MFA enrollment, and deregistration are managed internally and are not extensible You don’t need to implement any of these. Inject them where needed and call their methods. ### Extensibility for Developers [Section titled “Extensibility for Developers”](#extensibility-for-developers) These extension points are designed for you to implement or configure: | Extension point | Interface | Purpose | | ------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------------------- | | OTP delivery | `IOtpDispatcher` | Implement to deliver one-time password codes via your preferred channel (email, SMS, push notification, etc.) | | Password validation | `IPasswordValidator` | Implement custom password strength or policy rules beyond the built-in defaults | | Custom profile attributes | `IUserProfileSchemaAdmin` | Add application-specific attributes to the user profile schema | Register your implementation with the service provider at startup to override the default behavior. ### Not Extensible [Section titled “Not Extensible”](#not-extensible) The following are internal to Duende and not designed for override or extension: * Core user storage and the database schema backing it * The authentication and credential verification logic * The lifecycle state machine (registration flow, deregistration cascade, authenticator enrollment rules) Attempting to replace these by intercepting internal services is unsupported and may break across releases. ## Type Hierarchy [Section titled “Type Hierarchy”](#type-hierarchy) The subject identifier value objects follow the [RFC 9493](https://www.rfc-editor.org/rfc/rfc9493.html) subject identifier specification. They all implement the `ISubjectId` interface: * `OpaqueSubjectId`: opaque string identifier (max 255 characters) * `UserSubjectId`: User Management user identifier (max 200 characters) * `EmailAddress`: validated email address (max 320 characters) * `PhoneNumber`: validated phone number in E.164 format (max 15 digits) These are all `record` types. There is no inheritance between them: they are independent types that share the `ISubjectId` contract. ----- # Getting Started with User Management > Build a complete OTP (one-time password) login flow from scratch using Duende User Management with IdentityServer as the foundation. In this tutorial, you’ll build a complete OTP (one-time password) login flow from scratch using Duende User Management on top of IdentityServer. By the end, you’ll have a working ASP.NET Core Razor Pages app where IdentityServer handles authentication and users log in with their email address and a one-time code. [YouTube video player](https://www.youtube.com/embed/8VtPnjE8UJ0) The tutorial is split into two phases: * **Phase 1** scaffolds IdentityServer with User Management wired in. * **Phase 2** builds the OTP login pages that drive the actual user experience. Caution This guide uses a file-based SQLite database for simplicity. SQLite is suitable for local development and single-server deployments, but not recommended for production at scale. For production, switch to PostgreSQL or SQL Server. See the [Storage documentation](/identityserver/identity/user-management/fundamentals/storage/) for details. ## Prerequisites [Section titled “Prerequisites”](#prerequisites) * .NET 10 SDK or later * A code editor (e.g. Visual Studio, VS Code, JetBrains Rider) *** ## Phase 1: Scaffold IdentityServer with User Management [Section titled “Phase 1: Scaffold IdentityServer with User Management”](#phase-1-scaffold-identityserver-with-user-management) ### Create the Project [Section titled “Create the Project”](#create-the-project) Run the following commands to scaffold a new ASP.NET Core web app and move into its directory: Terminal ```bash dotnet new webapp -o OtpIdentityServer && cd OtpIdentityServer ``` The `webapp` template gives you a Razor Pages project with a `Program.cs` entry point, a `Pages/` folder with a sample `Index` page, and the standard `appsettings.json` configuration files. ### Add NuGet Packages [Section titled “Add NuGet Packages”](#add-nuget-packages) Add the IdentityServer, IdentityServer User Management, and a storage NuGet package: Terminal ```bash dotnet add package Duende.IdentityServer dotnet add package Duende.UserManagement.IdentityServer8 dotnet add package Duende.Storage.Sqlite ``` `Duende.IdentityServer` is the core IdentityServer package. `Duende.UserManagement.IdentityServer8` adds the User Management integration on top of it. The storage package provides the backing store for user data. Three storage adapters are available: | Package | Database | Notes | | --------------------------- | ---------- | ------------------------------------------------------------------------------------------------------------------------- | | `Duende.Storage.Postgresql` | PostgreSQL | Recommended for production. Requires an `NpgsqlDataSource` registered in DI. | | `Duende.Storage.Mssql` | SQL Server | Production-ready for Microsoft/Windows environments. | | `Duende.Storage.Sqlite` | SQLite | File-based storage for local development. Also supports an in-memory mode (`Data Source=:memory:`) for automated testing. | This tutorial uses SQLite for simplicity. For production configuration and setup of each provider, see the [Storage documentation](/identityserver/identity/user-management/fundamentals/storage/). ### Add an IdentityServer Config File [Section titled “Add an IdentityServer Config File”](#add-an-identityserver-config-file) Create a `Config.cs` file at the project root with minimal in-memory configuration. This is enough to get IdentityServer running for local development. You can expand it later to add real clients and resources. Config.cs ```csharp using Duende.IdentityServer.Models; public static class Config { public static IEnumerable IdentityResources => [ new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResources.Email(), ]; public static IEnumerable ApiScopes => [ new ApiScope("api", "My API"), ]; public static IEnumerable Clients => [ new Client { ClientId = "interactive", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, RedirectUris = { "https://localhost:5002/signin-oidc" }, PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" }, AllowedScopes = { "openid", "profile", "email", "api" }, }, ]; } ``` For a full explanation of clients, resources, and scopes, see the [IdentityServer documentation](/identityserver/overview/big-picture/). ### Configure Services [Section titled “Configure Services”](#configure-services) The key call in the code below is `.AddIdentityServer(...).AddUserManagement(...)`: User Management is registered as an extension on the IdentityServer builder, not as a separate top-level service. In our application, we can make use of the built-in SMTP dispatcher for sending OTP codes, or use a console-based sender for purpose of this sample. * Console sender (development) The console sender prints OTP codes to the terminal, useful during local development without any mail infrastructure. User Management does not include a console-based OTP dispatcher. Implement `IOtpDispatcher` to write codes to the console. Create `ConsoleOtpDispatcher.cs`: ConsoleOtpDispatcher.cs ```csharp using Duende.UserManagement.Authentication.Otp; public class ConsoleOtpDispatcher : IOtpDispatcher { public bool CanDispatch(OtpAddress address) => true; public Task DispatchAsync(OtpAddress address, PlainTextOtp otp, TimeSpan expiresAfter, CancellationToken ct) { Console.WriteLine($"OTP for {address}: {otp.Text}"); return Task.CompletedTask; } } ``` Then open `Program.cs` and replace its contents with the following: Program.cs ```csharp using Duende.IdentityServer; using Duende.Storage.Schema; using Duende.Storage.Sqlite; using Duende.UserManagement.Authentication.Otp; using Microsoft.AspNetCore.DataProtection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services .AddIdentityServer(options => { options.UserInteraction.LoginUrl = "/Account/Login"; options.UserInteraction.LogoutUrl = "/Account/Logout"; options.Events.RaiseErrorEvents = true; options.Events.RaiseInformationEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseSuccessEvents = true; }) .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients) .AddUserManagement(options => { options.AddSqliteStore(o => { o.ConnectionString = "Data Source=usermanagement.db"; }); }); builder.Services.AddSingleton(); builder.Services.AddDataProtection() .SetApplicationName("OtpIdentityServerGettingStarted"); var app = builder.Build(); using (var scope = app.Services.CreateScope()) { await scope.ServiceProvider .GetRequiredService() .MigrateAsync(CancellationToken.None); } // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { 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(); app.UseRouting(); app.UseIdentityServer(); app.UseAuthorization(); app.MapStaticAssets(); app.MapRazorPages() .WithStaticAssets(); app.Run(); ``` * SMTP sender The SMTP sender delivers OTP codes by email. Bind the `Smtp` configuration section to the sender options. Replace the contents of `Program.cs`: Program.cs ```csharp using Duende.IdentityServer; using Duende.Storage.Schema; using Duende.Storage.Sqlite; using Duende.UserManagement.Authentication.Otp; using Microsoft.AspNetCore.DataProtection; var builder = WebApplication.CreateBuilder(args); builder.Services.AddRazorPages(); builder.Services .AddIdentityServer(options => { options.UserInteraction.LoginUrl = "/Account/Login"; options.UserInteraction.LogoutUrl = "/Account/Logout"; options.Events.RaiseErrorEvents = true; options.Events.RaiseInformationEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseSuccessEvents = true; }) .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients) .AddUserManagement(options => { options.Authentication(configure => { configure.UseSmtpOtpDispatcher(x => builder.Configuration.GetSection("Smtp").Bind(x)); }); options.AddSqliteStore(o => { o.ConnectionString = "Data Source=usermanagement.db"; }); }); builder.Services.AddDataProtection() .SetApplicationName("OtpIdentityServerGettingStarted"); var app = builder.Build(); using (var scope = app.Services.CreateScope()) { await scope.ServiceProvider .GetRequiredService() .MigrateAsync(CancellationToken.None); } // Configure the HTTP request pipeline. if (!app.Environment.IsDevelopment()) { 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(); app.UseRouting(); app.UseIdentityServer(); app.UseAuthorization(); app.MapStaticAssets(); app.MapRazorPages() .WithStaticAssets(); app.Run(); ``` Add the corresponding section to `appsettings.json`: appsettings.json ```json { "Smtp": { "Host": "localhost", "Port": 1025, "FromEmail": "noreply@example.com", "FromName": "noreply" } } ``` Development SMTP server If you prefer an SMTP experience locally, [Mailpit](https://github.com/axllent/mailpit) is a lightweight local SMTP server that captures outgoing email in a web UI. No external mail account required. A few things to note about this setup: * Storage is configured **inside** `AddUserManagement` using `options.AddSqliteStore(...)`, not as a separate top-level call. * `app.UseIdentityServer()` replaces the standalone `app.UseAuthentication()` call. IdentityServer sets up authentication for you. * The database migration runs at startup using `IDatabaseSchema.MigrateAsync`. This creates the SQLite file and schema on first run. Tip For production environments, configure [ASP.NET Core Data Protection](/general/data-protection/) to persist encryption keys across restarts, for example using Azure Key Vault, Redis, or a shared file system. *** ## Phase 2: Build the OTP Login Pages [Section titled “Phase 2: Build the OTP Login Pages”](#phase-2-build-the-otp-login-pages) With IdentityServer running, you now need the Razor Pages that handle the actual login flow. IdentityServer will redirect unauthenticated users to `/Account/Login` (as configured in `options.UserInteraction.LoginUrl`), so that is where you start. ### Add a Login Page [Section titled “Add a Login Page”](#add-a-login-page) 1. Create the `Pages/Account/` directory, then create `Pages/Account/Login.cshtml` with an email input form: Pages/Account/Login.cshtml ```html @page @model OtpIdentityServer.Pages.Account.LoginModel @{ ViewData["Title"] = "Log in"; }

Log in

``` 2. Create the page model in `Pages/Account/Login.cshtml.cs`: Pages/Account/Login.cshtml.cs ```csharp using System.ComponentModel.DataAnnotations; using Duende.UserManagement; using Duende.UserManagement.Authentication.Otp; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace OtpIdentityServer.Pages.Account; public class LoginModel(IOtpSender otpSender) : PageModel { [BindProperty] public InputModel Input { get; set; } = new(); public class InputModel { [Required] [EmailAddress] public string Email { get; set; } = string.Empty; } public void OnGet() { } public async Task OnPostAsync() { if (!ModelState.IsValid) { return Page(); } if (!EmailAddress.TryCreate(Input.Email, out var email)) { ModelState.AddModelError(nameof(Input.Email), "Invalid email format."); return Page(); } var result = await otpSender.TrySendOtpAsync( new OtpAddress(OtpChannel.Email, email), HttpContext.RequestAborted); if (result is SendOtpResult.Sent sentResult) { TempData["OtpToken"] = sentResult.Token.Value.ToString(); return RedirectToPage("/Account/EnterOtp"); } if (result is SendOtpResult.Blocked blocked) { var blockedFor = blocked.SendingBlockedUntilUtc - DateTimeOffset.UtcNow; var blockedMessage = $"Too many attempts. Try again in {Math.Ceiling(blockedFor.TotalSeconds)} second(s)."; ModelState.AddModelError(string.Empty, blockedMessage); return Page(); } ModelState.AddModelError(string.Empty, "Failed to send one-time password."); return Page(); } } ``` When the form is submitted, `IOtpAuthenticator.TrySendOtpAsync` sends a one-time password to the provided email address and returns a result containing the OTP token. If sending succeeds, the token is stored in `TempData` so the next page can verify the code the user enters. If sending is blocked due to rate limiting, a descriptive error is shown instead. Note You can configure OTP expiry, code length, and rate-limiting behavior through the `OtpAuthenticatorOptions`. See the [OTP authentication docs](/identityserver/identity/user-management/authentication/otp/) for all available options. ### Add an OTP Verification Page [Section titled “Add an OTP Verification Page”](#add-an-otp-verification-page) 1. Create `Pages/Account/EnterOtp.cshtml`, the page where the user enters the one-time password they received by email: Pages/Account/EnterOtp.cshtml ```html @page @model OtpIdentityServer.Pages.Account.EnterOtpModel

Enter one-time password

@if (!ViewData.ModelState.IsValid) {
    @foreach (var error in ViewData.ModelState.Values.SelectMany(v => v.Errors)) {
  • @error.ErrorMessage
  • }
}
``` 2. Create `Pages/Account/EnterOtp.cshtml.cs`: Pages/Account/EnterOtp.cshtml.cs ```csharp using System.ComponentModel.DataAnnotations; using System.Security.Claims; using Duende.IdentityServer.Services; using Duende.UserManagement.Authentication.Otp; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace OtpIdentityServer.Pages.Account; public class EnterOtpModel( IOtpAuthenticator otpAuthenticator, IIdentityServerInteractionService interaction) : PageModel { [BindProperty] public InputModel Input { get; set; } = new(); public class InputModel { [Required] public string Token { get; set; } = string.Empty; [Required] public string Code { get; set; } = string.Empty; } public IActionResult OnGet() { var token = TempData["OtpToken"]?.ToString(); if (token is null) { return RedirectToPage("/Account/Login"); } Input.Token = token; return Page(); } public async Task OnPostAsync(string? returnUrl) { if (!ModelState.IsValid) { return Page(); } if (string.IsNullOrWhiteSpace(Input.Code) || string.IsNullOrWhiteSpace(Input.Token)) { ModelState.AddModelError(string.Empty, "Invalid input."); return Page(); } var otp = PlainTextOtp.Create(Input.Code); var token = OtpToken.Create(Input.Token); var authResult = await otpAuthenticator.TryAuthenticateAsync( otp, token, HttpContext.RequestAborted); if (authResult is not OtpAuthenticationResult.Success otpSuccess) { ModelState.AddModelError(string.Empty, "Invalid or expired code. Please try again."); return Page(); } var claims = new List { new("sub", otpSuccess.UserSubjectId.ToString()!), new(ClaimTypes.Name, otpSuccess.Address.SubjectId.ToString()!), }; var identity = new ClaimsIdentity(claims, "otp"); var principal = new ClaimsPrincipal(identity); await HttpContext.SignInAsync( Duende.IdentityServer.IdentityServerConstants.DefaultCookieAuthenticationScheme, principal, new AuthenticationProperties()); returnUrl = interaction.IsValidReturnUrl(returnUrl) ? returnUrl : "~/"; return LocalRedirect(returnUrl!); } } ``` **`OnGet`** reads the OTP token that the Login page stored in `TempData["OtpToken"]` and copies it into the hidden form field. If the token is missing (for example, the user navigated here directly), the page redirects back to `/Account/Login`. **`OnPostAsync`** parses the submitted token and code into their strongly-typed counterparts (`OtpToken` and `PlainTextOtp`), then calls `IOtpAuthenticator.TryAuthenticateAsync`. The method returns an `OtpAuthenticationResult`, which is a discriminated union: pattern-match on `OtpAuthenticationResult.Success` to continue, or show an error for any other result. On success, the page signs the user in using IdentityServer’s default cookie scheme and redirects to the `returnUrl` provided by IdentityServer (validated with `IIdentityServerInteractionService`). Automatic User Registration After a successful OTP verification, User Management automatically creates a user record and profile if this is the first time the address has been used. The `UserSubjectId` on the `Success` result is always set, so you can use it directly to build the user’s claims without any additional lookup or registration step. Tip If you need to access the full user record, for example to read additional OTP addresses or linked authenticators, inject `IUserSelfService` and call `Authenticators.TryGetAsync` with the subject ID. See [OTP Authentication](/identityserver/identity/user-management/authentication/otp/) for details. ### Add a Logout Page [Section titled “Add a Logout Page”](#add-a-logout-page) 1. Create `Pages/Account/Logout.cshtml`: Pages/Account/Logout.cshtml ```html @page @model OtpIdentityServer.Pages.Account.LogoutModel ``` 2. Create `Pages/Account/Logout.cshtml.cs`: Pages/Account/Logout.cshtml.cs ```csharp using Duende.IdentityServer.Services; using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Mvc; using Microsoft.AspNetCore.Mvc.RazorPages; namespace OtpIdentityServer.Pages.Account; public class LogoutModel(IIdentityServerInteractionService interaction) : PageModel { public async Task OnPostAsync(string? logoutId) { var context = await interaction.GetLogoutContextAsync(logoutId, HttpContext.RequestAborted); await HttpContext.SignOutAsync( Duende.IdentityServer.IdentityServerConstants.DefaultCookieAuthenticationScheme); var postLogoutRedirect = context?.PostLogoutRedirectUri; if (!string.IsNullOrEmpty(postLogoutRedirect)) { return Redirect(postLogoutRedirect); } return RedirectToPage("/Account/Login"); } } ``` `OnPostAsync` signs the user out of IdentityServer’s cookie scheme and then redirects to the post-logout URI provided by the client application, or back to `/Account/Login` if none is set. Using POST (rather than GET) prevents cross-site request forgery attacks that could silently sign a user out by embedding a link. ### Protect the Home Page [Section titled “Protect the Home Page”](#protect-the-home-page) 1. Update `Pages/Index.cshtml.cs` to require an authenticated user by adding the `[Authorize]` attribute: Pages/Index.cshtml.cs ```csharp using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc.RazorPages; namespace OtpIdentityServer.Pages; [Authorize] public class IndexModel : PageModel { public void OnGet() { } } ``` 2. Update `Pages/Index.cshtml` to greet the signed-in user and provide a sign-out button: Pages/Index.cshtml ```html @page @model OtpIdentityServer.Pages.IndexModel @{ ViewData["Title"] = "Home"; }

Welcome, @User.Identity!.Name!

``` `User.Identity!.Name` contains the email address that was set as `ClaimTypes.Name` during OTP verification. Because IdentityServer is configured with `LoginUrl = "/Account/Login"`, any unauthenticated request to `/` is automatically redirected to the login page. *** ## Run and Test [Section titled “Run and Test”](#run-and-test) 1. Start the application: Terminal ```bash dotnet run --launch-profile https ``` 2. Open your browser and navigate to `https://localhost:7083` (or the URL shown in the terminal). Note IdentityServer requires HTTPS because its session cookies use `SameSite=None`, which browsers only accept on secure connections. Always use the `https` launch profile during development. 3. You are redirected to `/Account/Login`. Enter your email address and click **Send one-time password**. 4. Retrieve the OTP code: * Console sender The OTP code is printed directly to the terminal. Copy it from there. * SMTP sender Check your inbox for the email containing the OTP code. If you are using Mailpit locally, open `http://localhost:8025` to find the message. 5. Enter the code on the `/Account/EnterOtp` page and click **Sign in**. 6. You land on the home page showing a welcome message with the email address of the user. 7. Click **Sign out** to return to the login page. **Congratulations!** You now have a working ASP.NET Core application with IdentityServer and OTP-based authentication powered by Duende User Management. Users can sign in with their email address, verify a one-time code, and access protected pages. New users are automatically registered on their first successful login. [User Management Getting Started Sample](/identityserver/samples/usermanagement#user-management-getting-started-sample)Full source code in Duende User Management samples ## Next Steps [Section titled “Next Steps”](#next-steps) [IdentityServer Integration](/identityserver/identity/user-management/identityserver-integration)Learn how User Management integrates with IdentityServer for claims mapping and profile services. [Storage options](/identityserver/identity/user-management/fundamentals/storage)Switch to PostgreSQL or SQL Server, or configure separate stores for different data categories. [OTP customization](/identityserver/identity/user-management/authentication/otp)Configure code length, expiry, rate limiting, and custom OTP senders. [User profiles](/identityserver/identity/user-management/fundamentals/profiles)Store and retrieve custom attributes for your users. [More authenticators](/identityserver/identity/user-management/authentication/overview)Add TOTP, passkeys, or external identity providers alongside OTP. ----- # Enabling User Management in IdentityServer > How to add User Management to IdentityServer, including user storage, authentication flows, claims mapping, and role-based authorization. The `Duende.UserManagement.IdentityServer8` package is what you add to your IdentityServer project to enable User Management. It wires up user storage, authentication flows, and claims mapping through a single `AddUserManagement()` call on the IdentityServer builder. Under the hood, it registers an `IProfileService` implementation that maps user profile attributes to OIDC claims and emits role claims (both direct and transitive, deduplicated). ## Setup [Section titled “Setup”](#setup) Once you have IdentityServer set up, adding User Management takes a single call on the builder. 1. **Add the NuGet package** Terminal ```bash dotnet add package Duende.UserManagement.IdentityServer8 ``` 2. **Call `AddUserManagement()` on the IdentityServer builder** The configure delegate is where you enable features and configure storage. This registers both the User Management services and the `UserManagementProfileService` implementation of `IProfileService`. Program.cs ```csharp using Duende.IdentityServer; builder.Services .AddIdentityServer(options => { options.UserInteraction.LoginUrl = "/Account/Login"; options.UserInteraction.LogoutUrl = "/Account/Logout"; }) .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients) .AddUserManagement(options => { options.Authentication(configure => { configure.UseSmtpOtpDispatcher(x => builder.Configuration.GetSection("Smtp").Bind(x)); }); options.AddPostgreSqlStore(); }); ``` Tip You do not need to call `AddUserManagement()` separately on `IServiceCollection`. The extension method on `IIdentityServerBuilder` handles both registrations. ## Claims Mapping [Section titled “Claims Mapping”](#claims-mapping) When IdentityServer calls `GetProfileDataAsync`, the `UserManagementProfileService` loads the user’s profile and maps it to claims: * **Profile attributes**: Each attribute’s code becomes the claim type and its value becomes the claim value. Boolean attributes emit `"true"` or `"false"` with `ClaimValueTypes.Boolean`. * **`role`**: Emitted for every role assigned directly to the user and for every role inherited transitively through group membership. Duplicate roles (by role ID) are deduplicated before emission. Note Standard claims such as `preferred_username` are not emitted automatically. To emit one, define a profile attribute whose attribute code matches the claim type (for example, `preferred_username`). The profile service will then include it like any other attribute claim. Only claims that match the requested scopes are issued. The `UserManagementProfileService` uses `context.AddRequestedClaims(claims)` internally, so IdentityServer’s standard scope-to-claim filtering applies. ## Fallback Behavior [Section titled “Fallback Behavior”](#fallback-behavior) All User Management modules (profiles, authentication, membership) are registered automatically when you call `AddUserManagement()`. The `UserManagementProfileService` constructor requires `IUserProfileAdmin`, `IUserAuthenticatorsAdmin`, and `IMembershipAdmin`. These are all registered by `AddUserManagement()`, so no additional configuration is needed in normal usage. If a module’s services are not available in the service provider (for example, in testing scenarios with a partial setup): * **Profiles not available** (no `IUserProfileAdmin`): The profile service falls back to passing through the claims from the authentication session’s subject principal. `IsActiveAsync` treats the user as always active. This matches the behavior of IdentityServer’s built-in `DefaultProfileService`. * **Authenticators not available** (no `IUserAuthenticatorsAdmin`): The active-user check skips the authenticator requirement and only verifies that the profile exists. * **Membership not available** (no `IMembershipAdmin`): Role claim resolution is skipped entirely. No `role` claims are emitted. ## Subject ID Requirements [Section titled “Subject ID Requirements”](#subject-id-requirements) The `sub` claim in the token must be a non-empty string (up to 200 characters) that can be parsed as a `UserSubjectId`. The `UserManagementProfileService` converts it using `UserSubjectId.TryCreate`. If parsing fails, no claims are issued and a warning is logged. `IsActiveAsync` returns `true` when the user profile exists **and** the user has at least one registered authenticator (password, passkey, external login, or OTP address). A user whose profile exists but has no authenticators is treated as inactive, which causes IdentityServer to reject tokens for that user. Users who have been deleted or never existed also return `false`. ## Customization [Section titled “Customization”](#customization) `UserManagementProfileService` is a public class with virtual methods you can override by subclassing it and registering your subclass with the service provider: * `FindUserAsync(UserSubjectId subjectId, CancellationToken ct)` - override to customize how the user profile is loaded. * `GetProfileDataAsync(ProfileDataRequestContext context, UserSubjectId subjectId, CancellationToken ct)` - override to intercept profile loading after the subject ID is parsed but before the user profile is fetched. * `GetProfileDataAsync(ProfileDataRequestContext context, UserProfile user, CancellationToken ct)` - override to customize how claims are built from the profile. * `GetRoleClaimsAsync(ProfileDataRequestContext context, UserSubjectId subjectId, CancellationToken ct)` - override to customize how role claims are collected. * `IsUserActiveAsync(UserSubjectId subjectId, CancellationToken ct)` - override to customize the active-user check. The default implementation verifies that the user’s profile exists and that at least one authenticator is registered. Return `false` to force IdentityServer to reject tokens for the user. The `Logger` property is available to subclasses as a protected property: ```csharp protected ILogger Logger { get; } ``` Register your subclass after calling `AddUserManagement()`: Program.cs ```csharp using Duende.IdentityServer; builder.Services .AddIdentityServer(...) .AddUserManagement(options => { ... }) .AddProfileService(); ``` ----- # Importing and migrating data > Import users, roles, and authenticators from external systems or other identity providers using the IUserImporter API. User Management lets you migrate users, roles, and authenticators from another identity provider, or seed users from an external system. This is helpful in several scenarios: * Migrating from ASP.NET Identity or another identity provider * Seeding users from an HR system, directory service, or CSV export * Consolidating users from multiple applications into a single identity store Import works by submitting a batch of `UserImportRecord` objects, with a per-record result indicating whether a record was created, updated, skipped, or failed. Records in a batch are processed independently, so a failure on one record does not affect the others. ## Migration Strategies [Section titled “Migration Strategies”](#migration-strategies) Before you write any import code, a bit of planning saves you from surprises mid-migration. ### Planning Your Migration [Section titled “Planning Your Migration”](#planning-your-migration) Most migrations involve some combination of user profiles, passwords, and group or role memberships. For some, all of this data may need to be migrated. Other migrations will be more selective. A user who authenticated exclusively through an external provider (Google, Entra ID, …) often has no password to migrate, for example, and a system that never had roles can skip membership entirely. Each field on `UserImportRecord` is optional, so you only need to populate what applies. Passwords deserve special attention. The import system stores pre-hashed passwords verbatim: it does not re-hash them on the way in. When a user logs in after migration, the system looks up the `algorithmId` stored alongside the hash, routes the verification call to the matching `IPasswordHashAlgorithm`, and checks whether `NeedsRehash()` returns `true`. If it does, the password is transparently re-hashed with the current preferred algorithm. The user notices nothing. This means you need a registered `IPasswordHashAlgorithm` that understands your source system’s hash format. See [Password Hashing Algorithms](/identityserver/identity/user-management/reference/password-hashing/) for how to implement and register one. If you have multiple hash algorithms registered and need to determine which one is the preferred algorithm (the one used to hash new passwords), inject `PasswordHashAlgorithms` from `Duende.UserManagement.Authentication`: ```csharp using Duende.UserManagement.Authentication; public class MyImportService(PasswordHashAlgorithms hashAlgorithms) { public PasswordImport HashPlaintextPassword(string plaintextPassword) { // Hash with the preferred algorithm (e.g., PBKDF2-SHA512) var hashedData = hashAlgorithms.Preferred.Hash(plaintextPassword); return new PasswordImport(hashedData); } } ``` This is useful when importing from a source where you have access to plaintext passwords (for example, a CSV export or a live migration). Rather than storing the source hash verbatim and implementing a custom `IPasswordHashAlgorithm`, you can hash directly with the preferred algorithm. A few things to sort out before you start: * **Groups and roles must exist first.** `MembershipImport` references groups and roles by ID. If a referenced group or role does not exist at import time, the record fails. Create them before you run the import. * **Decide on batch size.** For small datasets (a few thousand users), a single import call works fine. For larger datasets, process records in chunks of 100 to 500. Smaller batches make it easier to track progress, isolate failures, and resume after an interruption. * **Test with a small batch.** Run a representative sample of 10 to 20 records before committing to a full migration. Verify that passwords verify correctly, profile attributes land in the right fields, and memberships are assigned as expected. ### Migrating from ASP.NET Identity [Section titled “Migrating from ASP.NET Identity”](#migrating-from-aspnet-identity) If you are moving from Duende IdentityServer with ASP.NET Identity to User Management, see the [ASP.NET Identity Integration](/identityserver/identity/aspnet-identity/) docs for background on how the existing integration works. ASP.NET Identity stores user data across several tables. Here is how each concept maps to User Management’s import types: * **`AspNetUsers.Id`** → `UserImportRecord.SubjectId` — you can use the GUID string directly as the subject ID. * **`AspNetUsers.UserName`** → `UserImportRecord.ProfileAttributes` — map to a `preferred_username` profile attribute using the schema (see below). * **`AspNetUsers.Email`, `PhoneNumber`, and other profile columns** → `UserImportRecord.ProfileAttributes` — map each column to a profile attribute using the schema. * **`AspNetUsers.PasswordHash`** → `AuthenticatorImport.Password` — requires a custom `IPasswordHashAlgorithm` to verify the ASP.NET Identity hash format. See [Migrating from ASP.NET Identity](/identityserver/identity/user-management/import/aspnet-identity/) for the format details and a complete implementation. * **`AspNetUserRoles` + `AspNetRoles`** → `MembershipImport.DirectRoles` — create the roles first using `IRoleAdmin`, then reference them by ID during import. * **`AspNetUserClaims`** → `UserImportRecord.ProfileAttributes` — claims that represent profile data (name, address, etc.) map to profile attributes. Claims used for authorization decisions map better to roles or groups. * **`AspNetUserLogins`** → `AuthenticatorImport.ExternalAuthenticatorAddresses` — each row becomes an `ExternalAuthenticatorAddress` with the `LoginProvider` as the provider name and `ProviderKey` as the subject ID. * **`AspNetUserTokens`** → not imported — tokens (2FA recovery codes, authenticator keys) are runtime state. For TOTP authenticator keys, use `AuthenticatorImport.TotpAuthenticators` if you have access to the raw secret. Recovery codes can be imported via `AuthenticatorImport.RecoveryCodes`. Passwords are the trickiest part of the migration because ASP.NET Identity uses a proprietary binary format for its hashes. You need to register a custom `IPasswordHashAlgorithm` that can verify these hashes. The [Migrating from ASP.NET Identity](/identityserver/identity/user-management/import/aspnet-identity/) page explains the format and includes a complete implementation. For a complete walkthrough with working code, see [Migrating from ASP.NET Identity](/identityserver/identity/user-management/import/aspnet-identity/). ### Batch Processing [Section titled “Batch Processing”](#batch-processing) You do not have to import every user in a single call. For large datasets, splitting the work into chunks is more practical. Smaller batches keep memory usage reasonable, make failures easier to diagnose, and let you checkpoint progress so a crash does not force you to start over. The key thing that makes batching straightforward is that each record in a batch is processed independently. If record 47 out of 250 fails, the other 249 still succeed. After each batch completes, inspect `result.Results` for entries with `Status == UserImportStatus.Failed`, log the `SubjectId` and `Error`, and collect them for a retry pass. If you expect to re-run the import (for example, after fixing bad source data), configure your conflict resolver to return `Overwrite` for `ProfileAlreadyExists` and `AuthenticatorAlreadyExists`. That way, re-submitting a record that was already imported updates it in place rather than failing. This makes the whole process idempotent; you can run it as many times as you need without worrying about duplicates. Conflict Resolution A conflict resolver controls what happens when an import record collides with data that already exists in the store. The default resolver (`DefaultUserImportConflictResolver`) skips most conflicts and retries on concurrency errors, which is safe for a first-time import but means re-running the same batch will skip every record. For idempotent re-runs, you need a custom resolver that returns `Overwrite` instead of `Skip`. See [Conflict Resolution](/identityserver/identity/user-management/import/reference/#conflict-resolution) for the full details and a custom resolver example. BatchImportService.cs ```csharp public class BatchImportService(IUserImporter importer) { private const int BatchSize = 250; public async Task ImportAllAsync(IEnumerable allRecords, CancellationToken ct) { var failed = new List(); var batch = new List(BatchSize); int totalCreated = 0, totalUpdated = 0, totalFailed = 0; foreach (var record in allRecords) { batch.Add(record); if (batch.Count < BatchSize) continue; var result = await importer.ImportAsync(batch, ct); totalCreated += result.CreatedCount; totalUpdated += result.UpdatedCount; totalFailed += result.FailedCount; failed.AddRange(result.Results.Where(r => r.Status == UserImportStatus.Failed)); batch.Clear(); } // Process the final partial batch. if (batch.Count > 0) { var result = await importer.ImportAsync(batch, ct); totalCreated += result.CreatedCount; totalUpdated += result.UpdatedCount; totalFailed += result.FailedCount; failed.AddRange(result.Results.Where(r => r.Status == UserImportStatus.Failed)); } Console.WriteLine($"Created: {totalCreated}, Updated: {totalUpdated}, Failed: {totalFailed}"); foreach (var r in failed) Console.WriteLine($" FAILED {r.SubjectId}: {r.Error}"); } } ``` ## Where to go from here [Section titled “Where to go from here”](#where-to-go-from-here) The best starting point depends on where your users live today: [Migrating from ASP.NET Identity](/identityserver/identity/user-management/import/aspnet-identity)Step-by-step guide covering source data extraction, password hash verification, and building import records. [Import API reference](/identityserver/identity/user-management/import/reference)Full reference for IUserImporter, UserImportRecord, conflict resolution, and result handling. [Password hashing algorithms](/identityserver/identity/user-management/reference/password-hashing)Implement and register a custom IPasswordHashAlgorithm to verify hashes from your source system. [User Management Sample](/identityserver/samples/usermanagement)A working sample that includes ASP.NET Identity migration with password hash compatibility and claims-to-attributes mapping. ----- # Migrating from ASP.NET Identity > Step-by-step guide to migrating users from an ASP.NET Identity database into Duende User Management using a hosted service and the IUserImporter API. You have a working application with real users. Passwords are stored, sessions are active, and everything runs. Now you want to adopt Duende User Management without losing a single account or forcing anyone to reset their password. This guide walks you through building a hosted service that reads users from your existing ASP.NET Identity database and imports them into User Management. The approach: query the source database directly, map each user to a `UserImportRecord`, and call `IUserImporter.ImportAsync`. When the migration is done, your users log in exactly as before. They will not notice anything changed. Before you start, you need two things: an existing ASP.NET Identity database with users to migrate, and a target IdentityServer application that already has User Management configured. If you have not set up User Management yet, see the [getting started guide](/identityserver/identity/user-management/getting-started/) first. ## How it works [Section titled “How it works”](#how-it-works) The import API accepts a list of `UserImportRecord` objects. Each record describes one user: their subject ID, profile attributes, password, and any other authenticators. You call `IUserImporter.ImportAsync` with a batch of records, and the importer creates or updates each user independently. A failure on one record does not affect the others. The import is idempotent by default for new users. If you run it a second time, existing records are skipped rather than duplicated. If you want re-runs to overwrite existing data (useful when fixing source data and re-importing), you can register a custom conflict resolver (see [Conflict Resolution](/identityserver/identity/user-management/import/reference/#conflict-resolution) for details). Passwords carry over without any disruption. ASP.NET Identity uses a proprietary binary format for its hashes. You store the hash as-is during import, and register a custom `IPasswordHashAlgorithm` that knows how to verify it. On the first successful login after migration, User Management transparently re-hashes the password with its preferred algorithm. The user types their password, it works, and the hash is silently upgraded in the background. ## Set Up The Migration [Section titled “Set Up The Migration”](#set-up-the-migration) Your IdentityServer application already has `AddIdentityServer().AddUserManagement(...)` configured. You do not need a separate project. Instead, you add the migration as a hosted service that runs once on startup, imports the users, and then stops. Add the packages needed to read the source ASP.NET Identity database: ```bash dotnet add package Microsoft.AspNetCore.Identity.EntityFrameworkCore dotnet add package Npgsql.EntityFrameworkCore.PostgreSQL ``` Note The example code assumes the source database is PostgreSQL. Replace `Npgsql.EntityFrameworkCore.PostgreSQL` with the Entity Framework Core provider for your source database. In your existing `Program.cs`, register the source database context and the hosted service: ```csharp // In your existing Program.cs, add the source database context var sourceConnectionString = builder.Configuration.GetConnectionString("Source")!; builder.Services.AddDbContext(options => options.UseNpgsql(sourceConnectionString)); // Register the migration as a hosted service builder.Services.AddHostedService(); // Register an overwrite resolver so re-runs update existing records builder.Services.AddSingleton(); ``` Your User Management registration already includes the custom `AspNetIdentityPasswordHashAlgorithm` password hash algorithm. Make sure it includes: ```csharp using Duende.IdentityServer; builder.Services.AddIdentityServer() .AddUserManagement(users => { users.Authentication(auth => { auth.AddPasswordHashAlgorithm(); }); }); ``` Add the source connection string to `appsettings.json`: appsettings.json ```json { "ConnectionStrings": { "Source": "Host=localhost;Database=identity_db;Username=postgres;Password=..." } } ``` ## Connect to Your ASP.NET Identity Database [Section titled “Connect to Your ASP.NET Identity Database”](#connect-to-your-aspnet-identity-database) You need a minimal `DbContext` that points at the source database. You are reading data directly, not using `UserManager`. SourceIdentityDbContext.cs ```csharp using Microsoft.AspNetCore.Identity; using Microsoft.AspNetCore.Identity.EntityFrameworkCore; using Microsoft.EntityFrameworkCore; public sealed class SourceIdentityDbContext(DbContextOptions options) : IdentityDbContext(options) { } ``` If your source app uses a custom `ApplicationUser` class, substitute it for `IdentityUser` here. The important tables are `AspNetUsers` and `AspNetUserClaims`, which `IdentityDbContext` maps for you automatically. ## ASP.NET Identity Password Hash Format [Section titled “ASP.NET Identity Password Hash Format”](#aspnet-identity-password-hash-format) The `PasswordHash` column in the `AspNetUsers` table contains a base64-encoded blob. The first byte identifies the format version: * `0x00` — V2 format (ASP.NET Identity 2.x): 1 byte version + 16 bytes salt + 32 bytes PBKDF2-SHA1 hash (1000 iterations) * `0x01` — V3 format (ASP.NET Core Identity 3.x and later): 1 byte version + 4 bytes PRF (big-endian) + 4 bytes iteration count (big-endian) + 4 bytes salt length (big-endian) + N bytes salt + M bytes PBKDF2 hash output PRF values for V3: 0 = SHA1, 1 = SHA256, 2 = SHA512. For more detail on the ASP.NET Core Identity password hasher internals, see the [ASP.NET Identity `PasswordHasher` source on GitHub](https://github.com/dotnet/aspnetcore/blob/main/src/Identity/Extensions.Core/src/PasswordHasher.cs). ## Register the Password Hash Algorithm [Section titled “Register the Password Hash Algorithm”](#register-the-password-hash-algorithm) ASP.NET Identity stores passwords as base64-encoded binary blobs. User Management does not know how to verify these out of the box, so you need to register a custom `IPasswordHashAlgorithm` that handles them. Add this class to your project: AspNetIdentityPasswordHashAlgorithm.cs ```csharp using System.Buffers.Binary; using System.Security.Cryptography; using Duende.UserManagement.Authentication.Passwords; public sealed class AspNetIdentityPasswordHashAlgorithm : IPasswordHashAlgorithm { public const string Id = "aspnet-identity"; private const string ParamBlob = "blob"; public string AlgorithmId => Id; // Never produce new hashes in this format -- only verify imported ones. public HashedPasswordData Hash(string password) => throw new NotSupportedException( "This algorithm is for verifying imported hashes only."); public bool Verify(string password, HashedPasswordData data) { if (!data.Parameters.TryGetValue(ParamBlob, out var blob)) return false; byte[] bytes; try { bytes = Convert.FromBase64String(blob); } catch (FormatException) { return false; } if (bytes.Length == 0) return false; return bytes[0] switch { 0x00 => VerifyV2(password, bytes), 0x01 => VerifyV3(password, bytes), _ => false }; } // Always true: on first login, the password is re-hashed with the preferred algorithm. public bool NeedsRehash(HashedPasswordData data) => true; public static HashedPasswordData CreateImportData(string aspNetPasswordHash) => new(Id, [], [], new Dictionary { [ParamBlob] = aspNetPasswordHash }); // V2: 0x00 || salt (16 bytes) || PBKDF2-SHA1 hash (32 bytes), 1000 iterations private static bool VerifyV2(string password, byte[] bytes) { if (bytes.Length != 49) return false; var salt = bytes.AsSpan(1, 16); var stored = bytes.AsSpan(17, 32); var derived = Rfc2898DeriveBytes.Pbkdf2(password, salt, 1000, HashAlgorithmName.SHA1, 32); return CryptographicOperations.FixedTimeEquals(derived, stored); } // V3: 0x01 || PRF (4 BE) || iterations (4 BE) || saltLen (4 BE) || salt || hash private static bool VerifyV3(string password, byte[] bytes) { if (bytes.Length < 13) return false; var prf = BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(1, 4)); var iterations = (int)BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(5, 4)); var saltLen = (int)BinaryPrimitives.ReadUInt32BigEndian(bytes.AsSpan(9, 4)); if (iterations == 0 || saltLen == 0 || bytes.Length <= 13 + saltLen) return false; var salt = bytes.AsSpan(13, saltLen); var stored = bytes.AsSpan(13 + saltLen); var hashAlg = prf switch { 0 => HashAlgorithmName.SHA1, 1 => HashAlgorithmName.SHA256, 2 => HashAlgorithmName.SHA512, _ => default }; if (hashAlg == default) return false; var derived = Rfc2898DeriveBytes.Pbkdf2(password, salt, iterations, hashAlg, stored.Length); return CryptographicOperations.FixedTimeEquals(derived, stored); } } ``` Rather than parsing the hash at import time, the recommended approach is to store the raw base64 blob verbatim and verify it at login time using this custom `IPasswordHashAlgorithm`. This handles both V2 and V3 hashes, and transparently re-hashes the password with your preferred algorithm on the user’s first successful login. Use `AspNetIdentityPasswordHashAlgorithm.CreateImportData(user.PasswordHash)` to wrap the raw hash during import, and register the algorithm with `auth.AddPasswordHashAlgorithm()` in your User Management setup. See [Password Hashing Algorithms](/identityserver/identity/user-management/reference/password-hashing/) for a full walkthrough of implementing and registering a custom algorithm, including a complete example of a read-only legacy algorithm. ## Map Users to User Management [Section titled “Map Users to User Management”](#map-users-to-user-management) This is the core of the migration: reading each `IdentityUser` and its claims, then producing a `UserImportRecord`. The key decisions: * **`SubjectId`**: use `UserSubjectId.Create(user.Id)` to carry the existing GUID across as-is. This preserves any existing tokens or references that use the subject ID. * **`ProfileAttributes`**: map email, phone, username, and claims to profile attributes using the schema. The ASP.NET Identity `UserName` column maps to the SCIM `userName` attribute. * **`Password`**: wrap the raw base64 hash using `AspNetIdentityPasswordHashAlgorithm.CreateImportData`. Add a helper method that takes an `IdentityUser` and its claims and returns a `UserImportRecord`: MigrationHelpers.cs ```csharp using Duende.Storage.EntityAttributeValue; using Duende.UserManagement; using Duende.UserManagement.Authentication.Otp; using Duende.UserManagement.Authentication.Passkeys; using Duende.UserManagement.Import; using Microsoft.AspNetCore.Identity; static UserImportRecord MapUser( IdentityUser user, IReadOnlyList> claims, IReadOnlyList passkeys, IReadOnlyAttributeSchema schema) { var attrs = new AttributeValueCollection(schema); // Email as a multi-valued complex attribute if (!string.IsNullOrEmpty(user.Email)) { IReadOnlyList emailList = [ (IReadOnlyDictionary)new Dictionary { ["value"] = user.Email, ["type"] = "work", ["primary"] = true } ]; var emailCode = AttributeCode.Create("emails"); if (schema.AttributeDefinitions.ContainsKey(emailCode)) attrs.Set(emailCode, emailList); } // Username as a SCIM attribute if (!string.IsNullOrEmpty(user.UserName)) { var userNameCode = AttributeCode.Create("userName"); if (schema.AttributeDefinitions.ContainsKey(userNameCode)) attrs.Set(userNameCode, user.UserName); } // Simple scalar claims mapped to profile attributes foreach (var claim in claims) { var attrName = MapClaimToAttribute(claim.ClaimType); if (attrName is null) continue; var code = AttributeCode.Create(attrName); if (schema.AttributeDefinitions.ContainsKey(code)) attrs.Set(code, claim.ClaimValue); } // Name as a complex attribute built from given_name / family_name / middle_name claims var nameProps = new Dictionary(); foreach (var claim in claims) { switch (claim.ClaimType) { case "given_name": nameProps["givenName"] = claim.ClaimValue; break; case "family_name": nameProps["familyName"] = claim.ClaimValue; break; case "middle_name": nameProps["middleName"] = claim.ClaimValue; break; } } if (nameProps.Count > 0) { var nameCode = AttributeCode.Create("name"); if (schema.AttributeDefinitions.ContainsKey(nameCode)) attrs.Set(nameCode, (IReadOnlyDictionary)nameProps); } // Password PasswordImport? password = null; if (!string.IsNullOrEmpty(user.PasswordHash)) { password = new PasswordImport( AspNetIdentityPasswordHashAlgorithm.CreateImportData(user.PasswordHash)); } // OTP email address (for email-based one-time passwords) var otpAddresses = new List(); if (!string.IsNullOrEmpty(user.Email) && EmailAddress.TryCreate(user.Email, out var emailAddress)) { otpAddresses.Add(new OtpAddress(OtpChannel.Email, emailAddress)); } // Address as a complex attribute from claims var addressProps = new Dictionary(); foreach (var claim in claims) { switch (claim.ClaimType) { case "street_address": addressProps["streetAddress"] = claim.ClaimValue; break; case "locality": addressProps["locality"] = claim.ClaimValue; break; case "region": addressProps["region"] = claim.ClaimValue; break; case "postal_code": addressProps["postalCode"] = claim.ClaimValue; break; case "country": addressProps["country"] = claim.ClaimValue; break; } } if (addressProps.Count > 0) { addressProps.TryAdd("primary", false); IReadOnlyList addressList = [(IReadOnlyDictionary)addressProps]; var addressCode = AttributeCode.Create("addresses"); if (schema.AttributeDefinitions.ContainsKey(addressCode)) attrs.Set(addressCode, addressList); } // Passkeys var passkeyImports = passkeys .Select(p => new PasskeyImport { CredentialId = p.CredentialId, PublicKeyCose = p.PublicKey, Algorithm = p.Algorithm, SignCount = p.SignCount, BackupEligible = p.IsBackupEligible, BackedUp = p.IsBackedUp, Aaguid = p.Aaguid, Name = p.Name ?? "Imported passkey" }) .ToList(); // Set active status var activeCode = AttributeCode.Create("active"); if (schema.AttributeDefinitions.ContainsKey(activeCode)) attrs.Set(activeCode, true); return new UserImportRecord { SubjectId = UserSubjectId.Create(user.Id), ProfileAttributes = attrs.Count > 0 ? attrs.Validate() : null, Authenticators = new AuthenticatorImport { Password = password, OtpAddresses = otpAddresses.Count > 0 ? otpAddresses : null, Passkeys = passkeyImports.Count > 0 ? passkeyImports : null } }; } static string? MapClaimToAttribute(string claimType) => claimType switch { "display_name" => "displayName", "title" => "title", "user_type" => "userType", "preferred_language" => "preferredLanguage", "locale" => "locale", "timezone" => "timezone", // given_name, family_name, middle_name are handled as the complex "name" attribute // street_address, locality, region, postal_code, country are handled as the complex "addresses" attribute // phone_number is handled separately below if needed _ => null }; // Data class for passkey credentials read from the source database record PasskeyData( byte[] CredentialId, byte[] PublicKey, int Algorithm, uint SignCount, bool IsBackupEligible, bool IsBackedUp, Guid Aaguid, string? Name); ``` Notice that `given_name`, `family_name`, and `middle_name` are not mapped by `MapClaimToAttribute` They are assembled into the complex `name` attribute instead, because User Management stores name components as a structured object rather than flat strings. ## Run the Import [Section titled “Run the Import”](#run-the-import) Now wire everything together in a hosted service. The service runs once on application startup, imports the users, and completes: AspNetIdentityMigrationService.cs ```csharp using Duende.Storage.Schema; using Duende.Storage.EntityAttributeValue; using Duende.UserManagement; using Duende.UserManagement.Import; using Duende.UserManagement.Profiles; using Microsoft.AspNetCore.Identity; using Microsoft.EntityFrameworkCore; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; public class AspNetIdentityMigrationService : IHostedService { private readonly IServiceProvider _serviceProvider; private readonly ILogger _logger; public AspNetIdentityMigrationService( IServiceProvider serviceProvider, ILogger logger) { _serviceProvider = serviceProvider; _logger = logger; } public async Task StartAsync(CancellationToken cancellationToken) { await using var scope = _serviceProvider.CreateAsyncScope(); var sp = scope.ServiceProvider; // Ensure the User Management schema exists in the target database. await sp.GetRequiredService().CreateIfNotExistsAsync(cancellationToken); var sourceDb = sp.GetRequiredService(); var importer = sp.GetRequiredService(); var profileAdmin = sp.GetRequiredService(); var schema = await profileAdmin.GetSchemaAsync(cancellationToken); // Load all users and their claims from the source database var users = await sourceDb.Users .AsNoTracking() .ToListAsync(cancellationToken); var claimsByUser = await sourceDb.UserClaims .AsNoTracking() .GroupBy(c => c.UserId) .ToDictionaryAsync( g => g.Key, g => (IReadOnlyList>)g.ToList(), cancellationToken); _logger.LogInformation("Found {UserCount} users to migrate.", users.Count); const int batchSize = 100; var totalCreated = 0; var totalUpdated = 0; var totalFailed = 0; for (var i = 0; i < users.Count; i += batchSize) { var batch = users .Skip(i) .Take(batchSize) .Select(u => { var claims = claimsByUser.GetValueOrDefault(u.Id, []); return MapUser(u, claims, schema); }) .ToList(); var result = await importer.ImportAsync(batch, cancellationToken); totalCreated += result.CreatedCount; totalUpdated += result.UpdatedCount; totalFailed += result.FailedCount; foreach (var r in result.Results.Where(r => r.Status == UserImportStatus.Failed)) { _logger.LogWarning("FAILED {SubjectId}: {Error}", r.SubjectId, r.Error); } _logger.LogInformation( "Batch {BatchNumber}: created={Created}, updated={Updated}, failed={Failed}", i / batchSize + 1, result.CreatedCount, result.UpdatedCount, result.FailedCount); } _logger.LogInformation( "Migration complete. Created: {Created}, Updated: {Updated}, Failed: {Failed}", totalCreated, totalUpdated, totalFailed); } public Task StopAsync(CancellationToken cancellationToken) => Task.CompletedTask; } ``` ## Register the Conflict Resolver [Section titled “Register the Conflict Resolver”](#register-the-conflict-resolver) By default, re-running the import skips records that already exist. For a migration you may want to re-run after fixing source data, so register a resolver that overwrites existing records: OverwriteConflictResolver.cs ```csharp using Duende.UserManagement.Import; public sealed class OverwriteConflictResolver : IUserImportConflictResolver { public Task ResolveAsync( UserImportConflict conflict, CancellationToken ct) { UserImportConflictResolution resolution = conflict.Reason switch { UserImportConflictReason.ConcurrencyConflict => new UserImportConflictResolution.Retry(), _ => new UserImportConflictResolution.Overwrite(conflict.Record.SubjectId) }; return Task.FromResult(resolution); } } ``` This resolver overwrites any existing profile or authenticator data for the same subject ID, and retries on concurrency conflicts. Register it in your `Program.cs` alongside the other service registrations: ```csharp using Duende.IdentityServer; builder.Services.AddIdentityServer() .AddUserManagement(users => { users.Authentication(auth => { auth.AddPasswordHashAlgorithm(); }); }); ``` ## Mapping Claims to Profile Attributes [Section titled “Mapping Claims to Profile Attributes”](#mapping-claims-to-profile-attributes) ASP.NET Identity stores extra user data as claims in the `AspNetUserClaims` table. The claim type is a string key, and the claim value is the data. User Management stores profile data as typed attributes with a schema. The table below shows how common claim types map to User Management attribute codes: | ASP.NET Identity claim type | User Management attribute code | Notes | | --------------------------- | ------------------------------ | ------------------------------------------------ | | `given_name` | `name.givenName` (complex) | Assembled into the `name` complex attribute | | `family_name` | `name.familyName` (complex) | Assembled into the `name` complex attribute | | `middle_name` | `name.middleName` (complex) | Assembled into the `name` complex attribute | | `display_name` | `displayName` | Simple string attribute | | `title` | `title` | Simple string attribute | | `user_type` | `userType` | Simple string attribute | | `preferred_language` | `preferredLanguage` | Simple string attribute | | `locale` | `locale` | Simple string attribute | | `timezone` | `timezone` | Simple string attribute | | `phone_number` | `phoneNumbers` (complex list) | Multi-valued complex attribute | | `street_address` | `addresses.streetAddress` | Assembled into the `addresses` complex attribute | | `locality` | `addresses.locality` | Assembled into the `addresses` complex attribute | | `region` | `addresses.region` | Assembled into the `addresses` complex attribute | | `postal_code` | `addresses.postalCode` | Assembled into the `addresses` complex attribute | | `country` | `addresses.country` | Assembled into the `addresses` complex attribute | The `name`, `emails`, `phoneNumbers`, and `addresses` attributes are multi-valued complex types. You cannot set them with a plain string: you need to build a dictionary or list of dictionaries and pass it to `collection.Set`. The `MapUser` helper above shows how to do this for `name`, `emails`, and `addresses`. For phone numbers, the pattern is the same as for emails: ```csharp var phoneClaim = claims.FirstOrDefault(c => c.ClaimType == "phone_number"); if (phoneClaim is not null) { IReadOnlyList phoneList = [ (IReadOnlyDictionary)new Dictionary { ["value"] = phoneClaim.ClaimValue, ["type"] = "work", ["primary"] = false } ]; var phoneCode = AttributeCode.Create("phoneNumbers"); if (schema.AttributeDefinitions.ContainsKey(phoneCode)) { attrs.Set(phoneCode, phoneList); } } ``` The attribute codes and their types depend on the schema you have configured in User Management. The `schema.AttributeDefinitions.ContainsKey(code)` check in the mapping code guards against trying to set an attribute that does not exist in your schema, which would cause an import failure. ## Importing Passkeys [Section titled “Importing Passkeys”](#importing-passkeys) If your ASP.NET Identity database stores passkey (WebAuthn/FIDO2) credentials, you can import them alongside passwords and profile data. Each passkey is represented by a `PasskeyImport` object: ```csharp new PasskeyImport { CredentialId = credentialIdBytes, // The raw credential ID (byte[]) PublicKeyCose = publicKeyBytes, // COSE-encoded public key (byte[]) Algorithm = -7, // COSE algorithm identifier (e.g., -7 for ES256, -257 for RS256) SignCount = 0, // The current signature counter BackupEligible = true, // Whether the credential can be backed up BackedUp = false, // Whether the credential is currently backed up Aaguid = Guid.Empty, // The authenticator's AAGUID (or Guid.Empty if unknown) Name = "Imported passkey" // A human-readable name for the credential }; ``` The `Algorithm` field is the COSE algorithm identifier from the credential’s public key. Common values: * `-7` - ES256 (ECDSA with P-256 and SHA-256) * `-257` - RS256 (RSASSA-PKCS1-v1\_5 with SHA-256) * `-8` - EdDSA If your source database stores the COSE public key directly, you can extract the algorithm from key label 3 in the COSE key map. If you only know the key type, use the appropriate default (most WebAuthn implementations use ES256). Add the passkeys list to `AuthenticatorImport.Passkeys` in the import record. The `MapUser` helper above shows this pattern. ## Running the migration [Section titled “Running the migration”](#running-the-migration) **Test with a small batch first.** Before migrating all users, limit the query to 10 or 20 records and verify the results: ```csharp var users = await sourceDb.Users .AsNoTracking() .Take(10) // remove this line for the full migration .ToListAsync(cancellationToken); ``` Check that: * Passwords verify correctly by logging in as one of the migrated users. * Profile attributes land in the right fields in the User Management admin UI. * The subject IDs match what you expect. Once you are satisfied, remove the `Take(10)` and deploy the application. The hosted service runs the full migration on startup. After the migration completes, remove the `AddHostedService()` registration so it does not run again on subsequent deployments. **Run against staging first.** Deploy to a staging environment and run the full migration there before touching production. This gives you a chance to catch mapping errors without any risk to live users. **The import is idempotent.** With the `OverwriteConflictResolver` registered, you can re-run the migration as many times as you need. Each run updates existing records in place. This is useful when you discover a mapping bug and need to fix and re-import. **Check the output.** After each run, look at the `Failed` count and the error messages. Common failure causes are: * An attribute code in your mapping does not exist in the schema — add the attribute definition first. * A unique attribute value is already claimed by a different subject ID — this can happen if you have duplicate email addresses or usernames in the source database. * A malformed password hash — `AspNetIdentityPasswordHashAlgorithm` stores the raw base64 hash verbatim during import. If the hash starts with an unexpected version byte, verification will fail on the user’s first login attempt. Those users will need to reset their password. ## Next steps [Section titled “Next steps”](#next-steps) [Getting started](/identityserver/identity/user-management/getting-started)Set up User Management in your target application if you have not already. [Conflict resolution](/identityserver/identity/user-management/import/reference#conflict-resolution)Control what happens when an imported record collides with existing data. [Batch processing](/identityserver/identity/user-management/import/#batch-processing)Chunk large datasets, track progress, and resume after failures. ----- # Import API reference > API reference for IUserImporter, UserImportRecord, AuthenticatorImport, conflict resolution, and related types. This page covers the types and interfaces you use to build and submit import batches, handle results, and resolve conflicts when imported data overlaps with existing records. ## Importing Users [Section titled “Importing Users”](#importing-users) `IUserImporter` is the entry point for bulk import. It is registered as a transient service automatically when adding Duende User Management, and can be injected anywhere in your application. IUserImporter.cs ```csharp public interface IUserImporter { Task ImportAsync(IReadOnlyList records, CancellationToken ct); } ``` For each record in the batch, the importer first ensures a root user record exists for the given `SubjectId`. This record coordinates identity across all aspects and is created before any per-aspect work begins. Then, the importer runs up to three steps in order: 1. **Profile**: creates or updates the user profile with the provided `SubjectId` and `ProfileAttributes`. 2. **Authenticator**: creates or updates authenticator data (passwords, passkeys, TOTP keys, OTP addresses, external providers, recovery codes). 3. **Membership**: assigns the user to the specified groups and roles. Each step is optional. If you only provide `Authenticators` on a record, the profile and membership steps are skipped entirely. If a step encounters existing data (for example, a profile with the same `SubjectId` already exists), the importer calls the registered `IUserImportConflictResolver` to decide what to do. The default resolver retries on concurrency conflicts and skips everything else, which means a first-time import of new users works out of the box without any configuration. If a record needs to overwrite existing data, or if you want to customize the behavior for specific conflict reasons, you can register your own resolver. See [Conflict Resolution](#conflict-resolution) for details. A failure on one record does not affect the others in the batch. The importer processes every record and returns a `UserImportBatchResult` with per-record outcomes, so you always know exactly which records succeeded and which did not. ### End-to-end example [Section titled “End-to-end example”](#end-to-end-example) The following example imports two users: one with a password and group membership, and one with an external authenticator. UserImportService.cs ```csharp public class UserImportService(IUserImporter importer, IUserProfileAdmin profileAdmin) { public async Task RunAsync(CancellationToken ct) { // Build profile attributes using the schema so values are type-validated. var schema = await profileAdmin.GetSchemaAsync(ct); var aliceAttributes = new AttributeValueCollection(schema); aliceAttributes.Set(AttributeCode.Create("email"), "alice@example.com"); aliceAttributes.Set(AttributeCode.Create("display_name"), "Alice"); // Represent a pre-hashed bcrypt password from the source system. // Hash and Salt are raw bytes; supply the actual bytes from your source data. var bcryptHash = new HashedPasswordData( algorithmId: "bcrypt", hash: new byte[] { /* raw hash bytes from source */ }, salt: new byte[] { /* raw salt bytes from source */ }, parameters: new Dictionary { ["cost"] = "12" }); var records = new List { new UserImportRecord { SubjectId = new UserSubjectId("user-001"), ProfileAttributes = aliceAttributes.Validate(), Authenticators = new AuthenticatorImport { Password = new PasswordImport(bcryptHash), }, Memberships = new MembershipImport { Groups = new[] { GroupId.Create("admins") }, }, }, new UserImportRecord { SubjectId = new UserSubjectId("user-002"), Authenticators = new AuthenticatorImport { ExternalAuthenticatorAddresses = new[] { new ExternalAuthenticatorAddress( Provider: "google", ProviderSubjectId: "google-sub-abc123" ), }, }, }, }; UserImportBatchResult result = await importer.ImportAsync(records, ct); Console.WriteLine($"Created: {result.CreatedCount}"); Console.WriteLine($"Updated: {result.UpdatedCount}"); Console.WriteLine($"Skipped: {result.SkippedCount}"); Console.WriteLine($"Failed: {result.FailedCount}"); foreach (UserImportResult r in result.Results) { if (r.Status == UserImportStatus.Failed) Console.WriteLine($" FAILED {r.SubjectId}: {r.Error}"); } } } ``` ## Building Import Records [Section titled “Building Import Records”](#building-import-records) This section covers the types you use to describe what gets imported for each user: the record itself, authenticator data, and group or role memberships. ### `UserImportRecord` [Section titled “UserImportRecord”](#userimportrecord) Each record describes a single user to import. Only `SubjectId` is required; all other fields are optional. UserImportRecord.cs ```csharp public sealed record UserImportRecord { public required UserSubjectId SubjectId { get; init; } public ValidatedAttributeValueCollection? ProfileAttributes { get; init; } public AuthenticatorImport? Authenticators { get; init; } public MembershipImport? Memberships { get; init; } } ``` You can provide any combination of `ProfileAttributes`, `Authenticators`, and `Memberships`. ### `AuthenticatorImport` [Section titled “AuthenticatorImport”](#authenticatorimport) `AuthenticatorImport` groups all authenticator data for a user. Each field is optional; include only the authenticator types you are migrating. AuthenticatorImport.cs ```csharp public sealed record AuthenticatorImport { public IReadOnlyCollection? OtpAddresses { get; init; } public IReadOnlyCollection? ExternalAuthenticatorAddresses { get; init; } public IReadOnlyCollection? Passkeys { get; init; } public PasswordImport? Password { get; init; } public IReadOnlyCollection? TotpAuthenticators { get; init; } public IReadOnlyCollection? RecoveryCodes { get; init; } } ``` #### `PasswordImport` [Section titled “PasswordImport”](#passwordimport) PasswordImport.cs ```csharp public sealed record PasswordImport(HashedPasswordData Data); ``` `PasswordImport` carries a pre-hashed password from the source system. The platform stores the hash as-is and verifies it using the `IPasswordHashAlgorithm` registered for the stored algorithm ID. On the first successful authentication, the password is transparently re-hashed using the current preferred algorithm, so users are migrated to the new hashing scheme without any disruption. #### `TotpDeviceImport` [Section titled “TotpDeviceImport”](#totpdeviceimport) TotpDeviceImport.cs ```csharp public sealed record TotpDeviceImport(TotpDeviceName Name, PlainBytesTotpKey Key); ``` `TotpDeviceImport` carries the raw TOTP secret key from the source system. Provide the key as a `PlainBytesTotpKey` and a display name for the authenticator. #### `PasskeyImport` [Section titled “PasskeyImport”](#passkeyimport) PasskeyImport.cs ```csharp public sealed record PasskeyImport { public required IReadOnlyList CredentialId { get; init; } public required IReadOnlyList PublicKeyCose { get; init; } public required int Algorithm { get; init; } public uint SignCount { get; init; } public bool BackupEligible { get; init; } public bool BackedUp { get; init; } public Guid Aaguid { get; init; } public required string Name { get; init; } } ``` `PasskeyImport` carries the raw WebAuthn credential data. `CredentialId` and `PublicKeyCose` are the byte arrays from the original registration ceremony. `Algorithm` is the COSE algorithm identifier (for example, `-7` for ES256). `SignCount`, `BackupEligible`, `BackedUp`, and `Aaguid` correspond to the authenticator data fields from the original attestation. ### `MembershipImport` [Section titled “MembershipImport”](#membershipimport) MembershipImport.cs ```csharp public sealed record MembershipImport { public IReadOnlyCollection? Groups { get; init; } public IReadOnlyCollection? DirectRoles { get; init; } } ``` `MembershipImport` assigns the user to existing groups and roles. The referenced groups and roles must already exist before you run the import; a missing group or role causes a hard failure on that individual record. ## Handling Results [Section titled “Handling Results”](#handling-results) ### `UserImportBatchResult` [Section titled “UserImportBatchResult”](#userimportbatchresult) `ImportAsync` returns a `UserImportBatchResult` with a per-record result list and aggregate counts. UserImportBatchResult.cs ```csharp public sealed record UserImportBatchResult { public required IReadOnlyList Results { get; init; } public int CreatedCount { get; } public int UpdatedCount { get; } public int SkippedCount { get; } public int FailedCount { get; } } ``` ### `UserImportResult` [Section titled “UserImportResult”](#userimportresult) Each entry in `UserImportBatchResult.Results` corresponds to one input record and reports the outcome for that record. UserImportResult.cs ```csharp public sealed record UserImportResult { public required UserSubjectId SubjectId { get; init; } public required UserImportStatus Status { get; init; } public string? Error { get; init; } } ``` When `Status` is `Failed`, `Error` contains a description of what went wrong. For all other statuses, `Error` is `null`. ### `UserImportStatus` enum [Section titled “UserImportStatus enum”](#userimportstatus-enum) | Value | Meaning | | --------- | ------------------------------------------------------------------------------ | | `Created` | The user was successfully created. | | `Updated` | The user was successfully updated (overwrite conflict resolution was applied). | | `Skipped` | The user was skipped because a conflict was resolved as `Skip`. | | `Failed` | The user failed to import due to an error. | ## Conflict Resolution [Section titled “Conflict Resolution”](#conflict-resolution) When the importer tries to create a profile, authenticator, or membership record and discovers that matching data already exists, it raises a conflict. Rather than failing immediately, the importer delegates the decision to an `IUserImportConflictResolver`. The resolver inspects the conflict (which record, which step, and why it conflicted) and returns a resolution: skip the step, overwrite the existing data, or retry the operation. This design keeps the import pipeline itself generic. The policy for handling duplicates, unique attribute collisions, and concurrency races lives in the resolver, which you can swap out without changing any import logic. ### Default behavior [Section titled “Default behavior”](#default-behavior) The built-in resolver is intentionally conservative. It retries when a concurrency conflict occurs (another process modified the same record at the same time) and skips everything else. In practice, this means: * A first-time import of new users works without any configuration. * Re-running the same import skips every record that was already imported, because the default resolver treats `ProfileAlreadyExists` and `AuthenticatorAlreadyExists` as skip. * If you need idempotent re-runs (where re-importing a record updates it in place), you need a custom resolver that returns `Overwrite` instead of `Skip`. ### `IUserImportConflictResolver` [Section titled “IUserImportConflictResolver”](#iuserimportconflictresolver) IUserImportConflictResolver.cs ```csharp public interface IUserImportConflictResolver { Task ResolveAsync(UserImportConflict conflict, CancellationToken ct); } ``` The default implementation is described [above](#default-behavior). To customize conflict handling, register your own implementation with the service provider (see [Registering a custom resolver](#registering-a-custom-resolver)). ### `UserImportConflict` [Section titled “UserImportConflict”](#userimportconflict) The resolver receives a `UserImportConflict` describing the record, the step that failed, and the reason. UserImportConflict.cs ```csharp public sealed record UserImportConflict { public required UserImportRecord Record { get; init; } public required UserImportStep Step { get; init; } public required UserImportConflictReason Reason { get; init; } public required Exception Exception { get; init; } } ``` ### `UserImportStep` enum [Section titled “UserImportStep enum”](#userimportstep-enum) | Value | Meaning | | --------------- | ------------------------------------------ | | `Profile` | The user profile creation or update step. | | `Authenticator` | The authenticator creation or update step. | | `Membership` | The membership assignment step. | ### `UserImportConflictReason` enum [Section titled “UserImportConflictReason enum”](#userimportconflictreason-enum) | Value | Meaning | | ---------------------------- | -------------------------------------------------------------------------------------------- | | `ProfileAlreadyExists` | A user profile with the same subject ID already exists. | | `ProfileUniqueKeyConflict` | A unique attribute value on the incoming record already belongs to a different user profile. | | `AuthenticatorAlreadyExists` | Authenticators for the same subject ID already exist. | | `AuthenticatorKeyConflict` | A unique authenticator key is already claimed by a different user. | | `ConcurrencyConflict` | An optimistic concurrency conflict occurred. | | `MembershipAlreadyExists` | A membership record for the same subject ID already exists. | ### `UserImportConflictResolution` [Section titled “UserImportConflictResolution”](#userimportconflictresolution) The resolver returns one of three resolutions: UserImportConflictResolution.cs ```csharp public abstract record UserImportConflictResolution { public sealed record Skip : UserImportConflictResolution; public sealed record Overwrite(UserSubjectId TargetSubjectId) : UserImportConflictResolution; public sealed record Retry : UserImportConflictResolution; } ``` * `Skip`: skips the conflicting step; existing data is left unchanged. * `Overwrite(TargetSubjectId)`: overwrites the existing user; profile attributes are overlaid and authenticators are merged additively. * `Retry`: retries the operation, which is useful when the resolver has taken corrective action such as deleting the conflicting record. Retries are subject to an internal cap. ### Registering a custom resolver [Section titled “Registering a custom resolver”](#registering-a-custom-resolver) `IUserImportConflictResolver` is registered as a singleton with the default implementation. To override it, register your own implementation before the default is used: Program.cs ```csharp builder.Services.AddSingleton(); ``` The following example overwrites existing profiles but skips all other conflicts: CustomConflictResolver.cs ```csharp public class CustomConflictResolver : IUserImportConflictResolver { public Task ResolveAsync( UserImportConflict conflict, CancellationToken ct) { UserImportConflictResolution resolution = conflict.Reason switch { UserImportConflictReason.ProfileAlreadyExists => new UserImportConflictResolution.Overwrite(conflict.Record.SubjectId), UserImportConflictReason.ConcurrencyConflict => new UserImportConflictResolution.Retry(), _ => new UserImportConflictResolution.Skip(), }; return Task.FromResult(resolution); } } ``` ----- # Logging > How to configure and use logging in Duende User Management, including log categories, levels, and troubleshooting guidance. Duende User Management uses the standard logging facilities provided by ASP.NET Core (`Microsoft.Extensions.Logging`). You don’t need any extra configuration to get rich logging out of the box. For log level definitions, environment guidance, and actionable next steps for each level, see the [Logging Fundamentals](/general/logging) guide. [Logging Fundamentals](/general/logging)Log level definitions, environment configuration table, and the log level anxiety spectrum. ## Configuration [Section titled “Configuration”](#configuration) User Management writes logs under the `Duende.UserManagement` category. More specific sub-categories exist for individual features: | Feature | Log category | | ------------------------- | ------------------------------------------------------------- | | Authentication (general) | `Duende.UserManagement.Authentication.Internal` | | Passwords | `Duende.UserManagement.Authentication.Passwords.Internal` | | Passkeys | `Duende.UserManagement.Authentication.Passkeys.Internal` | | One-Time Passwords (OTP) | `Duende.UserManagement.Authentication.Otp.Internal` | | TOTP | `Duende.UserManagement.Authentication.Totp.Internal` | | Recovery codes | `Duende.UserManagement.Authentication.RecoveryCodes.Internal` | | User profiles | `Duende.UserManagement.Profiles.Internal` | | Membership (groups/roles) | `Duende.UserManagement.Membership.Internal` | | User import | `Duende.UserManagement.Import.Internal` | To enable detailed logging for all User Management components, set the `Duende.UserManagement` namespace to `Debug` in your `appsettings.json`: appsettings.json ```json { "Logging": { "LogLevel": { "Default": "Information", "Duende.UserManagement": "Debug" } } } ``` If you only want to troubleshoot a specific area (for example, passkey authentication), you can target that sub-category: appsettings.json ```json { "Logging": { "LogLevel": { "Default": "Information", "Duende.UserManagement.Authentication.Passkeys.Internal": "Debug" } } } ``` Note In production, keep logging at `Warning` or higher to avoid excessive log volume. Drop to `Information` or `Debug` only when actively troubleshooting. For definitions of each log level and guidance on what to do when you see one, see [Logging Fundamentals](/general/logging). ## What Gets Logged [Section titled “What Gets Logged”](#what-gets-logged) Here is an overview of the key events that User Management logs, grouped by feature area. ### Password Authentication [Section titled “Password Authentication”](#password-authentication) * Authentication attempt started, succeeded, or failed * User not found (timing-safe dummy authentication is still performed) * Throttling applied by the attempt policy * Password re-hashed to a newer algorithm after successful authentication ### Passkey Authentication and Registration [Section titled “Passkey Authentication and Registration”](#passkey-authentication-and-registration) * Begin/complete ceremony started, succeeded, or failed * Challenge expired or not found * Credential not found, user mismatch, or sign count update failures * Registration rejected (unauthenticated, duplicate credential, persist failure) ### OTP (One-Time Password) [Section titled “OTP (One-Time Password)”](#otp-one-time-password) * OTP send started, succeeded, or blocked by rate limiting * OTP authentication started, succeeded, or failed (workflow not found, expired, verification failed) * Email send success or failure * No sender registered for a given address type ### TOTP (Time-Based One-Time Password) [Section titled “TOTP (Time-Based One-Time Password)”](#totp-time-based-one-time-password) * Authentication attempt started, succeeded, or failed * User not found (dummy authentication performed) * Throttling applied ### Recovery Codes [Section titled “Recovery Codes”](#recovery-codes) * Authentication attempt started, succeeded, or failed * User not found or throttled ### User Profiles [Section titled “User Profiles”](#user-profiles) * Profile created, found, updated, or not found * Profile registration (self-service) succeeded or failed * Schema attribute/group added, removed, or reordered ### Membership (Groups and Roles) [Section titled “Membership (Groups and Roles)”](#membership-groups-and-roles) * Group/role created, updated, deleted, or not found * Role/group assigned to or removed from a user or group * Version conflicts on updates ### User Import [Section titled “User Import”](#user-import) * Batch import started and completed (with counts of created, updated, skipped, and failed records) * Validation failures and conflict detection * Retry attempts ### Optimistic Concurrency [Section titled “Optimistic Concurrency”](#optimistic-concurrency) Several authentication flows log when an optimistic concurrency conflict occurs during failed-attempt recording. These are typically at `Information` level for the first retry and `Error` if the retry also fails. ## Structured Log Properties [Section titled “Structured Log Properties”](#structured-log-properties) Log messages include structured properties that you can use for filtering and correlation in your log sink: * `subjectId` — The user’s subject identifier * `userName` — The username (password authentication) * `groupId` — Group identifier (membership operations) * `roleId` — Role identifier (membership operations) * `error` — Error details for failed ceremonies These properties appear in the log scope, so structured logging sinks like [Seq](https://datalust.co/seq) or [Elasticsearch](https://www.elastic.co/elasticsearch) let you filter and search by them. ----- # Configuration Reference > Complete reference for all configuration options in Duende User Management, including authentication, passwords, passkeys, TOTP, throttling, and endpoint routing. Duende User Management is configured through a set of strongly-typed options classes. This page documents every configurable property, its type, default value, and purpose. ## Registration [Section titled “Registration”](#registration) Register User Management services in `Program.cs` using the builder pattern: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => auth.Configure(options => { options.Passwords.MinLength = 10; options.Passkeys.RelyingPartyName = "My Application"; options.Passkeys.AllowedOrigins = ["https://app.example.com"]; options.Throttling.MaxFailedAttempts = 3; })) ); ``` To configure both options and the feature builder in a single call: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.Configure(options => { options.Passkeys.RelyingPartyName = "My Application"; options.Passkeys.AllowedOrigins = ["https://app.example.com"]; }); auth.ConfigureEndpoints(endpoints => { endpoints.Passkeys.Route = "/auth/passkeys"; }); }) ); ``` ## `UserAuthenticationOptions` [Section titled “UserAuthenticationOptions”](#userauthenticationoptions) Top-level options class for authentication configuration. Accessed via `IOptions`. | Property | Type | Description | | --------------- | --------------------------------- | ---------------------------------------------------------------------------- | | `Totp` | `TotpOptions` | Configuration for Time-Based One-Time Password (TOTP) authenticator storage. | | `Passkeys` | `PasskeyOptions` | Configuration for passkey registration and authentication. | | `Passwords` | `PasswordOptions` | Configuration for the password validator. | | `RecoveryCodes` | `RecoveryCodeOptions` | Configuration for recovery code behavior. | | `Throttling` | `AuthenticationThrottlingOptions` | Configuration for per-authenticator attempt throttling. | All sub-option objects are initialized with their defaults automatically. You only need to set the properties you want to override. ## `PasswordOptions` [Section titled “PasswordOptions”](#passwordoptions) Controls the built-in password complexity validator. Accessed via `UserAuthenticationOptions.Passwords`. | Property | Type | Default | Description | | ------------------------ | -------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------- | | `MinLength` | `int` | `8` | Minimum required password length in characters. | | `MaxLength` | `int` | `64` | Maximum allowed password length. Capped at 64 characters (512 bits) to avoid PBKDF2 pre-hashing vulnerabilities with SHA-512. | | `MinLower` | `int` | `2` | Minimum number of lowercase letters required. | | `MinUpper` | `int` | `2` | Minimum number of uppercase letters required. | | `MinDigits` | `int` | `2` | Minimum number of numeric digit characters required. | | `MinSymbols` | `int` | `2` | Minimum number of symbol characters required. | | `HistoryCount` | `int` | `0` | Number of previous passwords to remember and reject on change or reset; `0` disables history. | | `MaxAgeDays` | `int?` | `null` | Maximum password age in days before the password is considered expired; `null` disables expiration. | | `PreferredHashAlgorithm` | `string` | `"pbkdf2"` | Algorithm used when hashing new passwords; see [Password Hashing Algorithms](/identityserver/identity/user-management/reference/password-hashing/). | Example (relaxed password policy): Program.cs ```csharp .Authentication(auth => auth.Configure(options => { options.Passwords.MinLength = 12; options.Passwords.MinLower = 1; options.Passwords.MinUpper = 1; options.Passwords.MinDigits = 1; options.Passwords.MinSymbols = 0; })) ``` ## `PasskeyOptions` [Section titled “PasskeyOptions”](#passkeyoptions) Controls WebAuthn/passkey registration and authentication behavior. Accessed via `UserAuthenticationOptions.Passkeys`. | Property | Type | Default | Description | | --------------------------------- | ------------------------ | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `RelyingPartyName` | `string` | Assembly name | Human-readable display name of the relying party shown to the user during registration. Does not affect security. | | `ServerDomain` | `string?` | `null` | The effective domain used as the WebAuthn Relying Party ID. Set explicitly to share passkeys across subdomains (e.g. `"example.com"` for `auth.example.com` and `app.example.com`). | | `AllowedOrigins` | `IReadOnlyList?` | `null` | Required. One or more fully-qualified origins (scheme + host + optional port) permitted to use passkeys. The `clientDataJSON.origin` from the authenticator is validated against this list. | | `ChallengeSize` | `int` | `32` | Size of the WebAuthn challenge in bytes (256 bits). | | `ChallengeTimeout` | `TimeSpan` | `00:05:00` | Maximum lifetime of a passkey challenge. Challenges are single-use and rejected after this duration. | | `UserVerificationRequirement` | `string` | `"preferred"` | User verification requirement for authentication. See [User Verification Values](#user-verification-values). | | `AttestationConveyancePreference` | `string` | `"none"` | Attestation conveyance preference for credential creation. See [Attestation Conveyance Values](#attestation-conveyance-values). | | `AuthenticatorAttachment` | `string?` | `null` | Restricts the authenticator attachment modality. `null` allows any authenticator type. See [Authenticator Attachment Values](#authenticator-attachment-values). | | `ResidentKeyRequirement` | `string` | `"preferred"` | Discoverable credential (resident key) requirement for registration. See [Resident Key Values](#resident-key-values). | | `SupportedAlgorithms` | `IReadOnlyList` | `[]` | COSE algorithm identifiers to support, in preference order. An empty list accepts all algorithms supported by the library. Use `CoseAlgorithms` constants to specify values. | ### User Verification Values [Section titled “User Verification Values”](#user-verification-values) The `UserVerificationRequirement` property accepts the following string values: * `"required"`: User verification must be performed (PIN, biometric, etc.). * `"preferred"`: User verification is preferred but not required. **(default)** * `"discouraged"`: User verification should not be performed. ### Attestation Conveyance Values [Section titled “Attestation Conveyance Values”](#attestation-conveyance-values) The `AttestationConveyancePreference` property accepts the following string values: * `"none"`: No attestation statement is needed. **(default)** * `"indirect"`: Attestation statement may be anonymized by the browser. * `"direct"`: Attestation statement should be provided directly by the authenticator. * `"enterprise"`: Enterprise attestation for managed authenticators. ### Authenticator Attachment Values [Section titled “Authenticator Attachment Values”](#authenticator-attachment-values) The `AuthenticatorAttachment` property accepts the following string values: * `null`: Any authenticator type is allowed. **(default)** * `"platform"`: Built-in authenticators only (Windows Hello, Touch ID, Face ID). * `"cross-platform"`: Roaming authenticators only (USB security keys, Bluetooth). ### Resident Key Values [Section titled “Resident Key Values”](#resident-key-values) The `ResidentKeyRequirement` property accepts the following string values: * `"preferred"`: Discoverable credential is preferred if the authenticator supports it. **(default)** * `"required"`: Discoverable credential is required. * `"discouraged"`: Non-discoverable credential is preferred. ### Passkey Configuration Example [Section titled “Passkey Configuration Example”](#passkey-configuration-example) Program.cs ```csharp .Authentication(auth => auth.Configure(options => { options.Passkeys.RelyingPartyName = "ACME Corporation"; options.Passkeys.ServerDomain = "example.com"; options.Passkeys.AllowedOrigins = [ "https://app.example.com", "https://auth.example.com" ]; options.Passkeys.UserVerificationRequirement = "required"; options.Passkeys.AuthenticatorAttachment = "platform"; options.Passkeys.ChallengeTimeout = TimeSpan.FromMinutes(3); })) ``` ## `TotpOptions` [Section titled “TotpOptions”](#totpoptions) Controls TOTP authenticator app configuration. Accessed via `UserAuthenticationOptions.Totp`. | Property | Type | Description | | --------- | ---------------- | ------------------------------------- | | `Storage` | `StorageOptions` | Controls how TOTP secrets are stored. | ### `StorageOptions` [Section titled “StorageOptions”](#storageoptions) Nested within `TotpOptions`. Controls TOTP secret storage behavior. | Property | Type | Default | Description | | ------------- | ------ | ------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `ProtectKeys` | `bool` | `true` | When `true`, TOTP secrets are encrypted at rest using [ASP.NET Core Data Protection](/general/data-protection/) before being stored. Disable only if your storage layer provides its own encryption. | Example (disable key protection; not recommended unless storage is encrypted externally): Program.cs ```csharp .Authentication(auth => auth.Configure(options => { options.Totp.Storage.ProtectKeys = false; })) ``` ## `AuthenticationThrottlingOptions` [Section titled “AuthenticationThrottlingOptions”](#authenticationthrottlingoptions) Controls the built-in per-authenticator failed-attempt throttling policy. Accessed via `UserAuthenticationOptions.Throttling`. | Property | Type | Default | Description | | ----------------------------- | -------------------------- | ---------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `MaxFailedAttempts` | `int` | `5` | Number of failed attempts allowed within the `FailureWindow` before throttling is applied. | | `FailureWindow` | `TimeSpan` | `00:15:00` | Rolling window from the last failure during which the failure count is tracked. If `LastFailedAtUtc + FailureWindow` has elapsed, the count resets to zero. | | `ThrottleDuration` | `TimeSpan` | `00:05:00` | How long to block further attempts after the threshold is exceeded, measured from the last failed attempt. | | `MaxAttemptsPerWindow` | `int` | `5` | Maximum total authentication attempts (successful and failed) allowed within the `VelocityWindow`. | | `VelocityWindow` | `TimeSpan` | `00:00:10` | Sliding window for counting total authentication attempts. | | `VelocityThrottleDuration` | `TimeSpan` | `00:00:30` | How long to block further attempts after the velocity threshold is exceeded. | | `EscalatingThrottleDurations` | `IReadOnlyList?` | `null` | Per-lockout durations for escalating lockout; when set, each successive lockout uses the next duration in the list; when `null` or empty, `ThrottleDuration` applies. | Example (stricter throttling): Program.cs ```csharp .Authentication(auth => auth.Configure(options => { options.Throttling.MaxFailedAttempts = 3; options.Throttling.FailureWindow = TimeSpan.FromMinutes(30); options.Throttling.ThrottleDuration = TimeSpan.FromMinutes(15); options.Throttling.MaxAttemptsPerWindow = 5; options.Throttling.VelocityWindow = TimeSpan.FromSeconds(10); options.Throttling.VelocityThrottleDuration = TimeSpan.FromSeconds(30); })) ``` ## `RecoveryCodeOptions` [Section titled “RecoveryCodeOptions”](#recoverycodeoptions) Controls recovery code generation and authentication. Accessed via `UserAuthenticationOptions.RecoveryCodes`. | Property | Type | Default | Description | | --------- | ------ | ------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `Count` | `int` | `10` | Number of recovery codes generated per call to `TryCreateRecoveryCodesAsync`; valid range is 1 to 50. | | `Enabled` | `bool` | `true` | When `false`, recovery codes are disabled; `TryCreateRecoveryCodesAsync` returns `null` and `TryAuthenticateAsync` returns `false`. | ## `UserAuthenticationEndpointOptions` [Section titled “UserAuthenticationEndpointOptions”](#userauthenticationendpointoptions) Controls the HTTP endpoint routes exposed by the web layer. Configure via `ConfigureEndpoints()` on the authentication builder: Program.cs ```csharp .Authentication(auth => { auth.Configure(options => { /* UserAuthenticationOptions */ }); auth.ConfigureEndpoints(endpoints => { endpoints.Passkeys.Route = "/auth/passkeys"; }); }) ``` Or bind from configuration: Program.cs ```csharp .Authentication(auth => { auth.Configure(options => { }); auth.ConfigureEndpoints( builder.Configuration.GetSection("UserAuthentication:Endpoints") ); }) ``` | Property | Type | Description | | ---------- | ---------------------- | ---------------------------------------------- | | `Passkeys` | `PasskeysRouteOptions` | Route configuration for all passkey endpoints. | ## `PasskeysRouteOptions` [Section titled “PasskeysRouteOptions”](#passkeysrouteoptions) Controls the individual route paths for passkey HTTP endpoints. All paths under `Passkeys` are relative to the `Route` prefix. Accessed via `UserAuthenticationEndpointOptions.Passkeys`. | Property | Type | Default | Description | | --------------------------------- | -------- | ------------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Route` | `string` | `"/passkeys"` | Base route prefix for all passkey endpoints. | | `BeginRegistration` | `string` | `"/register/begin"` | Path for the passkey registration initiation endpoint (relative to `Route`). Full default: `/passkeys/register/begin`. | | `CompleteRegistration` | `string` | `"/register/complete"` | Path for the passkey registration completion endpoint (relative to `Route`). Full default: `/passkeys/register/complete`. | | `BeginAuthentication` | `string` | `"/authenticate/begin"` | Path for the passkey authentication initiation endpoint (relative to `Route`). Full default: `/passkeys/authenticate/begin`. | | `BeginDiscoverableAuthentication` | `string` | `"/authenticate/discoverable/begin"` | Path for the discoverable (usernameless) passkey authentication initiation endpoint (relative to `Route`). Full default: `/passkeys/authenticate/discoverable/begin`. | | `CompleteAuthentication` | `string` | `"/authenticate/complete"` | Path for the passkey authentication completion endpoint (relative to `Route`). Full default: `/passkeys/authenticate/complete`. | | `PasskeysJavaScript` | `string` | `"/js"` | Path for the passkeys JavaScript helper endpoint (relative to `Route`). Full default: `/passkeys/js`. | Example (custom route prefix): Program.cs ```csharp auth.ConfigureEndpoints(endpoints => { endpoints.Passkeys.Route = "/auth/webauthn"; }) ``` This changes all passkey endpoints to use `/auth/webauthn` as the base, so registration begins at `/auth/webauthn/register/begin`, and so on. ## Membership Module [Section titled “Membership Module”](#membership-module) The membership module provides administrative services for managing users, roles, and groups within your application. It is registered automatically by `AddUserManagement()` when your application needs to programmatically create or modify users, assign roles, or manage group membership from server-side code (for example, in admin UIs or API endpoints). The following services are registered automatically with the service provider: | Service | Description | | ------------------ | ----------------------------------------------------------------------------------------------------------- | | `IMembershipAdmin` | Provides administrative operations for user accounts: creating, updating, deleting, and querying users. | | `IRoleAdmin` | Provides administrative operations for roles: creating, updating, deleting, and assigning roles to users. | | `IGroupAdmin` | Provides administrative operations for groups: creating, updating, deleting, and managing group membership. | All three services are registered with scoped lifetime and can be injected wherever you need to perform administrative operations on the user store. ----- # Password Hashing Algorithms > How to configure, extend, and migrate password hashing algorithms in Duende User Management, including the IPasswordHashAlgorithm interface, transparent re-hashing, and custom algorithm registration. Password hashing algorithms improve over time. What was considered strong a decade ago (MD5, SHA-1, even early PBKDF2 iteration counts) may be inadequate today. Applications that have been running for years often carry a mix of hashes: some users have passwords hashed with an older algorithm, others with a newer one. Migrating all users at once is not possible without knowing their plaintext passwords, which you do not have. User Management solves this with a pluggable hashing system built around `IPasswordHashAlgorithm`. Each stored hash carries an algorithm identifier alongside the hash bytes. When a user logs in successfully, the system checks whether their stored hash was produced by the current preferred algorithm. If not, it transparently re-hashes the plaintext password (which is available at that moment) and stores the updated hash. The user notices nothing; the migration happens silently on next login. This mechanism is also the extension point for bringing in a custom algorithm, for example to verify passwords originally hashed by a legacy system before migration, or to adopt a memory-hard algorithm such as Argon2 in the future. ## IPasswordHashAlgorithm [Section titled “IPasswordHashAlgorithm”](#ipasswordhashalgorithm) `IPasswordHashAlgorithm` is the interface that every hashing algorithm must implement. The built-in PBKDF2-HMAC-SHA-512 implementation uses it, and you can register additional implementations to support legacy hash formats or stronger algorithms. ```csharp public interface IPasswordHashAlgorithm { // A short, stable identifier stored alongside each hash (e.g. "pbkdf2-sha512"). // Must be unique across all registered algorithms. string AlgorithmId { get; } // Hashes a plaintext password and returns the result with all metadata needed to verify it later. HashedPasswordData Hash(string password); // Returns true if the supplied password matches the stored hash. bool Verify(string password, HashedPasswordData data); // Returns true if the stored hash should be upgraded (e.g. iteration count is too low). // Called after a successful Verify(); triggers transparent re-hashing on next login. bool NeedsRehash(HashedPasswordData data); } ``` `AlgorithmId` is stored in the database alongside every hash. It is how the system routes a verification call to the correct algorithm implementation. Choose a short, stable, human-readable string (for example `"pbkdf2-sha512"` or `"argon2id"`). Once a value is in production it must not change, because existing hashes will no longer be routable. ## HashedPasswordData [Section titled “HashedPasswordData”](#hashedpassworddata) `HashedPasswordData` is the data transfer object that travels between the hashing layer and the storage layer. It carries everything needed to verify a password later, including the algorithm identifier, the hash bytes, the salt, and any algorithm-specific parameters (such as iteration count or memory cost): ```csharp public sealed class HashedPasswordData { // The AlgorithmId of the IPasswordHashAlgorithm that produced this hash. public string AlgorithmId { get; } // The raw hash bytes. public IReadOnlyList Hash { get; } // The random salt used during hashing. public IReadOnlyList Salt { get; } // Algorithm-specific parameters, e.g. { "iterations": "210000" }. // Stored alongside the hash so that Verify() and NeedsRehash() can read them. public IReadOnlyDictionary Parameters { get; } } ``` Storing parameters alongside the hash is what makes transparent migration possible. If you increase the PBKDF2 iteration count from 210,000 to 600,000, existing hashes still carry `"iterations": "210000"` in their `Parameters`. `NeedsRehash()` can read that value and return `true`, triggering a re-hash at the new iteration count on the user’s next successful login. ## NeedsRehash(): Transparent Migration [Section titled “NeedsRehash(): Transparent Migration”](#needsrehash-transparent-migration) `NeedsRehash()` is called automatically after every successful `Verify()`. If it returns `true`, User Management re-hashes the plaintext password (which is available at login time) using the current preferred algorithm and stores the result. The user is not interrupted. This covers two migration scenarios: * **Parameter upgrade within the same algorithm**. The algorithm is the same but the parameters have changed (e.g. higher iteration count). `NeedsRehash()` detects the old parameters and returns `true`. * **Algorithm replacement**. A new algorithm is registered as the preferred one. The old algorithm’s `NeedsRehash()` always returns `true`, so every user is migrated on their next login. A typical `NeedsRehash()` implementation reads the stored parameters and compares them to the current target values: ```csharp public bool NeedsRehash(HashedPasswordData data) { // Migrate hashes produced by a different algorithm entirely. if (data.AlgorithmId != AlgorithmId) return true; // Migrate hashes produced with a lower iteration count. if (!data.Parameters.TryGetValue("iterations", out var raw) || !int.TryParse(raw, out var storedIterations)) return true; return storedIterations < CurrentIterations; } ``` ## Built-in Algorithm: Pbkdf2Sha512PasswordHashAlgorithm [Section titled “Built-in Algorithm: Pbkdf2Sha512PasswordHashAlgorithm”](#built-in-algorithm-pbkdf2sha512passwordhashalgorithm) The default implementation is `Pbkdf2Sha512PasswordHashAlgorithm`. It uses PBKDF2-HMAC-SHA-512 at 210,000 iterations with a 256-bit random salt, following the [OWASP Password Storage Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#pbkdf2) recommendation. Its `AlgorithmId` is `"pbkdf2-sha512"`. The iteration count is stored in `HashedPasswordData.Parameters` under the key `"iterations"`. If you increase the iteration count in a future release, `NeedsRehash()` will detect hashes produced at the lower count and transparently upgrade them on next login. ## Implementing a Custom Algorithm [Section titled “Implementing a Custom Algorithm”](#implementing-a-custom-algorithm) To add a custom algorithm (for example, to verify passwords originally stored by a legacy system), implement `IPasswordHashAlgorithm` and register it with the service provider. The following example wraps a hypothetical legacy MD5-based hash for read-only verification. It always returns `true` from `NeedsRehash()` so that every user who logs in is immediately migrated to the current preferred algorithm: ```csharp public class LegacyMd5PasswordHashAlgorithm : IPasswordHashAlgorithm { public string AlgorithmId => "legacy-md5"; public HashedPasswordData Hash(string password) { // Legacy algorithm is read-only: new hashes should never be produced here. // The preferred algorithm handles all new hashes. throw new NotSupportedException( "The legacy-md5 algorithm is read-only. Register a preferred algorithm for new hashes."); } public bool Verify(string password, HashedPasswordData data) { // Reproduce the legacy hash and compare. var inputHash = ComputeLegacyMd5(password, data.Salt.ToArray()); return CryptographicOperations.FixedTimeEquals( inputHash, data.Hash.ToArray()); } public bool NeedsRehash(HashedPasswordData data) { // Always migrate away from this algorithm. return true; } private static byte[] ComputeLegacyMd5(string password, byte[] salt) { // ... legacy hash logic ... throw new NotImplementedException(); } } ``` Register the custom algorithm alongside the built-in one. The first registration is treated as the preferred algorithm for new hashes; additional registrations are used only for verification and migration: ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => { auth.Configure(options => { // Preferred algorithm for all new hashes. options.Passwords.PasswordHashAlgorithm = new Pbkdf2Sha512PasswordHashAlgorithm(); }); }) ); // Register the legacy algorithm so existing hashes can still be verified. builder.Services.AddSingleton(); ``` Once all users have logged in at least once, their hashes will have been migrated to the preferred algorithm. At that point the legacy registration can be removed. ----- # SMTP OTP Dispatcher Reference > Reference for the SmtpOtpDispatcher, including configuration options, template placeholders, and security best practices for email-based one-time password delivery. The `SmtpOtpDispatcher` delivers one-time passwords (OTPs) via email using SMTP. It includes a built-in default template with security warnings and supports fully customizable plain text, HTML, and subject templates. ## Registration [Section titled “Registration”](#registration) Register the SMTP One-Time Password (OTP) dispatcher using `UseSmtpOtpDispatcher` on the authentication builder: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => auth.UseSmtpOtpDispatcher(options => { options.Host = "smtp.example.com"; options.Port = 587; options.EnableSsl = true; options.FromEmail = "noreply@example.com"; options.FromName = "MyApp"; })) ); ``` ## `SmtpOtpDispatcherOptions` [Section titled “SmtpOtpDispatcherOptions”](#smtpotpdispatcheroptions) All properties on `SmtpOtpDispatcherOptions` are configured via the `Action` delegate passed to `UseSmtpOtpDispatcher`. | Property | Type | Required | Default | Description | | ------------------- | --------- | -------- | ------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `Host` | `string` | Yes | N/A | SMTP server hostname or IP address. | | `Port` | `int` | No | `1025` | SMTP server port. Typically `587` for STARTTLS or `465` for implicit TLS. | | `EnableSsl` | `bool` | No | `true` | Whether to use SSL/TLS for the SMTP connection. Always set to `true` in production. | | `FromEmail` | `string` | Yes | N/A | The sender email address used in the `From` header. | | `FromName` | `string` | Yes | N/A | The sender display name used in the `From` header and in email templates. | | `Domain` | `string?` | No | `null` | The domain or URL where the user should enter the code (e.g. `"https://app.example.com"`). When set, the default template includes a domain-specific security warning. When `null`, templates receive `"our official website"` for the `{Domain}` placeholder. | | `PlainTextTemplate` | `string?` | No | `null` | Custom plain text body template. Supports [template placeholders](#template-placeholders). When `null`, the built-in default template is used. | | `HtmlTemplate` | `string?` | No | `null` | Custom HTML body template. Supports [template placeholders](#template-placeholders). When set, the email is sent as HTML. Takes precedence over `PlainTextTemplate`. | | `SubjectTemplate` | `string?` | No | `null` | Custom subject line template. Supports `{FromName}` and `{Code}` placeholders. When `null`, defaults to `"{FromName} confirmation code"`. | ## Default Email Format [Section titled “Default Email Format”](#default-email-format) When no custom templates are configured, the sender uses a built-in plain text template with security warnings. **Subject:** ```text MyApp confirmation code ``` **Body:** ```text 123-456 is your MyApp confirmation code (expires after 5 minute(s)) IMPORTANT SECURITY INFORMATION: - You should only use this code if you requested it - If you did not request this code, please ignore this email - Only enter this code on https://app.example.com - Do not share this code with anyone - MyApp will never ask you for this code ``` The domain line is only included when `Domain` is set. Without it, the line reads `Only enter this code on our official website`. ## Template Placeholders [Section titled “Template Placeholders”](#template-placeholders) All three template properties (`PlainTextTemplate`, `HtmlTemplate`, `SubjectTemplate`) support the following placeholders: | Placeholder | Description | Example Value | | ------------------ | ---------------------------------------------------------------------------------------------------- | ----------------- | | `{Code}` | The OTP code, formatted with hyphens between groups. | `123-456` | | `{FromName}` | The configured sender name (`SmtpOtpDispatcherOptions.FromName`). | `MyApp` | | `{ExpiresMinutes}` | The number of minutes until the code expires, as a whole number. | `5` | | `{Domain}` | The configured domain (`SmtpOtpDispatcherOptions.Domain`), or `"our official website"` when not set. | `app.example.com` | Note: `SubjectTemplate` only supports `{FromName}` and `{Code}`. ## Custom Templates [Section titled “Custom Templates”](#custom-templates) ### Plain Text Template [Section titled “Plain Text Template”](#plain-text-template) ```csharp using Duende.IdentityServer; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => auth.UseSmtpOtpDispatcher(options => { options.Host = "smtp.example.com"; options.Port = 587; options.EnableSsl = true; options.FromEmail = "noreply@example.com"; options.FromName = "MyApp"; options.Domain = "app.example.com"; options.PlainTextTemplate = @" Hello, Your verification code is: {Code} This code will expire in {ExpiresMinutes} minutes. SECURITY NOTICE: - If you did not request this code, please ignore this email - Only enter this code on {Domain} - Never share this code with anyone, including {FromName} staff - We will never ask you to provide this code over phone or email Thank you, The {FromName} Team "; })) ); ``` ### HTML Template [Section titled “HTML Template”](#html-template) Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => auth.UseSmtpOtpDispatcher(options => { options.Host = "smtp.example.com"; options.Port = 587; options.EnableSsl = true; options.FromEmail = "noreply@example.com"; options.FromName = "MyApp"; options.Domain = "app.example.com"; options.HtmlTemplate = @"

{FromName}

{Code}

This code expires in {ExpiresMinutes} minutes.

Security: Only enter this code on {Domain}. Never share it with anyone.
"; })) ); ``` ### Custom Subject [Section titled “Custom Subject”](#custom-subject) ```csharp options.SubjectTemplate = "[{FromName}] Your verification code: {Code}"; ``` ## Binding From Configuration [Section titled “Binding From Configuration”](#binding-from-configuration) SMTP connection settings can be bound from `appsettings.json`: appsettings.json ```json { "Smtp": { "Host": "smtp.sendgrid.net", "Port": 587, "FromEmail": "noreply@mycompany.com" } } ``` Your startup code can then bind to this section: Program.cs ```csharp using Duende.IdentityServer; using Duende.UserManagement; builder.Services .AddIdentityServer() .AddUserManagement(um => um .Authentication(auth => auth.UseSmtpOtpDispatcher(options => { builder.Configuration.GetSection("Smtp").Bind(options); options.EnableSsl = true; options.FromName = "MyCompany"; options.Domain = "https://app.mycompany.com"; })) ); ``` ## Security Best Practices [Section titled “Security Best Practices”](#security-best-practices) * **Always include security warnings**: Whether using the default or a custom template, tell users to only enter the code if they requested it, where to enter it, and never to share it. * **Set the `Domain` property**: Telling users the exact URL where the code should be entered reduces phishing risk. * **Enable SSL/TLS**: Always set `EnableSsl = true` and use port `587` (STARTTLS) or `465` (implicit TLS) in production. * **Use clear expiration times**: Include `{ExpiresMinutes}` in your template so users know how long the code is valid. * **Brand your emails consistently**: Use your organization name via `FromName` throughout the template to build user trust. ----- # The Big Picture > An overview of modern application architecture patterns and how OpenID Connect and OAuth 2.0 protocols implemented by IdentityServer solve authentication and API access challenges Most modern applications look more or less like this: ``` --- title: Modern Application Architecture --- flowchart LR Browser@{ icon: "material-symbols:tabs-rounded", label: "Browser", shape: icon } NativeApp@{ icon: "material-symbols:phone-android-rounded", label: "Native App", shape: icon } ServerApp@{ icon: "material-symbols:computer-rounded", label: "Server / App", shape: icon } Backend@{ icon: "material-symbols:storage-rounded", label: "Backend", shape: icon } API_main@{ icon: "material-symbols:api-rounded", label: "API", shape: icon } API_top@{ icon: "material-symbols:api-rounded", label: "API", shape: icon } API_tail@{ icon: "material-symbols:api-rounded", label: "API", shape: icon } Browser --> Backend Browser --> API_main Backend --> API_top Backend --> API_main NativeApp --> API_main ServerApp --> API_main API_main --> API_tail ``` The most common interactions are: * Browsers communicate with web applications * Web applications communicate with web APIs (sometimes on their own, sometimes on behalf of a user) * Browser-based applications communicate with web APIs * Native applications communicate with web APIs * Server-based applications communicate with web APIs * Web APIs communicate with web APIs (sometimes on their own, sometimes on behalf of a user) Typically, each and every layer (front-end, middle-tier and back-end) has to protect resources and implement authentication and/or authorization – often against the same user store. Outsourcing these fundamental security functions to a security token service prevents duplicating that functionality across those applications and endpoints. Restructuring the application to support a security token service leads to the following architecture and protocols: ``` --- title: Protocols Used Within Architecture --- flowchart LR Browser@{ icon: "material-symbols:tabs-rounded", label: "Browser (OIDC)", shape: icon } NativeApp@{ icon: "material-symbols:phone-android-rounded", label: "Native App (OIDC)", shape: icon } ServerApp@{ icon: "material-symbols:computer-rounded", label: "Server / App", shape: icon } Backend@{ icon: "material-symbols:storage-rounded", label: "Backend", shape: icon } API_main@{ icon: "material-symbols:api-rounded", label: "API", shape: icon } API_top@{ icon: "material-symbols:api-rounded", label: "API", shape: icon } API_tail@{ icon: "material-symbols:api-rounded", label: "API", shape: icon } Browser -- OIDC --> Backend Browser -- OAUTH --> API_main Backend -- OAUTH --> API_top Backend -- OAUTH --> API_main NativeApp -- OAUTH --> API_main ServerApp -- OAUTH --> API_main API_main -- OAUTH --> API_tail ``` Such a design divides security concerns into two parts: ## Authentication [Section titled “Authentication”](#authentication) Authentication is needed when an application needs to know the identity of the current user. Typically, these applications manage data on behalf of that user and need to make sure that this user can only access the data for which they are allowed. The most common example for that is (classic) web applications – but native and JS-based applications also have a need for authentication. The most common authentication protocols are SAML2p, WS-Federation and OpenID Connect – SAML2p being the most popular and the most widely deployed. OpenID Connect is the newest of the three, but is considered to be the future because it has the most potential for modern applications. It was built for mobile application scenarios right from the start and is designed to be API friendly. ## API Access [Section titled “API Access”](#api-access) Applications have two fundamental ways with which they communicate with APIs – using the application identity, or delegating the user’s identity. Sometimes both methods need to be combined. OAuth 2.0 is a protocol that allows applications to request access tokens from a security token service and use them to communicate with APIs. This delegation reduces complexity in both the client applications and the APIs since authentication and authorization can be centralized. ## OpenID Connect And OAuth 2.0 – Better Together! [Section titled “OpenID Connect And OAuth 2.0 – Better Together!”](#openid-connect-and-oauth-20--better-together) OpenID Connect and OAuth 2.0 are very similar – in fact OpenID Connect is an extension on top of OAuth 2.0. The two fundamental security concerns, authentication and API access, are combined into a single protocol - often with a single round trip to the security token service. We believe that the combination of OpenID Connect and OAuth 2.0 is the best approach to secure modern applications for the foreseeable future. Duende IdentityServer is an implementation of these two protocols and is highly optimized to solve the typical security problems of today’s mobile, native and web applications. ## How Duende IdentityServer Can Help [Section titled “How Duende IdentityServer Can Help”](#how-duende-identityserver-can-help) Duende IdentityServer is middleware that adds spec-compliant OpenID Connect and OAuth 2.0 endpoints to an arbitrary ASP.NET Core host. Typically, you build (or re-use) an application that contains login and logout pages (and optionally a consent page, depending on your needs) and add the IdentityServer middleware to that application. The middleware adds the necessary protocol heads to the application so that clients can talk to it using those standard protocols. ``` --- title: ASP.NET Core Middleware Configuration --- flowchart LR login@{ icon: "material-symbols:login-rounded", label: "login", shape: icon } logout@{ icon: "material-symbols:logout-rounded", label: "logout", shape: icon } more@{ icon: "material-symbols:pending", label: "more...", shape: icon } authorize@{ icon: "material-symbols:verified-user-rounded", label: "authorize", shape: icon } token@{ icon: "material-symbols:key-rounded", label: "token", shape: icon } discovery@{ icon: "material-symbols:travel-explore-rounded", label: "discovery", shape: icon } subgraph ASPNET["ASP.NET Core Request Pipeline"] direction TB subgraph IS[" "] is_space@{ icon: "material-symbols:assured-workload-rounded", label: "IdentityServer Middleware", shape: icon } end subgraph YC[" "] yc_space@{ icon: "material-symbols:code-rounded", label: "Your Code", shape: icon } end end login --> YC logout --> YC more --> YC authorize --> IS token --> IS discovery --> IS style YC stroke:#74acfb,stroke-width:2px style IS stroke:#61fb92,stroke-width:2px ``` The hosting application can be as complex as you want, but we typically recommend to keep the attack surface as small as possible by including authentication/federation related UI only. ----- # Packaging and Builds > A guide to Duende IdentityServer packages, templates, UI components, and source code accessibility ## Product [Section titled “Product”](#product) The licensed and supported libraries can be accessed via NuGet: [Duende IdentityServer](https://www.nuget.org/packages/Duende.IdentityServer)Core IdentityServer package. [Entity Framework Integration](https://www.nuget.org/packages/Duende.IdentityServer.EntityFramework)EF Core persistence for configuration and operational data. [ASP.NET Identity Integration](https://www.nuget.org/packages/Duende.IdentityServer.AspNetIdentity)User management via ASP.NET Core Identity. ## Templates [Section titled “Templates”](#templates) Contains Duende templates for the `dotnet` CLI to help jump-start your Duende-powered solutions. You can install the templates using the following command: Terminal ```bash dotnet new install Duende.Templates ``` [Templates](https://www.nuget.org/packages/Duende.Templates)NuGet Package for IdentityServer Templates [Source Code](https://github.com/DuendeSoftware/products/tree/main/identity-server/templates/src/)Source code for IdentityServer Templates Running the command `dotnet new list duende` should give you a list of the following templates ```bash Template Name Short Name Language Tags ---------------------------------------------------------- -------------------- -------- ------------------------- Duende BFF Host using a Remote API duende-bff-remoteapi [C#] Web/Duende/BFF Duende BFF using a Local API duende-bff-localapi [C#] Web/Duende/BFF Duende BFF with Blazor autorender duende-bff-blazor [C#] Web/Duende/BFF Duende IdentityServer Empty duende-is-empty [C#] Web/Duende/IdentityServer Duende IdentityServer Quickstart UI (UI assets only) duende-is-ui [C#] Web/IdentityServer Duende IdentityServer with ASP.NET Core Identity duende-is-aspid [C#] Web/Duende/IdentityServer Duende IdentityServer with Entity Framework Stores duende-is-ef [C#] Web/Duende/IdentityServer Duende IdentityServer with In-Memory Stores and Test Users duende-is-inmem [C#] Web/Duende/IdentityServer Duende IdentityServer duende-is [C#] Web/Duende/IdentityServer ``` Note You may have a previous version of Duende templates (`Duende.Templates`) installed on your machine. To uninstall the previous template package, and install the latest version, use the following command: Terminal ```bash dotnet new uninstall Duende.Templates dotnet new install Duende.Templates ``` ## Template Descriptions [Section titled “Template Descriptions”](#template-descriptions) In this section, we’ll discuss what each IdentityServer template offers and why you would choose to start with it. While there are similarities across templates, there are nuances that can make for better starting points depending on your particular use case. We’ll start with the simplest templates and then move to the most feature-rich ones. Many of these templates build on each other’s work, so moving from one to another is straightforward. Note All templates currently target .NET 8.0, but you can alter the target framework after creating the project to target higher framework versions. All templates are provided as a starting point for your customization. Using the templates, you assume development responsibility for the choices, alterations, and inevitable deployment of your IdentityServer instance. ### Duende IdentityServer Empty [Section titled “Duende IdentityServer Empty”](#duende-identityserver-empty) You want to run the following command to start using the **Duende IdentityServer Empty** template. ```bash dotnet new duende-is-empty ``` Once created, this template has three essential files: `Config`, `HostingExtensions`, and `Program`. You can modify the `Config` file to add clients, scopes, and claims, as all configurations are from in-memory objects. ```csharp public static class Config { public static IEnumerable IdentityResources => new IdentityResource[] { new IdentityResources.OpenId() }; public static IEnumerable ApiScopes => new ApiScope[] { }; public static IEnumerable Clients => new Client[] { }; } ``` This template doesn’t include user interface elements, so it doesn’t support OpenID Connect unless you add those UI elements. You can do so by running the UI-only template of `duende-is-ui`. ```bash dotnet new duende-is-ui --project ``` The executed command will add Razor Pages to your web project. You will need to add Razor Pages to your `HostingExtensions` file. ```csharp using Serilog; internal static class HostingExtensions { public static WebApplication ConfigureServices(this WebApplicationBuilder builder) { builder.Services.AddRazorPages(); builder.Services.AddIdentityServer() .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients) .AddLicenseSummary(); return builder.Build(); } public static WebApplication ConfigurePipeline(this WebApplication app) { app.UseSerilogRequestLogging(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseStaticFiles(); app.UseRouting(); app.UseIdentityServer(); app.UseAuthorization(); app.MapRazorPages().RequireAuthorization(); return app; } } ``` ### Duende IdentityServer with In-Memory Stores and Test Users [Section titled “Duende IdentityServer with In-Memory Stores and Test Users”](#duende-identityserver-with-in-memory-stores-and-test-users) The `duende-is-inmem` template is similar to the `duende-is-empty` and `duende-is-ui` templates combined into a single project template. ```bash dotnet new duende-is-inmem ``` This template differs from others in that we have defined some starting clients, scopes, and claims for common development scenarios and a speedier development experience. Config.cs ```csharp public static class Config { public static IEnumerable IdentityResources => new IdentityResource[] { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; public static IEnumerable ApiScopes => new ApiScope[] { new ApiScope("scope1"), new ApiScope("scope2"), }; public static IEnumerable Clients => new Client[] { // m2m client credentials flow client new Client { ClientId = "m2m.client", ClientName = "Client Credentials Client", AllowedGrantTypes = GrantTypes.ClientCredentials, ClientSecrets = { new Secret("511536EF-F270-4058-80CA-1C89C192F69A".Sha256()) }, AllowedScopes = { "scope1" } }, // interactive client using code flow + pkce new Client { ClientId = "interactive", ClientSecrets = { new Secret("49C1A7E1-0C79-4A89-A3D6-A37998FB86B0".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, RedirectUris = { "https://localhost:44300/signin-oidc" }, FrontChannelLogoutUri = "https://localhost:44300/signout-oidc", PostLogoutRedirectUris = { "https://localhost:44300/signout-callback-oidc" }, AllowOfflineAccess = true, AllowedScopes = { "openid", "profile", "scope2" } }, }; } ``` This template is a great starting point for proof of concepts and a learning tool for developers experiencing OAuth 2.0 and OpenID Connect in the .NET space for the first time. ### Duende IdentityServer with Entity Framework Stores [Section titled “Duende IdentityServer with Entity Framework Stores”](#duende-identityserver-with-entity-framework-stores) For developers looking to quickly go to a production-like environment, starting with the `duende-is-ef` template is a great starting point. ```bash dotnet new duende-is-ef ``` This template stores all operational and configuration data of the IdentityServer instance in your chosen data storage, utilizing EF Core’s ability to target multiple database engines. The template targets SQLite by default, but we have included scripts to easily swap out and regenerate migrations for your database. [Read more about the Entity Framework Core setup here.](/identityserver/data/providers/entityframework-core/) ### Duende IdentityServer [Section titled “Duende IdentityServer”](#duende-identityserver) The Duende IdentityServer template is our most feature-rich offering and a great starting point for developers who want a simple yet effective UI/UX experience. ```bash dotnet new duende-is ``` The template is built on the Entity Framework Core template but provides an administrative UI for managing clients, scopes, and claims against a database storage engine. It also has a diagnostics dashboard showing system information, including the licensing tier and features currently used in your IdentityServer deployment. #### Third-party Dependencies [Section titled “Third-party Dependencies”](#third-party-dependencies) This template includes several third-party dependencies: * [Serilog](https://serilog.net/) * [Bootstrap 5](https://getbootstrap.com) * [Bootstrap 5 tags](https://github.com/lekoala/bootstrap5-tags) * [JQuery](https://jquery.org) * [Entity Framework Core](https://learn.microsoft.com/en-us/ef/core/) ### Duende IdentityServer with ASP.NET Core Identity [Section titled “Duende IdentityServer with ASP.NET Core Identity”](#duende-identityserver-with-aspnet-core-identity) The **Duende IdentityServer with ASP.NET Core Identity** template integrates with ASP.NET Identity to provide you with an instance of Duende IdentityServer that has a user store powered by the Microsoft library. [Please read our ASP.NET Identity documentation](/identityserver/identity/aspnet-identity/), to learn more about this integration. ### BFF Templates [Section titled “BFF Templates”](#bff-templates) For Duende BFF template description, refer the [Duende BFF project templates](/bff/getting-started/templates/). ----- # More Reading Resources > Collection of learning resources including demo server access, OAuth fundamentals, and ASP.NET security guides ## Demo Server [Section titled “Demo Server”](#demo-server) You can try Duende IdentityServer with your favourite client library. We have a test instance at [demo.duendesoftware.com](https://demo.duendesoftware.com). On the main page you can find instructions on how to configure your client and how to call an API. [IdentityServer Demo Server](https://demo.duendesoftware.com)Visit https\://demo.duendesoftware.com ## OAuth and OIDC Fundamentals [Section titled “OAuth and OIDC Fundamentals”](#oauth-and-oidc-fundamentals) [![OAuth the Good Parts](/_astro/oauth-good-parts.BC0cedTz.jpg)](https://www.youtube.com/watch?v=Ps8ep-glDfc) [OAuth the Good Parts](https://www.youtube.com/watch?v=Ps8ep-glDfc) July 2022 — NDC Porto [![Securing SPAs and Blazor with BFF](/_astro/securing-spas-bff.To7zhxGn.jpg)](https://www.youtube.com/watch?v=xzRhabmlc8M) [Securing SPAs and Blazor with BFF](https://www.youtube.com/watch?v=xzRhabmlc8M) July 2022 — NDC Porto [![Automated OAuth Token Management](/_astro/automated-token-mgmt.40MUWJ7f.jpg)](https://www.youtube.com/watch?v=zr-LAYg5BCE) [Automated OAuth Token Management](https://www.youtube.com/watch?v=zr-LAYg5BCE) November 2022 — .NET Workers and ASP.NET Web Apps ## ASP.NET Security [Section titled “ASP.NET Security”](#aspnet-security) [![ASP.NET Core Authentication & Authorization](/_astro/aspnet-auth-intro.BzR77ch-.jpg)](https://www.youtube.com/watch?v=02Yh3sxzAYI) [ASP.NET Core Authentication & Authorization](https://www.youtube.com/watch?v=02Yh3sxzAYI) October 2022 — Introduction [![External Authentication Providers Part 1](/_astro/external-providers-1.CagwBXAv.jpg)](https://www.youtube.com/watch?v=HH_tw7dFhpg) [External Authentication Providers (Part 1)](https://www.youtube.com/watch?v=HH_tw7dFhpg) November 2022 [![External Authentication Providers Part 2](/_astro/external-providers-2.DG20Gc9I.jpg)](https://www.youtube.com/watch?v=daeVaU5CmPw) [External Authentication Providers (Part 2)](https://www.youtube.com/watch?v=daeVaU5CmPw) November 2022 [ASP.NET Cookie Authentication](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/cookie)Microsoft documentation. ## End-User Authorization [Section titled “End-User Authorization”](#end-user-authorization) [![Authorization for Modern Applications](/_astro/authz-modern-apps.BUeXc-34.jpg)](https://www.youtube.com/watch?v=Dlrf85NTuAU) [Authorization for Modern Applications](https://www.youtube.com/watch?v=Dlrf85NTuAU) October 2018 — DevConf ----- # Supported Specifications > A comprehensive list of supported OpenID Connect, OAuth 2.x and SAML specifications implemented in Duende IdentityServer Duende IdentityServer implements the following specifications: ## OpenID Connect [Section titled “OpenID Connect”](#openid-connect) * OpenID Connect Core 1.0 ([spec](https://openid.net/specs/openid-connect-core-1_0.html)) * OpenID Connect Discovery 1.0 ([spec](https://openid.net/specs/openid-connect-discovery-1_0.html)) * OpenID Connect RP-Initiated Logout 1.0 ([spec](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)) * OpenID Connect Session Management 1.0 ([spec](https://openid.net/specs/openid-connect-session-1_0.html)) * OpenID Connect Front-Channel Logout 1.0 ([spec](https://openid.net/specs/openid-connect-frontchannel-1_0.html)) * OpenID Connect Back-Channel Logout 1.0 ([spec](https://openid.net/specs/openid-connect-backchannel-1_0.html)) * Multiple Response Types ([spec](https://openid.net/specs/oauth-v2-multiple-response-types-1_0.html)) * Form Post Response Mode ([spec](https://openid.net/specs/oauth-v2-form-post-response-mode-1_0.html)) * Enterprise Edition: OpenID Connect Client-Initiated Backchannel Authentication (CIBA) ([spec](https://openid.net/specs/openid-client-initiated-backchannel-authentication-core-1_0.html)) * FAPI 2.0 Security Profile ([spec](https://openid.net/specs/fapi-security-profile-2_0-final.html)) ## OAuth 2.x [Section titled “OAuth 2.x”](#oauth-2x) * OAuth 2.0 ([RFC 6749](https://tools.ietf.org/html/rfc6749)) * OAuth 2.0 Bearer Token Usage ([RFC 6750](https://tools.ietf.org/html/rfc6750)) * JSON Web Token ([RFC 7519](https://tools.ietf.org/html/rfc7519)) * OAuth 2.0 Token Revocation ([RFC 7009](https://tools.ietf.org/html/rfc7009)) * OAuth 2.0 Token Introspection ([RFC 7662](https://tools.ietf.org/html/rfc7662)) * Proof Key for Code Exchange by OAuth Public Clients ([RFC 7636](https://tools.ietf.org/html/rfc7636)) * OAuth 2.0 JSON Web Tokens for Client Authentication ([RFC 7523](https://tools.ietf.org/html/rfc7523)) * OAuth 2.0 Device Authorization Grant ([RFC 8628](https://tools.ietf.org/html/rfc8628)) * Proof-of-Possession Key Semantics for JSON Web Tokens ([RFC 7800](https://tools.ietf.org/html/rfc7800)) * OAuth 2.0 Mutual TLS Client Authentication and Certificate-Bound Access Tokens ([RFC 8705](https://tools.ietf.org/html/rfc8705)) * OAuth 2.0 Token Exchange ([RFC 8693](https://tools.ietf.org/html/rfc8693)) * JWT Secured Authorization Request / JAR ([RFC 9101](https://datatracker.ietf.org/doc/html/rfc9101)) * JWT Profile for OAuth 2.0 Access Tokens ([RFC 9068](https://datatracker.ietf.org/doc/html/rfc9068)) * OAuth 2.0 Authorization Server Issuer Identifier in Authorization Response ([RFC 9207](https://datatracker.ietf.org/doc/html/rfc9207)) * OAuth 2.0 Step-up Authentication Challenge Protocol ([RFC 9470](https://datatracker.ietf.org/doc/html/rfc9470)) * Business (legacy), Enterprise (legacy), Standard, Advanced, and Custom Edition: OAuth 2.0 Dynamic Client Registration Protocol ([RFC 7591](https://www.rfc-editor.org/rfc/rfc7591)) * Business (legacy), Enterprise (legacy), Standard, Advanced, and Custom Edition: OAuth 2.0 Pushed Authorization Requests ([RFC 9126](https://www.rfc-editor.org/rfc/rfc9126)) * Enterprise (legacy), Standard, Advanced, and Custom Edition: Resource Indicators for OAuth 2.0 ([RFC 8707](https://tools.ietf.org/html/rfc8707)) * Enterprise (legacy), Standard, Advanced, and Custom Edition: OAuth 2.0 Demonstrating Proof-of-Possession at the Application Layer / DPoP ([RFC 9449](https://datatracker.ietf.org/doc/html/rfc9449)) * JSON Web Token (JWT) Response for OAuth Token Introspection ([RFC 9701](https://www.rfc-editor.org/rfc/rfc9701.html)) * OAuth 2.0 Authorization Server Metadata ([RFC 8414](https://datatracker.ietf.org/doc/html/rfc8414)) ## SAML [Section titled “SAML”](#saml) * Security Assertion Markup Language (SAML) v2.0 () * SAML Core 2.0 (Assertions, Protocols, Bindings) * SAML Profiles 2.0 (Web Browser SSO, Single Logout) * SAML Bindings 2.0 (HTTP-Redirect, HTTP-POST) * SAML Metadata 2.0 (EntityDescriptor, IDPSSODescriptor) ----- # Terminology > Learn about the key terms and concepts used in IdentityServer, including clients, resources, tokens, and user authentication flows. The specs, documentation and object model use a certain terminology that you should be aware of. ``` --- title: Key Roles & Relationships --- flowchart LR Users@{ icon: "material-symbols:person-rounded", label: "Users", shape: icon } IS@{ icon: "material-symbols:assured-workload-rounded", label: "Duende IdentityServer", shape: icon } Clients@{ icon: "material-symbols:computer-rounded", label: "Clients", shape: icon } Resources@{ icon: "material-symbols:api-rounded", label: "Resources", shape: icon } Users --> Clients Clients <-->|"authenticate users & request resource access"| IS Clients --> Resources ``` ## Duende IdentityServer [Section titled “Duende IdentityServer”](#duende-identityserver) Duende IdentityServer is an OpenID Connect & OAuth engine - it implements the OpenID Connect and OAuth 2.0 family of [protocols](/identityserver/overview/specs/). Different literature uses different terms for the same role - you probably also find the terms security token service, identity provider, authorization server, IP-STS and more. But they are in a nutshell all the same: a piece of software that issues security tokens to clients. A typical implementation of Duende IdentityServer has a number of jobs and features - including: * manage access to resources * authenticate users using a local account store or via an external identity provider * provide session management and single sign-on * manage and authenticate clients * issue identity and access tokens to clients ## User [Section titled “User”](#user) A user is a human that is using a registered client to access resources. ## Client [Section titled “Client”](#client) A [client](/identityserver/fundamentals/clients/) is a piece of software that requests tokens from your IdentityServer - either for authenticating a user (requesting an identity token) or for accessing a resource (requesting an access token). A client must be first registered with your IdentityServer before it can request tokens. While there are many different client types, e.g. web applications, native mobile or desktop applications, SPAs, server processes etc., they can all be put into two high-level categories. ### Machine to Machine Communication [Section titled “Machine to Machine Communication”](#machine-to-machine-communication) In this scenario two machines talk to each other (e.g. background processes, batch jobs, server daemons), and there is no interactive user present. To authorize this communication, your IdentityServer issues a token to the caller. In protocol terms, this scenario is called *Client Credentials Flow* and you can learn more about it in the issuing tokens [section](/identityserver/tokens/requesting/#machine-to-machine-communication) and in our [Quickstart](/identityserver/quickstarts/1-client-credentials/). ### Interactive Applications [Section titled “Interactive Applications”](#interactive-applications) This is the most common type of client scenario: web applications, SPAs or native/mobile apps with interactive users. This scenario typically involves a browser for user interaction (e.g. for authentication or consent). In protocol terms, this scenario is called *Authorization Code Flow* and you can learn more about it in the issuing tokens [section](/identityserver/tokens/requesting/#interactive-applications) and in our [Quickstart](/identityserver/quickstarts/2-interactive/). Note A client application can potentially have many instances - e.g. your web application might be physically deployed on multiple servers for load-balancing purposes, or your mobile application might be deployed to thousands of different phones. Logically these instances are still a single client. ## Resources [Section titled “Resources”](#resources) [Resources](/identityserver/fundamentals/resources) are something you want to protect with your IdentityServer - either identity data of your users, or APIs. Every resource has a unique name - and clients use this name to specify to which resources they want to get access to. **Identity data** Identity information (aka claims) about a user, e.g. name or email address. *`APIs`* APIs resources represent functionality a client wants to invoke - typically modelled as Web APIs, but not necessarily. ## Identity Token [Section titled “Identity Token”](#identity-token) An identity token represents the outcome of an authentication process. It contains at a bare minimum an identifier for the user (called the `sub` aka subject claim) and information about how and when the user authenticated. It can contain additional identity data. ## Access Token [Section titled “Access Token”](#access-token) An access token allows access to an API resource. Clients request access tokens and forward them to the API. Access tokens contain information about the client and the user (if present). APIs use that information to authorize access to their data and functionality. ----- # IdentityServer Quickstarts > Step-by-step tutorials for implementing common Duende IdentityServer scenarios, from basic setup to advanced features. The quickstarts provide step-by-step instructions for various common Duende IdentityServer scenarios. They start with the absolute basics and become more complex - it is recommended you do them in order. * adding Duende IdentityServer to an ASP.NET Core application * configuring Duende IdentityServer * issuing tokens for various clients * securing web applications and APIs * adding support for EntityFramework based configuration * adding support for ASP.NET Identity Every quickstart has a reference solution. You can find the code in the [samples](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts) folder. ## Preparation [Section titled “Preparation”](#preparation) The first thing you should do is install our templates: Terminal ```bash dotnet new install Duende.Templates ``` They will be used as a starting point for the various tutorials. Note You may have a previous version of Duende templates (`Duende.Templates`) installed on your machine. To uninstall the previous template package, and install the latest version, use the following command: Terminal ```bash dotnet new uninstall Duende.Templates dotnet new install Duende.Templates ``` [YouTube video player](https://www.youtube.com/embed/cxYmODQHErM) ----- # Protecting An API With Client Credentials > Learn how to set up IdentityServer to protect an API using client credentials, implementing server-to-server authentication with access tokens. Welcome to the first quickstart for IdentityServer! To see the full list of quickstarts, please see [Quickstarts Overview](/identityserver/quickstarts/0-overview/). This first quickstart provides step-by-step instructions to set up IdentityServer in the most basic scenario: protecting APIs for server-to-server communication. You will create a solution containing three projects: * An Identity Server * An API that requires authentication * A client that accesses that API The client will request an access token from IdentityServer using its client ID and secret and then use the token to gain access to the API. ## Source Code [Section titled “Source Code”](#source-code) Finished source code for each quickstart in this series is available in the [Samples](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts) repository, and a reference implementation of this quickstart is available [here](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/1_ClientCredentials). ## Video [Section titled “Video”](#video) In addition to the written steps below there’s also a YouTube video available: [YouTube video player](https://www.youtube.com/embed/EhuCpbH7Ad0) ## Preparation [Section titled “Preparation”](#preparation) The IdentityServer templates for the dotnet CLI are a good starting point for the quickstarts. To install the templates open a console window and type the following command: ```console dotnet new install Duende.Templates ``` Note You may have a previous version of Duende templates (`Duende.Templates`) installed on your machine. To uninstall the previous template package, and install the latest version, use the following command: Terminal ```bash dotnet new uninstall Duende.Templates dotnet new install Duende.Templates ``` ## Create The Solution And IdentityServer Project [Section titled “Create The Solution And IdentityServer Project”](#create-the-solution-and-identityserver-project) In this section, you will create a directory for the solution and use the `isempty` (IdentityServer Empty) template to create an ASP.NET Core application that includes a basic IdentityServer setup. Back in the console, run the following commands to create the directory structure for the solution. ```console mkdir quickstart cd quickstart mkdir src dotnet new sln -n Quickstart ``` This will create a quickstart directory that will serve as the root of the solution, a src subdirectory to hold your source code, and a solution file to organize your projects. Throughout the rest of the quickstart series, paths will be written relative to the quickstart directory. From the new quickstart directory, run the following commands to use the `isempty` template to create a new project. The template creates a web project named IdentityServer with the IdentityServer package installed and minimal configuration added for it. ```console cd src dotnet new duende-is-empty -n IdentityServer ``` This will create the following files within a new `src/IdentityServer` directory: * `Properties/launchSettings.json` file - launch profile * `appsettings.json` - run time settings * `Config.cs` - definitions for [resources](/identityserver/overview/terminology/#resources) and [clients](/identityserver/overview/terminology/#client) used by IdentityServer * `HostingExtensions.cs` - configuration for ASP.NET pipeline and services Notably, the IdentityServer services are configured here and the IdentityServer middleware is added to the pipeline here. * `IdentityServer.csproj` - project file with the IdentityServer NuGet package added * `Program.cs` - main application entry point Note The `src/IdentityServer/Properties/launchSettings.json` file created by the `isempty` template sets the `applicationUrl` to `https://localhost:5001`. You can change the port that your IdentityServer host listens on by changing the port in this url. This url also sets the protocol (http or https) that the IdentityServer host will use. In production scenarios you should always use `https`. Next, add the IdentityServer project to the solution. Back in the console, navigate up to the quickstart directory and add the IdentityServer project to the solution. ```console cd .. dotnet sln add ./src/IdentityServer ``` ### Defining An API Scope [Section titled “Defining An API Scope”](#defining-an-api-scope) Scope is a core feature of OAuth that allows you to express the extent or scope of access. Clients request scopes when they initiate the protocol, declaring what scope of access they want. IdentityServer then has to decide which scopes to include in the token. Just because the client has asked for something doesn’t mean they should get it! There are built-in abstractions and extensibility points that you can use to make this decision. Ultimately, IdentityServer issues a token to the client, which then uses the token to access APIs. APIs can check the scopes that were included in the token to make authorization decisions. Scopes don’t have structure imposed by the protocols - they are just space-separated strings. This allows for flexibility when designing the scopes used by a system. In this quickstart, you will create a scope that represents complete access to an API that will be created later in this quickstart. Scope definitions can be loaded in many ways. This quickstart shows how to use a “code as configuration” approach. A minimal Config.cs was created by the template at `src/IdentityServer/Config.cs`. Open it and add an `ApiScope` to the `ApiScopes` property: ```csharp public static IEnumerable ApiScopes => new ApiScope[] { new ApiScope(name: "api1", displayName: "My API") }; ``` See the full file [here](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/1_ClientCredentials/src/IdentityServer/Config.cs). Note In production, it is important to give your API a useful name and display name. Use these names to describe your API in simple terms to both developers and users. Developers will use the name to connect to your API, and end users will see the display name on consent screens, etc. ### Defining The client [Section titled “Defining The client”](#defining-the-client) The next step is to configure a client application that you will use to access the API. You’ll create the client application project later in this quickstart. First, you’ll add configuration for it to your IdentityServer project. In this quickstart, the client will not have an interactive user and will authenticate with IdentityServer using a client secret. Add this client definition to `Config.cs`: ```csharp public static IEnumerable Clients => new Client[] { new Client { ClientId = "client", // no interactive user, use the clientid/secret for authentication AllowedGrantTypes = GrantTypes.ClientCredentials, // secret for authentication ClientSecrets = { new Secret("secret".Sha256()) }, // scopes that client has access to AllowedScopes = { "api1" } } }; ``` Again, see the full file [here](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/1_ClientCredentials/src/IdentityServer/Config.cs). Clients can be configured with many options. Your minimal machine-to-machine client here contains: * A ClientId, which identifies the application to IdentityServer so that it knows which client is trying to connect to it. * A Secret, which you can think of as the password for the client. * The list of scopes that the client is allowed to ask for. Notice that the allowed scope here matches the name of the ApiScope above. ### Configuring IdentityServer [Section titled “Configuring IdentityServer”](#configuring-identityserver) The scope and client definitions are loaded in [HostingExtensions.cs](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/1_ClientCredentials/src/IdentityServer/HostingExtensions.cs). The template created a ConfigureServices method there that is already loading the scopes and clients. You can take a look to see how it is done. Note that the template adds a few things that are not used in this quickstart. Here’s the minimal ConfigureServices method that is needed: Startup.cs ```csharp public static WebApplication ConfigureServices(this WebApplicationBuilder builder) { // Can also be found in Program.cs builder.Services.AddIdentityServer() .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients); return builder.Build(); } ``` That’s it - your IdentityServer is now configured. If you run the project and then navigate to `https://localhost:5001/.well-known/openid-configuration` in your browser, you should see the [discovery document](/identityserver/reference/v8/endpoints/discovery/). The discovery document is a standard endpoint in [OpenID Connect](https://openid.net/specs/openid-connect-discovery-1_0.html) and [OAuth](https://datatracker.ietf.org/doc/html/rfc8414). It is used by your clients and APIs to retrieve configuration data needed to request and validate tokens, login and logout, etc. ![Browser showing discovery endpoint JSON](/_astro/1_discovery.CglV0FSW_uefJN.webp) Note On first startup, IdentityServer will use its automatic key management feature to create a signing key and store it in the `src/IdentityServer/keys` directory. To avoid accidentally disclosing cryptographic secrets, the entire `keys` directory should be excluded from source control. It will be recreated if it is not present. ## Create An API Project [Section titled “Create An API Project”](#create-an-api-project) Next, add an API project to your solution. This API will serve protected resources that will be secured by IdentityServer. You can either use the ASP.NET Core Web API template from Visual Studio or use the .NET CLI to create the API project. To use the CLI, run the following commands: ```console cd src dotnet new webapi -n Api --no-openapi ``` Then navigate back up to the root quickstart directory and add it to the solution by running the following commands: ```console cd .. dotnet sln add ./src/Api ``` ### Add JWT Bearer Authentication [Section titled “Add JWT Bearer Authentication”](#add-jwt-bearer-authentication) Now you will add JWT Bearer Authentication to the API’s ASP.NET pipeline. The goal is to authorize calls to your API using tokens issued by the IdentityServer project. To that end, you will add authentication middleware to the pipeline from the `Microsoft.AspNetCore.Authentication.JwtBearer` NuGet package. This middleware will: * Find and parse a JWT sent with incoming requests as an *Authorization: Bearer* header. * Validate the JWT’s signature to ensure that it was issued by IdentityServer. * Validate that the JWT is not expired. Run this command to add the middleware package to the API: ```console dotnet add ./src/Api package Microsoft.AspNetCore.Authentication.JwtBearer ``` Now add the authentication and authorization services to the Service Collection, and configure the JWT Bearer authentication provider as the default [Authentication Scheme](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/?view=aspnetcore-8.0#authentication-scheme). Program.cs ```csharp builder.Services.AddAuthentication() .AddJwtBearer(options => { options.Authority = "https://localhost:5001"; options.TokenValidationParameters.ValidateAudience = false; }); builder.Services.AddAuthorization(); ``` Note Audience validation is disabled here because access to the api is modeled with `ApiScopes` only. By default, no audience will be emitted unless the api is modeled with `ApiResources` instead. See [here](/identityserver/apis/aspnetcore/jwt/#adding-audience-validation) for a more in-depth discussion. ### Add An Endpoint [Section titled “Add An Endpoint”](#add-an-endpoint) Replace the templated weather forecast endpoint with a new endpoint: ```csharp app.MapGet("identity", (ClaimsPrincipal user) => user.Claims.Select(c => new { c.Type, c.Value })) .RequireAuthorization(); ``` This endpoint will be used to test authorization and to display the claims identity through the eyes of the API. ### Configure API To Listen On Port 6001 [Section titled “Configure API To Listen On Port 6001”](#configure-api-to-listen-on-port-6001) Configure the API to run on `https://localhost:6001` only. You can do this by editing the [launchSettings.json](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/1_ClientCredentials/src/Api/Properties/launchSettings.json) file in the `src/Api/Properties` directory. Change these settings for the `https` profile: ```json { "launchUrl": "identity", "applicationUrl": "https://localhost:6001" } ``` ### Test The Identity Endpoint [Section titled “Test The Identity Endpoint”](#test-the-identity-endpoint) Run the API project using the `https` profile and then navigate to the identity controller at `https://localhost:6001/identity` in a browser. This should return a 401 status code, which means your API requires a credential and is now protected by IdentityServer. ## Create The Client Project [Section titled “Create The Client Project”](#create-the-client-project) The last step is to create a client that requests an access token and then uses that token to access the API. Your client will be a console project in your solution. Run the following commands: ```console cd src dotnet new console -n Client ``` Then as before, add it to your solution using: ```console cd .. dotnet sln add ./src/Client ``` ### Add The IdentityModel NuGet Package [Section titled “Add The IdentityModel NuGet Package”](#add-the-identitymodel-nuget-package) The token endpoint at IdentityServer implements the OAuth protocol, and you could use raw HTTP to access it. However, we have a client library called IdentityModel that encapsulates the protocol interaction in an easy-to-use API. Add the \*Duende.IdentityModel \* NuGet package to your client by running the following command: ```console dotnet add ./src/Client package Duende.IdentityModel ``` ### Retrieve The Discovery Document [Section titled “Retrieve The Discovery Document”](#retrieve-the-discovery-document) IdentityModel includes a client library to use with the discovery endpoint. This way you only need to know the base address of IdentityServer - the actual endpoint addresses can be read from the metadata. Add the following to the client’s Program.cs in the `src/Client/Program.cs` directory: ```csharp using Duende.IdentityModel.Client; // discovery endpoints from metadata var client = new HttpClient(); var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001"); if (disco.IsError) { Console.WriteLine(disco.Error); Console.WriteLine(disco.Exception); return 1; } ``` Note If you get an error connecting, it may be that the development certificate for `localhost` is not trusted. You can run *dotnet dev-certs https —trust* in order to trust the development certificate. This only needs to be done once. ### Request A Token From IdentityServer [Section titled “Request A Token From IdentityServer”](#request-a-token-from-identityserver) Next you can use the information from the discovery document to request a token from `IdentityServer` to access `api1`: ```csharp // request token var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest { Address = disco.TokenEndpoint, ClientId = "client", ClientSecret = "secret", Scope = "api1" }); if (tokenResponse.IsError) { Console.WriteLine(tokenResponse.Error); Console.WriteLine(tokenResponse.ErrorDescription); return 1; } Console.WriteLine(tokenResponse.AccessToken); ``` Note Copy and paste the access token from the console to [jwt.me](https://jwt.me) to inspect the raw token. ### Calling The API [Section titled “Calling The API”](#calling-the-api) To send the access token to the API you typically use the HTTP Authorization header. This is done using the `SetBearerToken` extension method: ```csharp // call api var apiClient = new HttpClient(); apiClient.SetBearerToken(tokenResponse.AccessToken!); // AccessToken is always non-null when IsError is false var response = await apiClient.GetAsync("https://localhost:6001/identity"); if (!response.IsSuccessStatusCode) { Console.WriteLine(response.StatusCode); return 1; } var doc = JsonDocument.Parse(await response.Content.ReadAsStringAsync()).RootElement; Console.WriteLine(JsonSerializer.Serialize(doc, new JsonSerializerOptions { WriteIndented = true })); return 0; ``` The completed `Program.cs` file can be found [here](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/1_ClientCredentials/src/Client/Program.cs). To test the flow, start the IdentityServer and API projects. Once they are running, run the Client project. The output should look like this: ![Windows console showing claims for a bearer token](/_astro/1_client_screenshot.DapOaE8B_Z21a1sv.webp) If you’re using Visual Studio, here’s how to start everything up: 1. Right-click the solution and select *Configure Startup Projects…* 2. Choose *Multiple Startup Projects* and set the action for Api and IdentityServer to Start 3. Run the solution and wait a moment for both the API and IdentityServer to start 4. Right-click the `Client` project and select Debug -> Start Without Debugging. Note By default, an access token will contain claims about the scope, lifetime (nbf and exp), the client ID (client\_id) and the issuer name (iss). #### Authorization At The API [Section titled “Authorization At The API”](#authorization-at-the-api) Right now, the API accepts any access token issued by your IdentityServer. In this section, you will add an [Authorization Policy](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-8.0) to the API that will check for the presence of the “api1” scope in the access token. The protocol ensures that this scope will only be in the token if the client requests it and IdentityServer allows the client to have that scope. You configured IdentityServer to allow this access by [including it in the allowedScopes property](#defining-the-client). Add the following to the `Program.cs` file of the API: Program.cs ```csharp builder.Services.AddAuthorization(options => { options.AddPolicy("ApiScope", policy => { policy.RequireAuthenticatedUser(); policy.RequireClaim("scope", "api1"); }); }); ``` You can now enforce this policy at various levels, e.g.: * globally * for all endpoints * for specific controllers, actions, or endpoints Add the policy to the identity endpoint in `src/Api/Program.cs`: ```csharp app.MapGet("identity", (ClaimsPrincipal user) => user.Claims.Select(c => new { c.Type, c.Value })) .RequireAuthorization("ApiScope"); ``` Now you can run the API again, and it will enforce that the api1 scope is present in the access token. ## Further Experiments [Section titled “Further Experiments”](#further-experiments) This quickstart focused on the success path: * The client was able to request a token. * The client could use the token to access the API. You can now try to provoke errors to learn how the system behaves, e.g.: * Try to connect to IdentityServer when it is not running (unavailable). * Try to use an invalid client id or secret to request the token. * Try to ask for an invalid scope during the token request. * Try to call the API when it is not running (unavailable). * Don’t send the token to the API. * Configure the API to require a different scope than the one in the token. ----- # Interactive Applications With ASP.NET Core > Learn how to add interactive user authentication to an ASP.NET Core application using OpenID Connect and IdentityServer, including configuring the UI, managing user login/logout, and accessing claims. Welcome to Quickstart 2 for Duende IdentityServer! In this quickstart, you will add support for interactive user authentication via the OpenID Connect protocol to the IdentityServer you built in [Quickstart 1](/identityserver/quickstarts/1-client-credentials/). Once that is in place, you will create an ASP.NET Razor Pages application that will use IdentityServer for authentication. Note We recommend you do the quickstarts in order. If you’d like to start here, begin from a copy of the [reference implementation of Quickstart 1](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/1_ClientCredentials). Throughout this quickstart, paths are written relative to the base `quickstart` directory created in part 1, which is the root directory of the reference implementation. You will also need to [install the IdentityServer templates](/identityserver/quickstarts/0-overview/#preparation). ## Video [Section titled “Video”](#video) In addition to the written steps below there’s also a YouTube video available: [YouTube video player](https://www.youtube.com/embed/4aYj4xb7_Cg) ## Enable OIDC In IdentityServer [Section titled “Enable OIDC In IdentityServer”](#enable-oidc-in-identityserver) To enable OIDC in IdentityServer you need: * An interactive UI * Configuration for OIDC scopes * Configuration for an OIDC client * Users to log in with ### Add The UI [Section titled “Add The UI”](#add-the-ui) Support for the OpenID Connect protocol is already built into IdentityServer. You need to provide the User Interface for login, logout, consent, and error. While the look & feel and workflows will differ in each implementation, we provide a Razor Pages-based UI that you can use as a starting point. You can use the .NET CLI to add the quickstart UI to a project. Run the following command from the `src/IdentityServer` directory: ```console dotnet new duende-is-ui ``` ### Enable The UI [Section titled “Enable The UI”](#enable-the-ui) Once you have added the UI, you will need to register its services and enable it in the pipeline. In `src/IdentityServer/HostingExtensions.cs` you will find commented out code in the `ConfigureServices` and `ConfigurePipeline` methods that enable the UI. Note that there are three places to comment in - two in `ConfigurePipeline` and one in `ConfigureServices`. Note There is also a template called `duende-is-inmem` which combines the basic IdentityServer from the `duende-is-empty` template with the quickstart UI from the `duende-is-ui` template. Comment in the service registration and pipeline configuration, run the `IdentityServer` project, and navigate to `https://localhost:5001`. You should now see a home page. Spend some time reading the pages and models, especially those in the `src/IdentityServer/Pages/Account` directory. These pages are the main UI entry points for login and logout. The better you understand them, the easier it will be to make future modifications. ### Configure OIDC Scopes [Section titled “Configure OIDC Scopes”](#configure-oidc-scopes) Similar to OAuth, OpenID Connect uses scopes to represent something you want to protect and that clients want to access. In contrast to OAuth, scopes in OIDC represent identity data like user id, name or email address rather than APIs. Add support for the standard `openid` (subject id) and `profile` (first name, last name, etc.) scopes by declaring them in `src/IdentityServer/Config.cs`: ```csharp public static IEnumerable IdentityResources => new IdentityResource[] { new IdentityResources.OpenId(), new IdentityResources.Profile(), }; ``` Then register the identity resources in `src/IdentityServer/HostingExtensions.cs`: Program.cs ```csharp builder.Services.AddIdentityServer() .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients); ``` Note All standard scopes and their corresponding claims can be found in the OpenID Connect [specification](https://openid.net/specs/openid-connect-core-1_0.html#scopeclaims). ### Add Test Users [Section titled “Add Test Users”](#add-test-users) The sample UI also comes with an in-memory “user database”. You can enable this by calling `AddTestUsers` in `src/IdentityServer/HostingExtensions.cs`: Program.cs ```csharp builder.Services.AddIdentityServer() .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients) .AddTestUsers(TestUsers.Users); ``` In the `TestUsers` class, you can see that two users called `alice` and `bob` are defined with some identity claims. You can use those users to login. Note that the test users’ passwords match their usernames. ### Register An OIDC client [Section titled “Register An OIDC client”](#register-an-oidc-client) The last step in the `IdentityServer` project is to add a new configuration entry for a client that will use OIDC to log in. You will create the application code for this client in the next section. For now, you will register its configuration. OpenID Connect-based clients are very similar to the OAuth clients we added in [Quickstart 1](/identityserver/quickstarts/1-client-credentials/). But since the flows in OIDC are always interactive, we need to add some redirect URLs to our configuration. The `Clients` list in `src/IdentityServer/Config.cs` should look like this: ```csharp public static IEnumerable Clients => new List { // machine to machine client (from quickstart 1) new Client { ClientId = "client", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.ClientCredentials, // scopes that client has access to AllowedScopes = { "api1" } }, // interactive ASP.NET Core Web App new Client { ClientId = "web", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, // where to redirect to after login RedirectUris = { "https://localhost:5002/signin-oidc" }, // where to redirect to after logout PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile } } }; ``` ## Create The OIDC client [Section titled “Create The OIDC client”](#create-the-oidc-client) Next you will create an ASP.NET web application that will allow interactive users to log in using OIDC. Use the webapp template to create the project. Run the following commands from the `src` directory: ```console dotnet new webapp -n WebClient cd .. dotnet sln add ./src/WebClient ``` Note This version of the quickstarts uses [Razor Pages](https://docs.microsoft.com/en-us/aspnet/core/razor-pages/?view=aspnetcore-8.0\&tabs=visual-studio) for the web client. If you prefer MVC, the conversion is straightforward. See the [quickstart for IdentityServer](/identityserver/quickstarts/2-interactive/) that uses it. ### Install The OIDC NuGet Package [Section titled “Install The OIDC NuGet Package”](#install-the-oidc-nuget-package) To add support for OpenID Connect authentication to the `WebClient` project, you need to add the NuGet package containing the OpenID Connect handler. From the `src/WebClient` directory, run the following command: ```console dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect ``` ### Configure Authentication Services [Section titled “Configure Authentication Services”](#configure-authentication-services) Then add the authentication service and register the cookie and OpenIdConnect authentication providers in `src/WebClient/Program.cs`: Program.cs ```csharp builder.Services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.Authority = "https://localhost:5001"; options.ClientId = "web"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.MapInboundClaims = false; // Don't rename claim types options.SaveTokens = true; }); ``` Note If you are unfamiliar with the fundamentals of how the ASP.NET Core authentication system works, then we recommend this recording of an [Introduction to ASP.NET Core Authentication and Authorization](https://www.youtube.com/watch?v=02Yh3sxzAYI). `AddAuthentication` registers the authentication services. Notice that in its options, the DefaultChallengeScheme is set to “oidc”, and the DefaultScheme is set to “Cookies”. The DefaultChallengeScheme is used when an unauthenticated user must log in. This begins the OpenID Connect protocol, redirecting the user to `IdentityServer`. After the user has logged in and been redirected back to the client, the client creates its own local cookie. Subsequent requests to the client will include this cookie and be authenticated with the default Cookie scheme. After the call to `AddAuthentication`, `AddCookie` adds the handler that can process the local cookie. Finally, `AddOpenIdConnect` is used to configure the handler that performs the OpenID Connect protocol. The `Authority` indicates where the trusted token service is located. The `ClientId` and the `ClientSecret` identify this client. The `Scope` is the collection of scopes that the client will request. By default, it includes the openid and profile scopes, but clear the collection and add them back for explicit clarity. `SaveTokens` is used to persist the tokens in the cookie (as they will be needed later). Note This uses the *authorization code* flow with PKCE to connect to the OpenID Connect provider. See [here](/identityserver/fundamentals/clients/) for more information on protocol flows. ### Configure The Pipeline [Section titled “Configure The Pipeline”](#configure-the-pipeline) Now add `UseAuthentication` to the ASP.NET pipeline in `src/WebClient/Program.cs`. Also chain a call to `RequireAuthorization` onto `MapRazorPages` to disable anonymous access for the entire application. ```csharp app.UseRouting(); app.UseAuthentication(); app.UseAuthorization(); app.MapRazorPages().RequireAuthorization(); ``` Note See the ASP.NET Core documentation on [Razor Pages authorization conventions](https://docs.microsoft.com/en-us/aspnet/core/security/authorization/razor-pages-authorization?view=aspnetcore-8.0) for more options that allow you to specify authorization on a per page or directory basis. ### Display The Auth Cookie [Section titled “Display The Auth Cookie”](#display-the-auth-cookie) Modify `src/WebClient/Pages/Index.cshtml` to display the claims of the user and the cookie properties: ```csharp @page @model IndexModel @using Microsoft.AspNetCore.Authentication

Claims

@foreach (var claim in User.Claims) {
@claim.Type
@claim.Value
}

Properties

@foreach (var prop in (await HttpContext.AuthenticateAsync()).Properties!.Items) {
@prop.Key
@prop.Value
}
``` ### Configure WebClient’s Port [Section titled “Configure WebClient’s Port”](#configure-webclients-port) Update the client’s applicationUrl in `src/WebClient/Properties/launchSettings.json` to use port 5002. ```json { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "WebClient": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:5002", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ``` ## Test The client [Section titled “Test The client”](#test-the-client) Now everything should be in place to log in to `WebClient` using OIDC. Run `IdentityServer` and `WebClient` and then trigger the authentication handshake by navigating to the protected home page. You should see a redirect to the login page in `IdentityServer`. ![login screen for IdentityServer](/_astro/2_login.CED0TwDu_xcUgm.webp) After you log in, `IdentityServer` will redirect back to `WebClient`, where the OpenID Connect authentication handler will process the response and sign-in the user locally by setting a cookie. Finally, the `WebClient`’s page will show the contents of the cookie. ![ASP.NET Core application showing ClaimsPrincipal's claims](/_astro/2_claims.iXvcLYaR_ZRP40f.webp) As you can see, the cookie has two parts: the claims of the user and some metadata in the properties. This metadata also contains the original access and id tokens issued by `IdentityServer`. Feel free to copy these tokens to [jwt.me](https://jwt.me) to inspect their contents. ## Adding Sign-out [Section titled “Adding Sign-out”](#adding-sign-out) Next you will add sign-out to `WebClient`. To sign out, you need to * Clear local application cookies * Make a roundtrip to `IdentityServer` using the OIDC protocol to clear its session The cookie auth handler will clear the local cookie when you sign out from its authentication scheme. The OpenId Connect handler will perform the protocol steps for the roundtrip to `IdentityServer` when you sign out of its scheme. Create a page to trigger sign-out of both schemes by running the following command from the `src/WebClient/Pages` directory: ```console dotnet new page -n Signout ``` Update the new page’s model (`src/WebClient/Pages/Signout.cshtml.cs`) with the following code: ```csharp public class SignoutModel : PageModel { public IActionResult OnGet() { return SignOut("Cookies", "oidc"); } } ``` This will clear the local cookie and then redirect to the IdentityServer. The IdentityServer will clear its cookies and then give the user a link to return back to the web application. Create a link to the logout page in `src/WebClient/Pages/Shared/_Layout.cshtml` within the navbar-nav list: ```html ``` Run the application again, and try logging out. Observe that you get redirected to the end session endpoint, and that both session cookies are cleared. ## Getting Claims From The UserInfo Endpoint [Section titled “Getting Claims From The UserInfo Endpoint”](#getting-claims-from-the-userinfo-endpoint) You might have noticed that even though you’ve configured the client to be allowed to retrieve the `profile` identity scope, the claims associated with that scope (such as `name`, `given_name`, `family_name`, etc.) don’t appear in the returned token. You need to tell the client to retrieve those claims from the userinfo endpoint by specifying scopes that the client application needs to access and setting the `GetClaimsFromUserInfoEndpoint` option. Add the following to `ConfigureServices` in `src/WebClient/Program.cs`: Program.cs ```csharp .AddOpenIdConnect("oidc", options => { // ... options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.GetClaimsFromUserInfoEndpoint = true; // ... }); ``` After restarting the client app and logging back in, you should see additional user claims associated with the `profile` identity scope displayed on the page. ![ASP.NET Core page showing additional claims](/_astro/2_additional_claims.D7m0XRhG_Z1I6mLe.webp) ## Further Experiments [Section titled “Further Experiments”](#further-experiments) This quickstart created a client with interactive login using OIDC. To experiment further you can * Add additional claims to the identity * Add support for external authentication ### Add More Claims [Section titled “Add More Claims”](#add-more-claims) To add more claims to the identity: * Add a new identity resource to the list in `src/IdentityServer/Config.cs`. Name it and specify which claims should be returned when it is requested. The `Name` property of the resource is the scope value that clients can request to get the associated `UserClaims`. For example, you could add an `IdentityResource` named “verification” which would include the `email` and `email_verified` claims. ```csharp public static IEnumerable IdentityResources => new List { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResource() { Name = "verification", UserClaims = new List { JwtClaimTypes.Email, JwtClaimTypes.EmailVerified } } }; ``` * Give the client access to the resource via the `AllowedScopes` property on the client configuration in `src/IdentityServer/Config.cs`. The string value in `AllowedScopes` must match the `Name` property of the resource. ```csharp new Client { ClientId = "web", //... AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "verification" } } ``` * Request the resource by adding it to the `Scopes` collection on the OpenID Connect handler configuration in `src/WebClient/Program.cs`, and add a [ClaimAction](https://docs.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.authentication.openidconnect.openidconnectoptions.claimactions?view=aspnetcore-8.0) to map the new claim returned from the userinfo endpoint onto a user claim. Program.cs ```csharp .AddOpenIdConnect("oidc", options => { // ... options.Scope.Add("verification"); options.ClaimActions.MapJsonKey("email_verified", "email_verified"); // ... } ``` IdentityServer uses the `IProfileService` to retrieve claims for tokens and the userinfo endpoint. You can provide your own implementation of `IProfileService` to customize this process with custom logic, data access, etc. Since you are using `AddTestUsers`, the `TestUserProfileService` is used automatically. It will automatically include requested claims from the test users added in `src/IdentityServer/TestUsers.cs`. ### Add Support for External Authentication [Section titled “Add Support for External Authentication”](#add-support-for-external-authentication) Adding support for external authentication to your IdentityServer can be done with very little code; all that is needed is an authentication handler. ASP.NET Core ships with handlers for OpenID Connect, and provides [integrations for Google, Facebook, Microsoft Account, Entra ID, and more](/identityserver/ui/login/external/#third-party-aspnet-core-authentication-handlers). In this section, you’ll register the Duende IdentityServer demo instance at `demo.duendesoftware.com` as an external provider. Since no other configuration is required apart from your IdentityServer, it is a good starting point. You’ll also see [how to add Google authentication support](#add-google-support). #### Adding An Additional OpenID Connect-Based External Provider [Section titled “Adding An Additional OpenID Connect-Based External Provider”](#adding-an-additional-openid-connect-based-external-provider) A cloud-hosted [demo instance of Duende IdentityServer](https://demo.duendesoftware.com) can be added as an additional external provider. Register and configure the services for the OpenId Connect handler in`src/IdentityServer/HostingExtensions.cs`: HostingExtensions.cs ```csharp builder.Services.AddAuthentication() .AddOpenIdConnect("oidc", "Sign-in with demo.duendesoftware.com", options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.SignOutScheme = IdentityServerConstants.SignoutScheme; options.SaveTokens = true; options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", RoleClaimType = "role" }; }); ``` Now if you try to authenticate, you should see an additional *Sign-in with demo.duendesoftware.com* button to log in to the cloud-hosted demo IdentityServer. If you click that button, you will be redirected to . Check that the page’s location has changed and then log in using the `alice` or `bob` users (their passwords are their usernames, just as they are for the local test users). You should land back at `WebClient`, authenticated with a demo user. The demo users are logically distinct entities from the local test users, even though they happen to have identical usernames. Inspect their claims in `WebClient` and note the differences between them, such as the distinct `sub` claims. Note The quickstart UI auto-provisions external users. When an external user logs in for the first time, a new local user is created with a copy of all the external user’s claims. This auto-provisioning process occurs in the `OnGet` method of `src/IdentityServer/Pages/ExternalLogin/Callback.cshtml.cs`, and is completely customizable. For example, you could modify `Callback` so that it will require registration before provisioning the external user. #### Add Google Support [Section titled “Add Google Support”](#add-google-support) `Microsoft.AspnetCore.Authentication.Google` no longer maintained Before .NET 10, the `Microsoft.AspnetCore.Authentication.Google` package was provided by Microsoft. Starting with .NET 10, Microsoft [stopped shipping new versions of the `Microsoft.AspnetCore.Authentication.Google` package](https://github.com/dotnet/aspnetcore/issues/61817). To add Google authentication, we recommend using the [`Google.Apis.Auth.AspNetCore3`](https://www.nuget.org/packages/Google.Apis.Auth.AspNetCore3/) package that is shipped by Google. To use Google for authentication, you need to: * Add the `Google.Apis.Auth.AspNetCore3` NuGet package to the IdentityServer project. * Register with Google and [set up a client](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-9.0#create-the-google-oauth-20-client-id-and-secret). * Store the client id and secret securely with `dotnet user-secrets`. * Add the Google authentication handler to the middleware pipeline and configure it. See [Microsoft’s guide](https://learn.microsoft.com/en-us/aspnet/core/security/authentication/social/google-logins?view=aspnetcore-9.0#create-the-google-oauth-20-client-id-and-secret) for details on how to register with Google, create the client, and store the secrets in user secrets. **Stop before adding the authentication middleware and Google authentication handler to the pipeline.** You will need an IdentityServer specific option. Add the following to `ConfigureServices` in `src/IdentityServer/HostingExtensions.cs`: HostingExtensions.cs ```csharp builder.Services.AddAuthentication() .AddGoogleOpenIdConnect( authenticationScheme: GoogleOpenIdConnectDefaults.AuthenticationScheme, displayName: "Google", configureOptions: options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.ClientId = builder.Configuration["Authentication:Google:ClientId"]; options.ClientSecret = builder.Configuration["Authentication:Google:ClientSecret"]; }); ``` Note Note that the `authenticationScheme` and `displayName` parameters are optional. They are added here to make the login button display a short and concise “Google” instad of the default “Google OpenIdConnect”. When authenticating with Google, there are again two [authentication schemes](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/#authentication-scheme). `AddGoogleOpenIdConnect` adds the `GoogleOpenIdConnect` scheme, which handles the protocol flow back and forth with Google. After successful login, the application needs to sign in to an additional scheme that can authenticate future requests without needing a roundtrip to Google - typically by issuing a local cookie. The `SignInScheme` tells the Google handler to use the scheme named `IdentityServerConstants.ExternalCookieAuthenticationScheme`, which is a cookie authentication handler automatically created by IdentityServer that is intended for external logins. Now run `IdentityServer` and `WebClient` and try to authenticate (you may need to log out and log back in) You will see a *Google* button on the login page. ![IdentityServer login page showing Google as an external login option](/_astro/2_google_login.BG4lBuSl_Z2fAHBr.webp) Click on *Google* and authenticate with a Google account. You should land back on the `WebClient` home page, showing that the user is now coming from Google with claims sourced from Google’s data. Note The login page renders the Google button automatically when there are external providers registered as authentication schemes. See the `BuildModelAsync` method in `src/IdentityServer/Pages/Account/Login/Index.cshtml.cs` and the corresponding Razor template for more details. ----- # ASP.NET Core And API access > Learn how to combine user authentication with API access by requesting both identity and API scopes during the OpenID Connect login flow. Welcome to Quickstart 3 for Duende IdentityServer! The previous quickstarts introduced [API access](/identityserver/quickstarts/1-client-credentials/) and [user authentication](/identityserver/quickstarts/2-interactive/). This quickstart will bring the two together. In addition to the written steps below a YouTube video is available: [YouTube video player](https://www.youtube.com/embed/zHVmzgPUImc) OpenID Connect and OAuth combine elegantly; you can achieve both user authentication and api access in a single exchange with the token service. In Quickstart 2, the token request in the login process asked for only identity resources, that is, only scopes such as *profile* and *openid*. In this quickstart, you will add scopes for API resources to that request. *IdentityServer* will respond with two tokens: 1. the identity token, containing information about the authentication process and session, and 2. the access token, allowing access to APIs on behalf of the logged on user Note We recommend you do the quickstarts in order. If you’d like to start here, begin from a copy of the [reference implementation of Quickstart 2](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/2_InteractiveAspNetCore). Throughout this quickstart, paths are written relative to the base `_quickstart` directory created in part 1, which is the root directory of the reference implementation. You will also need to [install the IdentityServer templates](/identityserver/quickstarts/0-overview/#preparation). ## Modifying The Client Configuration [Section titled “Modifying The Client Configuration”](#modifying-the-client-configuration) The client configuration in IdentityServer requires one straightforward update. We should add the *api1* resource to the allowed scopes list so that the client will have permission to access it. Update the *Client* in *src/IdentityServer/Config.cs* as follows: ```csharp new Client { ClientId = "web", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, // where to redirect to after login RedirectUris = { "https://localhost:5002/signin-oidc" }, // where to redirect to after logout PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" }, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "verification", "api1" } } ``` ## Modifying The Web client [Section titled “Modifying The Web client”](#modifying-the-web-client) Now configure the client to ask for access to api1 by requesting the *api1* scope. This is done in the OpenID Connect handler configuration in *src/WebClient/Program.cs*: Program.cs ```csharp builder.Services.AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.Authority = "https://localhost:5001"; options.ClientId = "web"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api1"); options.Scope.Add("verification"); options.ClaimActions.MapJsonKey("email_verified", "email_verified"); options.GetClaimsFromUserInfoEndpoint = true; options.MapInboundClaims = false; // Don't rename claim types options.SaveTokens = true; }); ``` Since *SaveTokens* is enabled, ASP.NET Core will automatically store the id and access tokens in the properties of the authentication cookie. If you run the solution and authenticate, you will see the tokens on the page that displays the cookie claims and properties created in quickstart 2. ## Using The Access Token [Section titled “Using The Access Token”](#using-the-access-token) Now you will use the access token to authorize requests from the *WebClient* to the *Api*. Create a page that will 1. Retrieve the access token from the session using the *GetTokenAsync* method from *Microsoft.AspNetCore.Authentication* 2. Set the token in an *Authentication: Bearer* HTTP header 3. Make an HTTP request to the *API* 4. Display the results Create the Page by running the following command from the *src/WebClient/Pages* directory: ```console dotnet new page -n CallApi ``` Update *src/WebClient/Pages/CallApi.cshtml.cs* as follows: ```csharp public class CallApiModel : PageModel { public string Json = string.Empty; public async Task OnGet() { var accessToken = await HttpContext.GetTokenAsync("access_token"); var client = new HttpClient(); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken); var content = await client.GetStringAsync("https://localhost:6001/identity"); var parsed = JsonDocument.Parse(content); var formatted = JsonSerializer.Serialize(parsed, new JsonSerializerOptions { WriteIndented = true }); Json = formatted; } } ``` And update *src/WebClient/Pages/CallApi.cshtml* as follows: ```html @page @model MyApp.Namespace.CallApiModel
@Model.Json
``` Also add a link to the new page in *src/WebClient/Shared/\_Layout.cshtml* with the following: ```html ``` Make sure the *IdentityServer* and *Api* projects are running, start the *WebClient* and request */CallApi* after authentication. ----- # Token Management > Learn how to manage access tokens in interactive applications, including requesting refresh tokens, caching, and automatic token refresh using Duende.AccessTokenManagement. Welcome to this Quickstart for Duende IdentityServer! The previous quickstart introduced [API access](/identityserver/quickstarts/3-api-access/) with interactive applications, but by far the most complex task for a typical client is to manage the access token. In addition to the written steps below a YouTube video is available: [YouTube video player](https://www.youtube.com/embed/W8jtc2Ou1d4) Given that the access token has a finite lifetime, you typically want to * request a refresh token in addition to the access token at login time * cache those tokens * use the access token to call APIs until it expires * use the refresh token to get a new access token * repeat the process of caching and refreshing with the new token ASP.NET Core has built-in facilities that can help you with some of those tasks (like caching or sessions), but there is still quite some work left to do. [Duende.AccessTokenManagement](/accesstokenmanagement) can help. It provides abstractions for storing tokens, automatic refresh of expired tokens, etc. ## Requesting A Refresh Token [Section titled “Requesting A Refresh Token”](#requesting-a-refresh-token) To allow the *web* client to request a refresh token set the *AllowOfflineAccess* property to true in the client configuration. Update the *Client* in *src/IdentityServer/Config.cs* as follows: ```csharp new Client { ClientId = "web", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, // where to redirect to after login RedirectUris = { "https://localhost:5002/signin-oidc" }, // where to redirect to after logout PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" }, AllowOfflineAccess = true, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "verification", "api1" } } ``` To get the refresh token the *offline\_access* scope has to be requested by the client. In *src/WebClient/Program.cs* add the scope to the scope list: ```csharp options.Scope.Add("offline_access"); ``` When running the solution the refresh token should now be visible under *Properties* on the landing page of the client. ## Automatically Refreshing An Access Token [Section titled “Automatically Refreshing An Access Token”](#automatically-refreshing-an-access-token) In the WebClient project add a reference to the NuGet package `Duende.AccessTokenManagement.OpenIdConnect` and in *Program.cs* add the needed types to dependency injection: Program.cs ```csharp builder.Services.AddOpenIdConnectAccessTokenManagement(); ``` In *CallApi.cshtml.cs* update the method body of `OnGet` as follows: CallApi.cshtml.cs ```csharp public async Task OnGet() { var tokenInfo = await HttpContext.GetUserAccessTokenAsync(); var client = new HttpClient(); client.SetBearerToken(tokenInfo.AccessToken!); var content = await client.GetStringAsync("https://localhost:6001/identity"); var parsed = JsonDocument.Parse(content); var formatted = JsonSerializer.Serialize(parsed, new JsonSerializerOptions { WriteIndented = true }); Json = formatted; } ``` There are two changes here that utilize the AccessTokenManagement NuGet package: * An object called tokenInfo containing all stored tokens is returned by the *GetUserAccessTokenAsync* extension method. This will make sure the access token is *automatically refreshed* using the refresh token if needed. * The *SetBearerToken* extension method on HttpClient is used for convenience to place the access token in the needed HTTP header. ## Using A Named HttpClient [Section titled “Using A Named HttpClient”](#using-a-named-httpclient) On each call to OnGet in *CallApi.cshtml.cs* a new HttpClient is created in the code above. Recommended however is to use the [HttpClientFactory](https://learn.microsoft.com/en-us/dotnet/core/extensions/httpclient-factory) pattern so that instances can be reused. `Duende.AccessTokenManagement.OpenIdConnect` builds on top of *HttpClientFactory* to create HttpClient instances that automatically retrieve the needed access token and refresh if needed. In the client in *Program.cs* under the call to *AddOpenIdConnectAccessTokenManagement* register the HttpClient: Program.cs ```csharp builder.Services.AddUserAccessTokenHttpClient("apiClient", configureClient: client => { client.BaseAddress = new Uri("https://localhost:6001"); }); ``` Now the *OnGet* method in *CallApi.cshtml.cs* can be even more straightforward: ```csharp public class CallApiModel(IHttpClientFactory httpClientFactory) : PageModel { public string Json = string.Empty; public async Task OnGet() { var client = httpClientFactory.CreateClient("apiClient"); var content = await client.GetStringAsync("https://localhost:6001/identity"); var parsed = JsonDocument.Parse(content); var formatted = JsonSerializer.Serialize(parsed, new JsonSerializerOptions { WriteIndented = true }); Json = formatted; } } ``` Note that: * The httpClientFactory is injected using a primary constructor. The type was registered when *AddOpenIdConnectAccessTokenManagement* was called in *Program.cs*. * The client is created using the factory passing in the name of the client that was registered in *program.cs*. * No additional code is needed. The client will automatically retrieve the access token and refresh it if needed. ----- # Entity Framework Core: Configuration & Operational Data > Learn how to configure IdentityServer to use Entity Framework Core for storing configuration and operational data in a persistent database. Welcome to Quickstart 4 for Duende IdentityServer! In this quickstart you will move configuration and other temporary data into a database using Entity Framework. In addition to the written steps below a YouTube video is available: [YouTube video player](https://www.youtube.com/embed/GKSp3StwaVA) Note We recommend you do the quickstarts in order. If you’d like to start here, begin from a copy of the [reference implementation of Quickstart 3](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/3_AspNetCoreAndApis). Throughout this quickstart, paths are written relative to the base `quickstart` directory created in part 1, which is the root directory of the reference implementation. You will also need to [install the IdentityServer templates](/identityserver/quickstarts/0-overview/#preparation). In the previous quickstarts, you configured clients and scopes with code. IdentityServer loaded this configuration data into memory on startup. Modifying the configuration required a restart. IdentityServer also generates temporary data, such as authorization codes, consent choices, and refresh tokens. Up to this point in the quickstarts, this data was also stored in memory. To move this data into a database that is persistent between restarts and across multiple IdentityServer instances, you will use the `Duende.IdentityServer.EntityFramework` library. Note This quickstart shows how to add Entity Framework support to IdentityServer manually. There is also a template that will create a new IdentityServer project with the EntityFramework integration already added: `dotnet new duende-is-ef`. ## Configure IdentityServer [Section titled “Configure IdentityServer”](#configure-identityserver) #### Install Duende.IdentityServer.EntityFramework [Section titled “Install Duende.IdentityServer.EntityFramework”](#install-duendeidentityserverentityframework) IdentityServer’s Entity Framework integration is provided by the `Duende.IdentityServer.EntityFramework` NuGet package. Run the following commands from the `src/IdentityServer` directory to replace the `Duende.IdentityServer` package with it. Replacing packages prevents any dependency issues with version mismatches. ```console dotnet remove package Duende.IdentityServer dotnet add package Duende.IdentityServer.EntityFramework ``` #### Install Microsoft.EntityFrameworkCore.Sqlite [Section titled “Install Microsoft.EntityFrameworkCore.Sqlite”](#install-microsoftentityframeworkcoresqlite) `Duende.IdentityServer.EntityFramework` can be used with any Entity Framework database provider. In this quickstart, you will use Sqlite. To add Sqlite support to your IdentityServer project, install the Entity framework Sqlite NuGet package by running the following command from the `src/IdentityServer` directory: ```console dotnet add package Microsoft.EntityFrameworkCore.Sqlite ``` #### Configuring The Stores [Section titled “Configuring The Stores”](#configuring-the-stores) `Duende.IdentityServer.EntityFramework` stores configuration and operational data in separate stores, each with their own DbContext. * ConfigurationDbContext: used for configuration data such as clients, resources, and scopes * PersistedGrantDbContext: used for dynamic operational data such as authorization codes and refresh tokens To use these stores, replace the existing calls to `AddInMemoryClients`, `AddInMemoryIdentityResources`, and `AddInMemoryApiScopes` in your `ConfigureServices` method in `src/IdentityServer/HostingExtensions.cs` with `AddConfigurationStore` and `AddOperationalStore`, like this: HostingExtensions.cs ```csharp public static WebApplication ConfigureServices(this WebApplicationBuilder builder) { builder.Services.AddRazorPages(); var migrationsAssembly = typeof(Program).Assembly.GetName().Name; const string connectionString = @"Data Source=Duende.IdentityServer.Quickstart.EntityFramework.db"; builder.Services.AddIdentityServer() .AddConfigurationStore(options => { options.ConfigureDbContext = b => b.UseSqlite(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); }) .AddOperationalStore(options => { options.ConfigureDbContext = b => b.UseSqlite(connectionString, sql => sql.MigrationsAssembly(migrationsAssembly)); }) .AddTestUsers(TestUsers.Users); //... } ``` Note You will use Entity Framework migrations later on in this quickstart to manage the database schema. The call to `MigrationsAssembly(...)` tells Entity Framework that the host project will contain the migrations. This is necessary since the host project is in a different assembly than the one that contains the `DbContext` classes. ## Managing Database Schema [Section titled “Managing Database Schema”](#managing-database-schema) The `Duende.IdentityServer.EntityFramework.Storage` NuGet package (installed as a dependency of `Duende.IdentityServer.EntityFramework`) contains entity classes that map onto IdentityServer’s models. These entities are maintained in sync with IdentityServer’s models - when the models are changed in a new release, corresponding changes are made to the entities. As you use IdentityServer and upgrade over time, you are responsible for your database schema and changes necessary to that schema. One approach for managing those changes is to use [EF migrations](https://docs.microsoft.com/en-us/ef/core/managing-schemas/migrations/index), which is what this quickstart will use. If migrations are not your preference, then you can manage the schema changes in any way you see fit. #### Adding Migrations [Section titled “Adding Migrations”](#adding-migrations) To create migrations, you will need to install the Entity Framework Core CLI tool on your machine and the `Microsoft.EntityFrameworkCore.Design` NuGet package in IdentityServer. Run the following commands from the `src/IdentityServer` directory: ```console dotnet tool install --global dotnet-ef dotnet add package Microsoft.EntityFrameworkCore.Design ``` #### Handle Expected Exception [Section titled “Handle Expected Exception”](#handle-expected-exception) The Entity Framework CLI internally starts up `IdentityServer` for a short time in order to read your database configuration. After it has read the configuration, it shuts `IdentityServer` down by throwing a `HostAbortedException` exception. We expect this exception to be unhandled and therefore stop `IdentityServer`. Since it is expected, you do not need to log it as a fatal error. Update the error logging code in `src/IdentityServer/Program.cs` as follows: ```csharp // See https://github.com/dotnet/runtime/issues/60600 re StopTheHostException catch (Exception ex) when (ex.GetType().Name is not "StopTheHostException") { Log.Fatal(ex, "Unhandled exception"); } ``` Now run the following two commands from the `src/IdentityServer` directory to create the migrations: ```console dotnet ef migrations add InitialIdentityServerPersistedGrantDbMigration -c PersistedGrantDbContext -o Data/Migrations/IdentityServer/PersistedGrantDb dotnet ef migrations add InitialIdentityServerConfigurationDbMigration -c ConfigurationDbContext -o Data/Migrations/IdentityServer/ConfigurationDb ``` You should now see a `src/IdentityServer/Data/Migrations/IdentityServer` directory in your project containing the code for your newly created migrations. #### Initializing Database [Section titled “Initializing Database”](#initializing-database) Now that you have the migrations, you can write code to create the database from them and seed the database with the same configuration data used in the previous quickstarts. Note The approach used in this quickstart is used to make it easy to get IdentityServer up and running. You should devise your own database creation and maintenance strategy that is appropriate for your architecture. In `src/IdentityServer/HostingExtensions.cs`, add this method to initialize the database: ```csharp private static void InitializeDatabase(IApplicationBuilder app) { using (var serviceScope = app.ApplicationServices.GetService()!.CreateScope()) { serviceScope.ServiceProvider.GetRequiredService().Database.Migrate(); var context = serviceScope.ServiceProvider.GetRequiredService(); context.Database.Migrate(); if (!context.Clients.Any()) { foreach (var client in Config.Clients) { context.Clients.Add(client.ToEntity()); } context.SaveChanges(); } if (!context.IdentityResources.Any()) { foreach (var resource in Config.IdentityResources) { context.IdentityResources.Add(resource.ToEntity()); } context.SaveChanges(); } if (!context.ApiScopes.Any()) { foreach (var resource in Config.ApiScopes) { context.ApiScopes.Add(resource.ToEntity()); } context.SaveChanges(); } } } ``` Call `InitializeDatabase` from the `ConfigurePipeline` method: ```csharp public static WebApplication ConfigurePipeline(this WebApplication app) { app.UseSerilogRequestLogging(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } InitializeDatabase(app); //... } ``` Now if you run the IdentityServer project, the database should be created and seeded with the quickstart configuration data. You should be able to use a tool like SQL Lite Studio to connect and inspect the data. ![SQLiteStudio showing the contents of an IdentityServer database](/_astro/ef_database.CgdRJRsh_ZtgcXf.webp) Note The `InitializeDatabase` method is convenient way to seed the database, but this approach is not ideal to leave in to execute each time the application runs. Once your database is populated, consider removing the call to the API. ## Run The Client Applications [Section titled “Run The Client Applications”](#run-the-client-applications) You should now be able to run any of the existing client applications and sign-in, get tokens, and call the API — all based upon the database configuration. ----- # ASP.NET Core Identity > Learn how to integrate ASP.NET Core Identity with IdentityServer to manage user authentication and storage using Entity Framework Core. Welcome to Quickstart 5 for Duende IdentityServer! In this quickstart you will integrate IdentityServer with ASP.NET Core Identity. Note We recommend you do the quickstarts in order. If you’d like to start here, begin from a copy of the [reference implementation of Quickstart 4](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/4_EntityFramework). Throughout this quickstart, paths are written relative to the base `quickstart` directory created in part 1, which is the root directory of the reference implementation. You will also need to [install the IdentityServer templates](/identityserver/quickstarts/0-overview/#preparation). IdentityServer’s flexible design allows you to use any database you want to store users and their data, including password hashes, multi-factor authentication details, roles, claims, profile data, etc. If you are starting with a new user database, then ASP.NET Core Identity is one option you could choose. This quickstart shows how to use ASP.NET Core Identity with IdentityServer. The approach this quickstart takes to using ASP.NET Core Identity is to create a new project for the IdentityServer host. This new project will replace the IdentityServer project you built up in the previous quickstarts. You will create a new project because it is a convenient way to get the UI assets that are needed to login and logout with ASP.NET Core Identity. All the other projects in this solution (for the clients and the API) will remain the same. Note This quickstart assumes you are familiar with how ASP.NET Core Identity works. If you are not, it is recommended that you first [learn about it](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-8.0). In addition to the written steps below a YouTube video is available: [YouTube video player](https://www.youtube.com/embed/blvZzYsr8uI) ## New Project For ASP.NET Core Identity [Section titled “New Project For ASP.NET Core Identity”](#new-project-for-aspnet-core-identity) The first step is to add a new project for ASP.NET Core Identity to your solution. We provide a template that contains the minimal UI assets needed to use ASP.NET Core Identity with IdentityServer. You will eventually delete the old project for IdentityServer, but there are some items that you will need to migrate over. Start by creating a new IdentityServer project that will use ASP.NET Core Identity. Run the following commands from the `src` directory: ```console dotnet new duende-is-aspid -n IdentityServerAspNetIdentity cd .. dotnet sln add ./src/IdentityServerAspNetIdentity ``` When prompted to “seed” the user database, choose “Y” for “yes”. This populates the user database with our “alice” and “bob” users. Their passwords are “Pass123$”. Note The template uses Sqlite as the database for the users, and EF migrations are pre-created in the template. If you wish to use a different database provider, you will need to change the provider used in the code and re-create the EF migrations. ## Inspect The New Project [Section titled “Inspect The New Project”](#inspect-the-new-project) Open the new project in the editor of your choice, and inspect the generated code. Much of it is the same from the prior quickstarts and templates. The following sections will describe some key differences and guide you through migrating configuration from the old IdentityServer Project, including: * The project file (`IdentityServerAspNetIdentity.csproj`) * Pipeline and service configuration (`HostingExtensions.cs`) * Resource and client configuration (Config.cs) * Entry point and seed data (`Program.cs` and `SeedData.cs`) * Login and logout pages (Pages in `Pages/Account`) #### IdentityServerAspNetIdentity.csproj [Section titled “IdentityServerAspNetIdentity.csproj”](#identityserveraspnetidentitycsproj) Notice the reference to `Duende.IdentityServer.AspNetIdentity`. This NuGet package contains the ASP.NET Core Identity integration components for IdentityServer. #### HostingExtensions.cs [Section titled “HostingExtensions.cs”](#hostingextensionscs) In `ConfigureServices` notice the necessary `AddDbContext()` and *AddIdentity\()* calls are done to configure ASP.NET Core Identity. Also notice that much of the same IdentityServer configuration you did in the previous quickstarts is already done. The template uses the in-memory style for clients and resources, which are defined in `Config.cs`. Finally, notice the addition of the new call to `AddAspNetIdentity()`. `AddAspNetIdentity()` adds the integration layer to allow IdentityServer to access the user data for the ASP.NET Core Identity user database. This is needed when IdentityServer must add claims for the users into tokens. Note that *AddIdentity\()* must be invoked before `AddIdentityServer()`. #### Config.cs [Section titled “Config.cs”](#configcs) `Config.cs` contains the hard-coded in-memory clients and resource definitions. To keep the same clients and API working as the prior quickstarts, we need to copy over the configuration data from the old IdentityServer project into this one. Do that now, and afterwards `Config.cs` should look like this: ```csharp public static class Config { public static IEnumerable IdentityResources => new IdentityResource[] { new IdentityResources.OpenId(), new IdentityResources.Profile(), new IdentityResource() { Name = "verification", UserClaims = new List { JwtClaimTypes.Email, JwtClaimTypes.EmailVerified } } }; public static IEnumerable ApiScopes => new ApiScope[] { new ApiScope(name: "api1", displayName: "My API") }; public static IEnumerable Clients => new Client[] { new Client { ClientId = "client", // no interactive user, use the clientid/secret for authentication AllowedGrantTypes = GrantTypes.ClientCredentials, // secret for authentication ClientSecrets = { new Secret("secret".Sha256()) }, // scopes that client has access to AllowedScopes = { "api1" } }, // interactive ASP.NET Core Web App new Client { ClientId = "web", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, // where to redirect to after login RedirectUris = { "https://localhost:5002/signin-oidc" }, // where to redirect to after logout PostLogoutRedirectUris = { "https://localhost:5002/signout-callback-oidc" }, AllowOfflineAccess = true, AllowedScopes = { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "verification", "api1" } } }; } ``` At this point, you no longer need the old IdentityServer project and can remove it from the solution. From the quickstart directory, run the following commands: ```console dotnet sln remove ./src/IdentityServer rm -r ./src/IdentityServer ``` #### Program.cs and SeedData.cs [Section titled “Program.cs and SeedData.cs”](#programcs-and-seeddatacs) The application entry point in `Program.cs` is a little different than most ASP.NET Core projects. Notice that it looks for a command line argument called `/seed` which is used as a flag to seed the users in the ASP.NET Core Identity database. This seed process is invoked during template creation and already ran when you were prompted to seed the database. Look at the `SeedData` class’ code to see how the database is created and the first users are created. #### Account Pages [Section titled “Account Pages”](#account-pages) Finally, take a look at the pages in the `src/IdentityServerAspNetIdentity/Pages/Account` directory. These pages contain slightly different login and logout code than the prior quickstart and templates because the login and logout processes now rely on ASP.NET Core Identity. Notice the use of the `SignInManager` and `UserManager` types from ASP.NET Core Identity to validate credentials and manage the authentication session. Much of the rest of the code is the same from the prior quickstarts and templates. ## Logging In With The Web client [Section titled “Logging In With The Web client”](#logging-in-with-the-web-client) At this point, you should be able to run all the existing clients and samples. Launch the Web client application, and you should be redirected to IdentityServer to log in. Login with one of the users created by the seed process (e.g., alice/Pass123$), and after that you will be redirected back to the Web client application where your user’s claims should be listed. ![ASP.NET Core application showing properties and claims on a ClaimsPrincipal](/_astro/aspid_claims.CuAZLh_S_2tSuUE.webp) You should also be able to go to the call api page at `https://localhost:5002/callapi` to invoke the API on behalf of the user: ![Showing claims retrieved from an API in an ASP.NET Core application](/_astro/aspid_api_claims.CdpDd3zD_Z2WLLd.webp) Congratulations, you’re using users from ASP.NET Core Identity in IdentityServer! ## Adding Custom Profile Data [Section titled “Adding Custom Profile Data”](#adding-custom-profile-data) Next you will add a custom property to your user model and include it as a claim when the appropriate Identity Resource is requested. First, add a `FavoriteColor` property in `src/IdentityServerAspNetIdentity/ApplicationUser.cs`. ```csharp public class ApplicationUser : IdentityUser { public string FavoriteColor { get; set; } } ``` Then, set the FavoriteColor of one of your test users in `SeedData.cs` ```csharp alice = new ApplicationUser { UserName = "alice", Email = "AliceSmith@email.com", EmailConfirmed = true, FavoriteColor = "red", }; ``` In the same file, add code to recreate the database when you re-seed the data, by calling `EnsureDeleted` just before `Migrate`: ```csharp var context = scope.ServiceProvider.GetService(); context.Database.EnsureDeleted(); context.Database.Migrate(); ``` Note Caution: this will destroy your test users when you make changes to them. While that is convenient for this quickstart, it is not recommended in production! Next, create an ef migration for the CustomProfileData and reseed your user database. Run the following commands from the `src/IdentityServerAspNetIdentity` directory: ```sh dotnet ef migrations add CustomProfileData dotnet run /seed ``` Now that you have more data in the database, you can use it to set claims. IdentityServer contains an extensibility point called the `IProfileService` that is responsible for retrieval of user claims. The ASP.NET Identity Integration includes an implementation of `IProfileService` that retrieves claims from ASP.NET Identity. You can extend that implementation to use the custom profile data as a source of claims data. [See here](/identityserver/reference/v8/services/profile-service/) for more details on the profile service. Create a new file called `src/IdentityServerAspNetIdentity/CustomProfileService.cs` and add the following code to it: ```csharp using Duende.IdentityServer.AspNetIdentity; using Duende.IdentityServer.Models; using IdentityServerAspNetIdentity.Models; using Microsoft.AspNetCore.Identity; using System.Security.Claims; namespace IdentityServerAspNetIdentity { public class CustomProfileService : ProfileService { public CustomProfileService(UserManager userManager, IUserClaimsPrincipalFactory claimsFactory) : base(userManager, claimsFactory) { } protected override async Task GetProfileDataAsync(ProfileDataRequestContext context, ApplicationUser user) { var principal = await GetUserClaimsAsync(user); var id = (ClaimsIdentity)principal.Identity; if (!string.IsNullOrEmpty(user.FavoriteColor)) { id.AddClaim(new Claim("favorite_color", user.FavoriteColor)); } context.AddRequestedClaims(principal.Claims); } } } ``` Register the `CustomProfileService` in `HostingExtensions.cs`: HostingExtensions.cs ```csharp builder.Services .AddIdentityServer(options => { // ... }) .AddInMemoryIdentityResources(Config.IdentityResources) .AddInMemoryApiScopes(Config.ApiScopes) .AddInMemoryClients(Config.Clients) .AddAspNetIdentity() .AddProfileService(); ``` Finally, you need to configure your application to make a request for the favorite\_color, and include that claim in your client’s configuration. Add a new `IdentityResource` in `src/IdentityServerAspNetIdentity/Config.cs` that will map the color scope onto the favorite\_color claim type: ```csharp public static IEnumerable IdentityResources => new IdentityResource[] { // ... new IdentityResource("color", new [] { "favorite_color" }) }; ``` Allow the web client to request the color scope (also in `Config.cs`): ```csharp new Client { ClientId = "web", // ... AllowedScopes = new List { IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, "api1", "color" } } ``` Finally, update the `WebClient` project so that it will request the color scope. In its `src/WebClient/Program.cs` file, add the color scope to the requested scopes, and add a claim action to map the favorite\_color into the principal: Program.cs ```csharp .AddOpenIdConnect("oidc", options => { // ... options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("offline_access"); options.Scope.Add("api1"); options.Scope.Add("color"); options.GetClaimsFromUserInfoEndpoint = true; options.ClaimActions.MapUniqueJsonKey("favorite_color", "favorite_color"); }); ``` Now restart the `IdentityServerAspNetIdentity` and `WebClient` projects, sign out and sign back in as alice, and you should see the favorite color claim. ## What’s Missing? [Section titled “What’s Missing?”](#whats-missing) The rest of the code in this template is similar to the other quickstarts and templates we provide. You will notice that this template does not include UI code for user registration, password reset, and other things you might expect from Microsoft’s templates that include ASP.NET Core Identity. Given the variety of requirements and different approaches to using ASP.NET Core Identity, our template deliberately does not provide those features. The intent of this template is to be a starting point to which you can add the features you need from ASP.NET Core Identity, customized according to your requirements. Alternatively, you can [create a new project based on the ASP.NET Core Identity template](https://docs.microsoft.com/en-us/aspnet/core/security/authentication/identity?view=aspnetcore-8.0\&tabs=netcore-cli#create-a-web-app-with-authentication) and add the IdentityServer features you have learned about in these quickstarts to that project. With that approach, you may need to configure IdentityServer so that it knows the paths to pages for user interactions. Set the LoginUrl, LogoutUrl, ConsentUrl, ErrorUrl, and DeviceVerificationUrl as needed in your `IdentityServerOptions`. ----- # Building Blazor WASM Client Applications > Learn how to build secure Blazor WebAssembly applications using the Duende BFF security framework and integrate them with IdentityServer. Blazor applications can be set up using different interactivity modes: * Static * Server * WebAssembly * Auto Projects using the static or server modes can be configured just like any other ASP.NET Core application. We covered that in the [interactive applications](/identityserver/quickstarts/2-interactive/) quickstart. Similar to JavaScript SPAs, you can build Blazor WebAssembly applications with and without a backend. Not having a backend has all the security disadvantages we discussed already in the [JavaScript quickstart](/identityserver/quickstarts/javascript-clients/). So in this quickstart we will focus on how to build a Blazor WebAssembly application using our Duende.BFF security framework. You can find the full source code [here](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/7_Blazor). The “auto” interactivity mode requires a mix of server-side authentication and authentication with a BFF. This is more complex than we want this quickstart to be. But we have a [template with annotations](https://github.com/DuendeSoftware/products/tree/main/bff/templates/src/BffBlazorAutoRenderMode) that helps with that. Before diving into that however, we recommend you first follow this quickstart first. Note To keep things simple, we will use our demo IdentityServer instance hosted at . We will provide more details on how to configure a Blazor client in your own IdentityServer at the end. ## Setting Up The Project [Section titled “Setting Up The Project”](#setting-up-the-project) The .NET CLI includes a template that sets up a standalone Blazor WebAssembly project. Create the directory where you want to work in, and run the following command: ```plaintext dotnet new blazorwasm -n BlazorWasm ``` Now create a backend that will host the BFF. ```plaintext dotnet new web -n BFF ``` And if you’re using Visual Studio or Rider, create a solution file and add the projects: ```plaintext dotnet new sln -n BlazorQuickstart dotnet sln add BlazorWasm/BlazorWasm.csproj dotnet sln add BFF/BFF.csproj ``` Open the solution in your IDE or if you use Visual Studio Code open the directory where you created the solution file. ## Configuring The BFF [Section titled “Configuring The BFF”](#configuring-the-bff) In the BFF project, add a reference to the BlazorWasm project and modify `Program.cs` as follows: Program.cs ```csharp var builder = WebApplication.CreateBuilder(args); var app = builder.Build(); app.MapStaticAssets(); app.MapFallbackToFile("index.html"); app.Run(); ``` When you run just the BFF project now, you should see the Blazor application running. The call to `MapFallbackToFile` renders the entry point of the Blazor application in the browser. It’s important that both projects run on the same site because the session cookie we’ll use has the samesite=strict flag to protect against CSRF attacks. Add the following package references to the BFF project: * [Microsoft.AspNetCore.Authentication.OpenIdConnect](https://www.nuget.org/packages/Microsoft.AspNetCore.Authentication.OpenIdConnect) * [Duende.BFF](https://www.nuget.org/packages/Duende.BFF) Next, we will add OpenID Connect and OAuth support to the BFF. For this we are adding the Microsoft OpenID Connect authentication handler for the protocol interactions with IdentityServer, and the cookie authentication handler for managing the resulting authentication session. See [here](/bff/fundamentals/session/handlers/) for more background information. The BFF services provide the logic to invoke the authentication plumbing from the frontend (more about this later). Add the following snippet to your `Program.cs` just before the call to `builder.Build();` * Duende BFF v4 Program.cs ```csharp builder.Services.AddAuthorization(); builder.Services.AddCascadingAuthenticationState(); builder.Services .AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.MapInboundClaims = false; options.ClaimActions.MapAll(); options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }) .ConfigureCookies(options => { options.Cookie.Name = "__Host-blazor"; options.Cookie.SameSite = SameSiteMode.Strict; }); ``` * Duende BFF v3 Program.cs ```csharp builder.Services.AddAuthorization(); builder.Services.AddCascadingAuthenticationState(); builder.Services.AddBff(); builder.Services .AddAuthentication(options => { options.DefaultScheme = "cookie"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("cookie", options => { options.Cookie.Name = "__Host-blazor"; options.Cookie.SameSite = SameSiteMode.Strict; }) .AddOpenIdConnect("oidc", options => { options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.ResponseMode = "query"; options.Scope.Clear(); options.Scope.Add("openid"); options.Scope.Add("profile"); options.Scope.Add("api"); options.Scope.Add("offline_access"); options.MapInboundClaims = false; options.ClaimActions.MapAll(); options.GetClaimsFromUserInfoEndpoint = true; options.SaveTokens = true; options.TokenValidationParameters.NameClaimType = "name"; options.TokenValidationParameters.RoleClaimType = "role"; }); ``` The last step is to add the required middleware for authentication, authorization and BFF session management. Add the following snippet before the call to `MapStaticAssets`: Program.cs ```csharp app.UseAuthentication(); app.UseBff(); app.UseAuthorization(); app.MapBffManagementEndpoints(); ``` Now run the BFF project again. **Be sure to use https.** Try to manually invoke the BFF login endpoint on `/bff/login` - this should bring you to the demo IdentityServer. After login (e.g. using bob/bob), the browser will return to the Blazor application. In other words, the fundamental authentication plumbing is already working. Now we need to make the frontend aware of it. ## Modifying The Frontend (Part 1) [Section titled “Modifying The Frontend (Part 1)”](#modifying-the-frontend-part-1) A couple of steps are necessary to add the security and identity plumbing to the Blazor application. *`a)`* Install the NuGet package [“Microsoft.AspNetCore.Components.WebAssembly.Authentication”](https://www.nuget.org/packages/Microsoft.AspNetCore.Components.WebAssembly.Authentication/). *`b)`* Add a using statement to `_Imports.razor` in the BlazorWasm project: ```csharp @using Microsoft.AspNetCore.Components.Authorization ``` *`c)`* To propagate the current authentication state to all pages in the Blazor client, a component called `CascadingAuthenticationState` is used. Wrap the Router component in the file `App.razor` with it: ```razor ``` *`d)`* Last but not least, we will add some conditional rendering to the layout page to be able to trigger login/logout and displaying the current user name when logged in. This is achieved by using the `AuthorizeView` component in `MainLayout.razor`. Replace the contents of the `
` with this: ```razor
Hello, @context.User.Identity.Name! Log out Log in
@Body
``` When you now run the Blazor application, you will see the following error in your browser console: ```plaintext crit: Microsoft.AspNetCore.Components.WebAssembly.Rendering.WebAssemblyRenderer[100] Unhandled exception rendering component: Cannot provide a value for property 'AuthenticationStateProvider' on type 'Microsoft.AspNetCore.Components.Authorization.CascadingAuthenticationState'. There is no registered service of type 'Microsoft.AspNetCore.Components.Authorization.AuthenticationStateProvider'. ``` `CascadingAuthenticationState` is an abstraction over an arbitrary authentication system. It internally relies on a service called `AuthenticationStateProvider` to return the required information about the current authentication state and the information about the currently logged on user. A special version of this component, aware of the BFF, has to be added, and that’s what we’ll do next. ## Modifying The Frontend (Part 2) [Section titled “Modifying The Frontend (Part 2)”](#modifying-the-frontend-part-2) The BFF library we just configured includes an endpoint that allows the Blazor application to query the current authentication session and state (see [here](/bff/fundamentals/session/management/user/)). We will now add a Blazor `AuthenticationStateProvider` that will internally use this endpoint. It is included in our NuGet package “Duende.BFF.Blazor.Client”. In the BlazorWasm.Client project: * Add the NuGet package [“Duende.BFF.Blazor.Client”](https://www.nuget.org/packages/Duende.BFF.Blazor.Client/). * In `Program.cs`, just before the call to `builder.Build().RunAsync();`, add the following code: ```csharp builder.Services.AddBffBlazorClient(); ``` If you restart the application again, the logon/logoff logic should work now. In addition, you can display the contents of the session on the main page by replacing the code in `Home.razor` with this: ```razor @page "/" Home

Hello, Blazor BFF!

@foreach (var claim in @context.User.Claims) {
@claim.Type
@claim.Value
}
``` The claims you see on the page are coming from the user endpoint on the BFF and the `AuthenticationStateProvider` we just registered with the call to `AddBffBlazorClient` takes care of polling the endpoint. ## Securing a Local API Endpoint [Section titled “Securing a Local API Endpoint”](#securing-a-local-api-endpoint) Right now the BFF project doesn’t contain any endpoints. Let’s create a simple one that will be used by the Blazor application. *`a)`* Observe the `Weather.razor` page in the BlazorWasm project. In `OnInitializedAsync`, it fetches data from a file. *`b)`* Move the file wwwwroot/sample-data/weather.json to the root of the BFF project. *`c)`* In the `Program.cs` file of the BFF project, just above the `MapFallbackToFile` call, add the following code: Program.cs ```csharp app.MapGet("/api/data", async () => { var json = await File.ReadAllTextAsync("weather.json"); return Results.Content(json, "application/json"); }).RequireAuthorization().AsBffApiEndpoint(); ``` `RequireAuthorization` is ASP.NET Core’s standard way to make sure a user is authenticated before accessing a given endpoint. `AsBffApiEndpoint` is an extension method provided by the BFF library that adds anti-forgery protection to the endpoint and returns the expected 401 response when the user is not authenticated. The anti-forgery protection consists of the requirement to include an `X-CSRF` HTTP header with each request. *`d)`* In the `Program.cs` file of the BlazorWasm project, replace the registration of the `HttpClient` with the following: Program.cs ```csharp builder.Services.AddTransient(sp => { var client = new HttpClient { BaseAddress = new Uri(builder.HostEnvironment.BaseAddress) }; client.DefaultRequestHeaders.Add("X-CSRF", "1"); return client; }); ``` Alternatively, a [handler](https://duendesoftware.com/blog/20250902-dotnet-httpclient-and-delegating-handlers) can be created and used with the `HttpClient` instance. And with this in place, the application should be able to fetch data from the API endpoint when the Weather page is shown. ## Setting Up A Blazor BFF client In IdentityServer [Section titled “Setting Up A Blazor BFF client In IdentityServer”](#setting-up-a-blazor-bff-client-in-identityserver) In essence, a BFF client is “just” a normal authorization code flow client: * use the code grant type * set a client secret * enable `AllowOfflineAccess` if you want to use refresh tokens * enable the required identity and resource scopes * set the redirect URIs for the OIDC handler Below is a typical code snippet for the client definition: ```csharp var bffClient = new Client { ClientId = "bff", ClientSecrets = { new Secret("secret".Sha256()) }, AllowedGrantTypes = GrantTypes.Code, RedirectUris = { "https://bff_host/signin-oidc" }, FrontChannelLogoutUri = "https://bff_host/signout-oidc", PostLogoutRedirectUris = { "https://bff_host/signout-callback-oidc" }, AllowOfflineAccess = true, AllowedScopes = { "openid", "profile", "remote_api" } }; ``` ----- # Securing an MCP Server with IdentityServer > Learn how to protect an MCP Server with Duende IdentityServer so AI clients can securely connect using OAuth and Dynamic Client Registration. AI agents and tools increasingly communicate via the Model Context Protocol (MCP). This quickstart shows how to secure an MCP Server with Duende IdentityServer, using [Dynamic Client Registration (DCR)](/identityserver/configuration/dcr/) so any compliant client can connect without being pre-configured. Note Each client that dynamically connects to IdentityServer using DCR counts towards the Client Ids allowed in your license. ## Quickstart Applications [Section titled “Quickstart Applications”](#quickstart-applications) We will create 3 projects. An IdentityServer project, an MCP Server, and a console client which will make requests to the MCP Server (but sign in using IdentityServer). ``` architecture-beta service mcpServer(server)[MCP Server] service client(server)[Client] service is(server)[IdentityServer] is:L -- R:mcpServer mcpServer:R -- L:client ``` We’ll start by creating a directory to store the 3 projects. ```console mkdir mcp-quickstart cd mcp-quickstart ``` ### Create the IdentityServer Project [Section titled “Create the IdentityServer Project”](#create-the-identityserver-project) The IdentityServer project needs to be configured to use DCR to allow clients at runtime. The below steps disable the static clients and adds DCR. Create a new Duende IdentityServer InMemory project to its own directory inside the `mcp-quickstart` directory you created above. ```console dotnet new duende-is-inmem --name "McpQuickStart.IdentityServer" ``` #### Add NuGet Packages [Section titled “Add NuGet Packages”](#add-nuget-packages) Add the latest versions of the `Duende.IdentityServer` and `Duende.IdentityServer.Configuration` NuGet packages to the project. The `.csproj` file should look like the below. ```xml net10.0 enable enable ``` #### Configure API Resources and Scopes [Section titled “Configure API Resources and Scopes”](#configure-api-resources-and-scopes) IdentityServer will need to know what API Scopes and Resources will be required at runtime. We will only configure a single scope, `mcp:tools`, which will be used to allow the client to access the tool hosted by the MCP Server connected application. The scope is tied to an `ApiResource` object specific to the MCP Server, meaning the client will receive a token that says the `mcp:tools` scope can only be used by the MCP Server. For more information, see [Resource Isolation](/identityserver/fundamentals/resources/isolation). To enable this, edit the `Config.cs` file to look like this: \~/mcp-quickstart/McpQuickStart.IdentityServer/Config.cs ```csharp public static class Config { public static IEnumerable IdentityResources => [ new IdentityResources.OpenId(), new IdentityResources.Profile() ]; public static IEnumerable ApiResources => [ new("https://localhost:7141", "MCP Server") { Scopes = { "mcp:tools" } } ]; public static IEnumerable ApiScopes => [ new("mcp:tools") ]; } ``` Note the configuration does not include any client registrations. MCP clients will dynamically register with IdentityServer when needed. #### Configure IdentityServer Services [Section titled “Configure IdentityServer Services”](#configure-identityserver-services) When configuring services for IdentityServer: 1. The `DiscoveryDocument` registration endpoint must be configured so that the registration endpoint is made visible, and the URL for it is inferred. You can do this by setting `options.Discovery.DynamicClientRegistration.RegistrationEndpointMode` to `RegistrationEndpointMode.Inferred`. 2. Add the API Scopes from `Config.cs`. 3. Add the API Resources from `Config.cs`. 4. Store the dynamically added clients. For this quickstart, keep them in memory. When you are done, the code for the `ConfigureServices()` method inside `HostingExtensions.cs` will look like this: \~/mcp-quickstart/McpQuickStart.IdentityServer/HostingExtensions.cs ```csharp public static WebApplication ConfigureServices(this WebApplicationBuilder builder) { builder.Services.AddRazorPages(); var isBuilder = builder.Services.AddIdentityServer(options => { // add the default dynamic client registration endpoint to the discovery/metadatada documents options.Discovery.DynamicClientRegistration.RegistrationEndpointMode = RegistrationEndpointMode.Inferred; }) .AddTestUsers(TestUsers.Users) .AddLicenseSummary(); // in-memory, code config isBuilder.AddInMemoryIdentityResources(Config.IdentityResources); isBuilder.AddInMemoryApiScopes(Config.ApiScopes); // note we're not adding static clients, they will be added dynamically at runtime through DCR isBuilder.AddInMemoryClients([]); isBuilder.AddInMemoryApiResources(Config.ApiResources); builder.Services.AddIdentityServerConfiguration(_ => { }) // in memory client store only used for sample .AddInMemoryClientConfigurationStore(); builder.Services.AddAuthentication() .AddOpenIdConnect("oidc", "Sign-in with demo.duendesoftware.com", options => { options.SignInScheme = IdentityServerConstants.ExternalCookieAuthenticationScheme; options.SignOutScheme = IdentityServerConstants.SignoutScheme; options.SaveTokens = true; options.Authority = "https://demo.duendesoftware.com"; options.ClientId = "interactive.confidential"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.TokenValidationParameters = new TokenValidationParameters { NameClaimType = "name", RoleClaimType = "role" }; }); // Add `.PersistKeysTo…()` and `.ProtectKeysWith…()` calls // See more at https://docs.duendesoftware.com/general/data-protection builder.Services.AddDataProtection() .SetApplicationName("IdentityServer"); return builder.Build(); } ``` #### Map DCR in the IdentityServer Pipeline [Section titled “Map DCR in the IdentityServer Pipeline”](#map-dcr-in-the-identityserver-pipeline) The IdentityServer middleware pipeline needs to be able to add clients dynamically. This is done with the `MapDynamicClientRegistration()` method from the `Duende.IdentityServer.Configuration` NuGet package. Add the line `app.MapDynamicClientRegistration();` inside the `ConfigurePipeline()` method. Afterwards, your `ConfigurePipeline()` method should look like: \~/mcp-quickstart/McpQuickStart.IdentityServer/HostingExtensions.cs ```csharp public static WebApplication ConfigurePipeline(this WebApplication app) { _ = app.UseSerilogRequestLogging(); if (app.Environment.IsDevelopment()) { _ = app.UseDeveloperExceptionPage(); } _ = app.UseStaticFiles(); _ = app.UseRouting(); _ = app.UseIdentityServer(); _ = app.UseAuthorization(); _ = app.MapRazorPages() .RequireAuthorization(); _ = app.MapDynamicClientRegistration(); return app; } ``` #### Update Launch Url For Local Development [Section titled “Update Launch Url For Local Development”](#update-launch-url-for-local-development) For this quickstart, we are using known URLs for each application. Force the project to self-host at `https://localhost:5001`. The `/Properties/launchSettings.json` file should look like: \~/mcp-quickstart/McpQuickStart.IdentityServer/Properties/launchSettings.json ```json { "profiles": { "https": { "commandName": "Project", "launchBrowser": true, "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, "applicationUrl": "https://localhost:5001" } } } ``` ### Create the MCP Server Project [Section titled “Create the MCP Server Project”](#create-the-mcp-server-project) The MCP Server project we create will return weather data. Start by adding a new ASP.NET Web API project to its own directory inside `mcp-quickstart` you created above. ```console dotnet new webapi --name "McpQuickStart.McpServer" ``` #### Add NuGet Packages [Section titled “Add NuGet Packages”](#add-nuget-packages-1) Add the latest versions of the `Microsoft.AspNetCore.Authentication.JwtBearer` and `ModelContextProtocol.AspNetCore` NuGet packages to the project. The `.csproj` file should look like the below. ```xml net10.0 enable enable ``` #### Configure MCP Tools [Section titled “Configure MCP Tools”](#configure-mcp-tools) Create a new directory called `McpTools` and add a `WeatherTools.cs` C# file. This will be the MCP Server definition that an AI agent or an MCP client can consume. The code for the file is below. \~/mcp-quickstart/McpQuickStart.McpServer/McpTools/WeatherTools.cs ```csharp using System.ComponentModel; using System.Globalization; using System.Text.Json; using ModelContextProtocol; using ModelContextProtocol.Server; namespace McpQuickStart.McpServer.McpTools; [McpServerToolType] public sealed class WeatherTools { private readonly IHttpClientFactory _httpClientFactory; public WeatherTools(IHttpClientFactory httpClientFactory) { _httpClientFactory = httpClientFactory; } [McpServerTool, Description("Get weather alerts for a US state.")] public async Task GetAlerts( [Description("The US state to get alerts for. Use the 2 letter abbreviation for the state (e.g. NY).")] string state) { var client = _httpClientFactory.CreateClient("WeatherApi"); using var jsonDocument = await client.GetFromJsonAsync($"/alerts/active/area/{state}") ?? throw new McpException("No JSON returned from alerts endpoint"); var alerts = jsonDocument.RootElement.GetProperty("features").EnumerateArray(); if (!alerts.Any()) { return "No active alerts for this state."; } return string.Join("\n--\n", alerts.Select(alert => { JsonElement properties = alert.GetProperty("properties"); return $""" Event: {properties.GetProperty("event").GetString()} Area: {properties.GetProperty("areaDesc").GetString()} Severity: {properties.GetProperty("severity").GetString()} Description: {properties.GetProperty("description").GetString()} Instruction: {properties.GetProperty("instruction").GetString()} """; })); } [McpServerTool, Description("Get weather forecast for a location.")] public async Task GetForecast( [Description("Latitude of the location.")] double latitude, [Description("Longitude of the location.")] double longitude) { var client = _httpClientFactory.CreateClient("WeatherApi"); var pointUrl = string.Create(CultureInfo.InvariantCulture, $"/points/{latitude},{longitude}"); using var locationDocument = await client.GetFromJsonAsync(pointUrl); var forecastUrl = locationDocument?.RootElement.GetProperty("properties").GetProperty("forecast").GetString() ?? throw new McpException($"No forecast URL provided by {client.BaseAddress}points/{latitude},{longitude}"); using var forecastDocument = await client.GetFromJsonAsync(forecastUrl); var periods = forecastDocument?.RootElement.GetProperty("properties").GetProperty("periods").EnumerateArray() ?? throw new McpException("No JSON returned from forecast endpoint"); return string.Join("\n---\n", periods.Select(period => $""" {period.GetProperty("name").GetString()} Temperature: {period.GetProperty("temperature").GetInt32()}°F Wind: {period.GetProperty("windSpeed").GetString()} {period.GetProperty("windDirection").GetString()} Forecast: {period.GetProperty("detailedForecast").GetString()} """)); } } ``` #### Configure MCP Services [Section titled “Configure MCP Services”](#configure-mcp-services) Inside the `Program.cs` file, configure the application to host an MCP Server. This is done by: 1. Calling `.AddMcp()`, then configuring the resource metadata clients will be able to access. The `ScopesSupported = ["mcp:tools"]` property states the client will only be able to request the `mcp:tools` scope from IdentityServer when the user signs in. 2. Registering the `WeatherTools` type as MCP tools the server should expose, by calling `.AddMcpServer().WithTools()`. The full code for `Program.cs` looks like: \~/mcp-quickstart/McpQuickStart.McpServer/Program.cs ```csharp using System.Net.Http.Headers; using McpQuickStart.McpServer.McpTools; using Microsoft.AspNetCore.Authentication.JwtBearer; using Microsoft.IdentityModel.Tokens; using ModelContextProtocol.AspNetCore.Authentication; var builder = WebApplication.CreateBuilder(args); var mcpServerUrl = "https://localhost:7141"; var inMemoryOAuthServerUrl = "https://localhost:5001"; builder.Services.AddAuthentication(options => { options.DefaultChallengeScheme = McpAuthenticationDefaults.AuthenticationScheme; options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.Authority = inMemoryOAuthServerUrl; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuerSigningKey = true, ValidAudience = mcpServerUrl, ValidIssuer = inMemoryOAuthServerUrl, NameClaimType = "name", RoleClaimType = "role" }; }) .AddMcp(options => { options.ResourceMetadata = new() { Resource = mcpServerUrl, ResourceDocumentation = "https://docs.example/api/weather", AuthorizationServers = { inMemoryOAuthServerUrl }, ScopesSupported = ["mcp:tools"] }; }); builder.Services.AddAuthorization(); builder.Services.AddHttpContextAccessor(); builder.Services.AddMcpServer() .WithTools() .WithHttpTransport(); builder.Services.AddHttpClient("WeatherApi", client => { client.BaseAddress = new Uri("https://api.weather.gov"); client.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue("weather-tool", "1.0")); }); var app = builder.Build(); app.UseAuthentication(); app.UseAuthorization(); app.MapMcp().RequireAuthorization(); app.Run(); ``` #### Update Launch Url For Local Development [Section titled “Update Launch Url For Local Development”](#update-launch-url-for-local-development-1) For this quickstart, we are using known URLs for each application. Force the project to self-host at `https://localhost:7141`. The `/Properties/launchSettings.json` file should look like: \~/mcp-quickstart/McpQuickStart.McpServer/Properties/launchSettings.json ```json { "profiles": { "https": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": false, "applicationUrl": "https://localhost:7141", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ``` ### Create the Client Console Application [Section titled “Create the Client Console Application”](#create-the-client-console-application) The MCP Server can be accessed by any client through HTTP requests. For this quickstart, we create a simple console application that will register itself with IdentityServer and make an authenticated request to the MCP server. Add the new project to its own directory inside the `mcp-quickstart` directory you created above. ```console dotnet new console --name "McpQuickStart.Client" ``` #### Add NuGet Packages [Section titled “Add NuGet Packages”](#add-nuget-packages-2) Add the latest versions of the `Microsoft.AspNetCore.Authentication.JwtBearer` and `ModelContextProtocol.AspNetCore` NuGet packages to the project. The `.csproj` file should look like the below. ```xml Exe net10.0 enable enable ``` #### Implement the Client [Section titled “Implement the Client”](#implement-the-client) The console client will need to: 1. Use the `ModelContextProtocol` NuGet package to create an `HttpClientTransport` object to communicate with the MCP Server. 2. Include a `RedirectUri` back to itself after the user signs in. 3. After user sign-in completes, make a call to the MCP Server using the `get_alerts` tool implemented by the MCP Server and output its response. When creating the `HttpClientTransport` object, the `new ClientOAuthOptions()` initializer needs to set the `RedirectUri` and `Scopes` properties. The `RedirectUri` is the local endpoint this console client expects a redirect to after the user signs in. Once the OAuth flow completes, IdentityServer needs to know where to redirect the user back to, and the `RedirectUri` property tells it to redirect back to this client application. The `Scopes` property uses the single scope we have configured for the clients. It was configured in IdentityServer and the MCP Server was configured to use that scope. When this client signs in, it will request and receive a token with this scope. The code for the entire console application is: \~/mcp-quickstart/McpQuickStart.Client/Program.cs ```csharp using System.Diagnostics; using System.Net; using System.Text; using System.Web; using ModelContextProtocol.Authentication; using ModelContextProtocol.Client; using ModelContextProtocol.Protocol; var mcpServerUrl = "https://localhost:7141"; Console.WriteLine("Protected MCP Client"); Console.WriteLine($"Connecting to server at {mcpServerUrl}..."); Console.WriteLine(); var httpClient = new HttpClient(); var transport = new HttpClientTransport(new HttpClientTransportOptions { Endpoint = new Uri(mcpServerUrl), Name = "Weather MCP Client", OAuth = new ClientOAuthOptions { RedirectUri = new Uri("http://localhost:1179/callback"), AuthorizationCallbackHandler = HandleAuthorizationUrlAsync, DynamicClientRegistration = new DynamicClientRegistrationOptions { ClientName = "ProtectedMcpClient" }, Scopes = ["mcp:tools"], }, }, httpClient); var client = await McpClient.CreateAsync(transport); var tools = await client.ListToolsAsync(); if (tools.Count == 0) { Console.WriteLine("No tools available on the server."); return; } Console.WriteLine($"Found {tools.Count} tools on the server."); Console.WriteLine(); if (tools.Any(t => t.Name == "get_alerts")) { Console.WriteLine("Calling get_alerts tool..."); var result = await client.CallToolAsync("get_alerts", new Dictionary { ["state"] = "NY" }); Console.WriteLine("Result: " + ((TextContentBlock)result.Content[0]).Text); Console.WriteLine(); } static async Task HandleAuthorizationUrlAsync(AuthorizationCallbackContext authContext, CancellationToken cancellationToken) { Console.WriteLine("Starting OAuth authorization flow..."); Console.WriteLine($"Opening browser to: {authContext.AuthorizationUri}"); var listenerPrefix = authContext.RedirectUri.GetLeftPart(UriPartial.Authority); if (!listenerPrefix.EndsWith("/")) listenerPrefix += "/"; using var listener = new HttpListener(); listener.Prefixes.Add(listenerPrefix); try { listener.Start(); Console.WriteLine($"Listening for OAuth callback on: {listenerPrefix}"); OpenBrowser(authContext.AuthorizationUri); var context = await listener.GetContextAsync(); var query = HttpUtility.ParseQueryString(context.Request.Url?.Query ?? string.Empty); var code = query["code"]; var state = query["state"]; var iss = query["iss"]; var error = query["error"]; string responseHtml = "

Authentication complete

You can close this window now.

"; byte[] buffer = Encoding.UTF8.GetBytes(responseHtml); context.Response.ContentLength64 = buffer.Length; context.Response.ContentType = "text/html"; context.Response.OutputStream.Write(buffer, 0, buffer.Length); context.Response.Close(); if (!string.IsNullOrEmpty(error)) { Console.WriteLine($"Auth error: {error}"); return null; } if (string.IsNullOrEmpty(code)) { Console.WriteLine("No authorization code received"); return null; } Console.WriteLine("Authorization code received successfully."); return new AuthorizationResult { Code = code, State = state, Iss = iss }; } catch (Exception ex) { Console.WriteLine($"Error getting auth code: {ex.Message}"); return null; } finally { if (listener.IsListening) listener.Stop(); } } static void OpenBrowser(Uri url) { // Validate the URI scheme - only allow safe protocols if (url.Scheme != Uri.UriSchemeHttp && url.Scheme != Uri.UriSchemeHttps) { Console.WriteLine($"Error: Only HTTP and HTTPS URLs are allowed."); return; } try { var psi = new ProcessStartInfo { FileName = url.ToString(), UseShellExecute = true }; Process.Start(psi); } catch (Exception ex) { Console.WriteLine($"Error opening browser: {ex.Message}"); Console.WriteLine($"Please manually open this URL: {url}"); } } ``` ### Run the Samples [Section titled “Run the Samples”](#run-the-samples) Start the IdentityServer and MCP Server applications, then run the console client. When prompted to sign in for the console client, use username `bob` with password `bob` to sign in. The console will output the response from the MCP Server after it self-registers. #### Sample Client Output [Section titled “Sample Client Output”](#sample-client-output) ```text Protected MCP Client Connecting to server at https://localhost:7141... Starting OAuth authorization flow... Opening browser to: https://localhost:5001/connect/authorize?client_id=AZwWIaA8ApNcB5jptVQrSaNO4hF-nbRtBz19QnKEGOI&redirect_uri=http%3a%2f%2flocalhost%3a1279%2fcallback&response_type=code&code_challenge=mfZCS1wY7EHZwUkIb50SD6dmzReczXjyDMC_GFKEHvM&code_challenge_method=S256&state=EAx8LsLDnK0gVNY8Oxw8wF0Jv6TPCu9-YlyHL7XfFHE&resource=https%3a%2f%2flocalhost%3a7141&scope=mcp%3atools+offline_access Listening for OAuth callback on: http://localhost:1279/ Authorization code received successfully. Found 2 tools on the server. Calling get_alerts tool... Result: Event: Coastal Flood Statement Area: Southern Queens; Southern Nassau Severity: Minor Description: * WHAT...Up to one half foot of inundation above ground level expected in vulnerable areas near the waterfront and shoreline. * WHERE...Southern Queens and Southern Nassau Counties. * WHEN...This evening. * IMPACTS...Brief minor flooding of the most vulnerable locations near the waterfront and shoreline. * ADDITIONAL DETAILS...Additional rounds of localized minor flooding are likely with the Wednesday Night and Thursday Night high tides. Minor coastal flooding could be a bit more widespread with the Wednesday night high tide. Instruction: Do not drive through flooded roadways. -- Event: Coastal Flood Statement Area: Southern Fairfield; Southern Westchester Severity: Minor Description: * WHAT...Up to one half foot of inundation above ground level expected in vulnerable areas near the waterfront and shoreline. * WHERE...In Connecticut, Southern Fairfield County. In New York, Southern Westchester County. * WHEN...This evening. * IMPACTS...Brief minor flooding of the most vulnerable locations near the waterfront and shoreline * ADDITIONAL DETAILS...Additional rounds of localized minor flooding are likely with the Wednesday Night and Thursday Night high tides. Minor coastal flooding could be a bit more widespread with the Wednesday night high tide. Instruction: Do not drive through flooded roadways. ``` ## Source Code [Section titled “Source Code”](#source-code) The finished source code is available in the Samples repository, and a reference implementation of this quickstart is available [here](/identityserver/samples/mcp-server). ----- # Building Browser-Based Client Applications > Overview of browser-based client application patterns and security considerations when implementing JavaScript clients with IdentityServer When building browser-based or SPA applications using javascript, there are two main styles: those with a backend and those without. Browser-based applications **with a backend** are more secure, making it the recommended style. This style uses the [“Backend For Frontend” pattern](https://duendesoftware.com/blog/20210326-bff), or “BFF” for short, which relies on the backend host to implement all the security protocol interactions with the token server. The `Duende.BFF` library is used in [this quickstart](/identityserver/quickstarts/javascript-clients/js-with-backend/) to easily support the BFF pattern. Browser-based applications **without a backend** need to do all the security protocol interactions on the client-side, including driving user authentication and token requests, session and token management, and token storage. This leads to more complex JavaScript, cross-browser incompatibilities, and a considerably higher attack surface. Since this style inherently needs to store security sensitive artifacts (like tokens) in JavaScript reachable locations, **this style is not recommended**. As the [“OAuth 2.0 for Browser-Based Apps” IETF/OAuth working group BCP document](https://www.rfc-editor.org/info/rfc10017/) says: > there is no browser API that allows to store tokens in a completely secure way. Additionally, modern browsers have recently added or are planning to add privacy features that can break some front-channel protocol interactions. See [here](/bff/#3rd-party-cookies) for more details. ----- # Browser-Based Applications With A BFF > Guide to building secure browser-based JavaScript applications using the Backend For Frontend (BFF) pattern with Duende.BFF library Note We recommend you do the quickstarts in order. If you’d like to start here, begin from a copy of the [reference implementation of Quickstart 3](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/3_AspNetCoreAndApis). Throughout this quickstart, paths are written relative to the base `quickstart` directory created in part 1, which is the root directory of the reference implementation. You will also need to [install the IdentityServer templates](/identityserver/quickstarts/0-overview/#preparation). In this quickstart, you will build a browser-based JavaScript client application with a backend. This means your application will have server-side code that supports the frontend application code. This is known as the Backend For Frontend (BFF) pattern. You will implement the BFF pattern with the help of the `Duende.BFF` library. The backend will implement all the security protocol interactions with the token server and will be responsible for management of the tokens. The client-side JavaScript authenticates with the BFF using traditional cookie authentication. This simplifies the JavaScript in the client-side, and reduces the attack surface of the application. The features that will be shown in this quickstart will allow the user to log in with IdentityServer, invoke a local API hosted in the backend (secured with cookie authentication), invoke a remote API running in a different host (secured with an access token), and logout of IdentityServer. ## New Project For The JavaScript Client And BFF [Section titled “New Project For The JavaScript Client And BFF”](#new-project-for-the-javascript-client-and-bff) Begin by creating a new project to host the JavaScript application and its BFF. A single project containing the front-end and its BFF facilitates cookie authentication - the front end and BFF need to be on the same host so that cookies will be sent from the front end to the BFF. Create a new ASP.NET Core web application and add it to the solution by running the following commands from the `src` directory: Terminal ```bash dotnet new web -n JavaScriptClient cd .. dotnet sln add ./src/JavaScriptClient ``` ### Add Additional NuGet Packages [Section titled “Add Additional NuGet Packages”](#add-additional-nuget-packages) Install NuGet packages to add BFF and OIDC support to the new project by running the following commands from the `src/JavaScriptClient` directory: Terminal ```bash dotnet add package Microsoft.AspNetCore.Authentication.OpenIdConnect dotnet add package Duende.BFF dotnet add package Duende.BFF.Yarp ``` ### Modify Hosting [Section titled “Modify Hosting”](#modify-hosting) Modify the `JavaScriptClient` project to run on `https://localhost:5003`. Its `Properties/launchSettings.json` should look like this: ```json { "$schema": "https://json.schemastore.org/launchsettings.json", "profiles": { "JavaScriptClient": { "commandName": "Project", "dotnetRunMessages": true, "launchBrowser": true, "applicationUrl": "https://localhost:5003", "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" } } } } ``` ### Add Services [Section titled “Add Services”](#add-services) In the BFF pattern, the server-side code triggers and receives OpenID Connect requests and responses. To do that, it needs the same services configured as the WebClient did in the prior [web application quickstart](/identityserver/quickstarts/3-api-access/). Additionally, the BFF services need to be added with `AddBff()`. In addition, the offline\_access scope is requested that will result in a refresh token that will be used by the BFF library to automatically refresh the access token for the remote API if needed. Add the following to `src/JavaScriptClient/Program.cs`: * Duende BFF v4 Program.cs ```csharp using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Duende.Bff.Yarp; using Microsoft.AspNetCore.Authorization; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthorization(); builder.Services .AddBff() .ConfigureOpenIdConnect(options => { options.Authority = "https://localhost:5001"; options.ClientId = "bff"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.Scope.Add("api1"); options.Scope.Add("offline_access"); options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; options.MapInboundClaims = false; }) .ConfigureCookies(options => options.Cookie.SameSite = SameSiteMode.Strict) .AddRemoteApis(); var app = builder.Build(); ``` * Duende BFF v3 Program.cs ```csharp using System.IdentityModel.Tokens.Jwt; using System.Security.Claims; using Duende.Bff.Yarp; using Microsoft.AspNetCore.Authorization; var builder = WebApplication.CreateBuilder(args); builder.Services.AddAuthorization(); builder.Services .AddBff() .AddRemoteApis(); builder.Services .AddAuthentication(options => { options.DefaultScheme = "Cookies"; options.DefaultChallengeScheme = "oidc"; options.DefaultSignOutScheme = "oidc"; }) .AddCookie("Cookies") .AddOpenIdConnect("oidc", options => { options.Authority = "https://localhost:5001"; options.ClientId = "bff"; options.ClientSecret = "secret"; options.ResponseType = "code"; options.Scope.Add("api1"); options.Scope.Add("offline_access"); options.SaveTokens = true; options.GetClaimsFromUserInfoEndpoint = true; options.MapInboundClaims = false; }); var app = builder.Build(); ``` ### Add Middleware [Section titled “Add Middleware”](#add-middleware) Similarly, the middleware pipeline for this application will resemble the WebClient, with the addition of the BFF middleware and the BFF endpoints. Continue by adding the following to `src/JavaScriptClient/Program.cs`: Program.cs ```csharp var app = builder.Build(); if (app.Environment.IsDevelopment()) { app.UseDeveloperExceptionPage(); } app.UseDefaultFiles(); app.UseStaticFiles(); app.UseRouting(); app.UseAuthentication(); app.UseBff(); app.UseAuthorization(); app.MapBffManagementEndpoints(); app.Run(); ``` ### Add HTML And JavaScript Files [Section titled “Add HTML And JavaScript Files”](#add-html-and-javascript-files) Next, add HTML and JavaScript files for your client-side application to the `wwwroot` directory in the `JavaScriptClient` project. Create that directory (`src/JavaScriptClient/wwwroot`) and add an `index.html` and an `app.js` file to it. *`index.html`* The index.html file will be the main page in your application. It contains * buttons for the user to login, logout, and call the APIs * a `
` container used to show messages to the user
* a `
  

```

*`app.js`*

The app.js file will contain the client-side code for your application.

First, add a helper function to display messages in the `
`:

```js
function log() {
  document.getElementById("results").innerText = "";


  Array.prototype.forEach.call(arguments, function (msg) {
    if (typeof msg !== "undefined") {
      if (msg instanceof Error) {
        msg = "Error: " + msg.message;
      } else if (typeof msg !== "string") {
        msg = JSON.stringify(msg, null, 2);
      }
      document.getElementById("results").innerText += msg + "\r\n";
    }
  });
}
```

Next, you can use the BFF `user` management endpoint to query if the user is logged in or not. Notice the `userClaims` variable is global; it will be needed elsewhere.

```js
let userClaims = null;


(async function () {
  var req = new Request("/bff/user", {
    headers: new Headers({
      "X-CSRF": "1",
    }),
  });


  try {
    var resp = await fetch(req);
    if (resp.ok) {
      userClaims = await resp.json();


      log("user logged in", userClaims);
    } else if (resp.status === 401) {
      log("user not logged in");
    }
  } catch (e) {
    log("error checking user status");
  }
})();
```

Next, register `click` event handlers on the buttons:

```js
document.getElementById("login").addEventListener("click", login, false);
document.getElementById("local").addEventListener("click", localApi, false);
document.getElementById("remote").addEventListener("click", remoteApi, false);
document.getElementById("logout").addEventListener("click", logout, false);
```

Next, implement the `login` and `logout` functions.

Login is simple - just redirect the user to the BFF `login` endpoint.

```js
function login() {
  window.location = "/bff/login";
}
```

Logout is more involved, as you need to redirect the user to the BFF `logout` endpoint, which requires an anti-forgery token to prevent cross site request forgery attacks. The `userClaims` that you populated earlier contain that token and the full logout URL in its `bff:logout_url` claim, so redirect to that url:

```plaintext
function logout() {
  if (userClaims) {
    var logoutUrl = userClaims.find(
      (claim) => claim.type === "bff:logout_url"
    ).value;
    window.location = logoutUrl;
  } else {
    window.location = "/bff/logout";
  }
}
```

Finally, add empty stubs for the other button event handler functions. You will implement those after you get login and logout working.

```js
async function localApi() {}


async function remoteApi() {}
```

## Add JavaScript Client Registration To IdentityServer

[Section titled “Add JavaScript Client Registration To IdentityServer”](#add-javascript-client-registration-to-identityserver)

Now that the client application is ready to go, you need to define a configuration entry in IdentityServer for the new JavaScript client.

In the IdentityServer project locate the client configuration in `src/IdentityServer/Config.cs`. Add a new `Client` to the list for your new JavaScript application. Because this client uses the BFF pattern, the configuration will be very similar to the Web client. In addition, requesting the offline\_access scope should be allowed for this client. It should have the configuration listed below:

```csharp
// JavaScript BFF client
new Client
{
    ClientId = "bff",
    ClientSecrets = { new Secret("secret".Sha256()) },


    AllowedGrantTypes = GrantTypes.Code,


    // where to redirect to after login
    RedirectUris = { "https://localhost:5003/signin-oidc" },


    // where to redirect to after logout
    PostLogoutRedirectUris = { "https://localhost:5003/signout-callback-oidc" },
    AllowOfflineAccess = true,


    AllowedScopes = new List
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        "api1"
    }
}
```

## Run And Test Login And Logout

[Section titled “Run And Test Login And Logout”](#run-and-test-login-and-logout)

At this point, you should be able to run the `JavaScriptClient` application. You should see that the user is not logged in initially.

![A simple javascript client with multiple action buttons](/_astro/jsbff_not_logged_in.CdU2CJD4_ZC2tC7.webp)

When you click the login button, you’ll be redirected to IdentityServer to login. After you log in, you’ll be redirected back to the `JavaScriptClient` application, where you’ll be signed in with the Cookies authentication scheme with your tokens saved in the session.

The app loads again, but this time it has a session cookie. So, when it makes the HTTP request to get userClaims, that cookie is included in the request. This allows the BFF middleware to authenticate the user and return user info. Once the `JavaScriptClient` application receives the response, the user should appear logged in and their claims should be displayed.

![showing claims after the login action is invoked](/_astro/jsbff_logged_in.Dmunfv6t_peHEJ.webp)

Finally, the logout button should successfully get the user logged out.

![showing the logout view on IdentityServer](/_astro/jsbff_signed_out.C6LecfKJ_Z2vDWkI.webp)

## Add API Support

[Section titled “Add API Support”](#add-api-support)

Now that you have login and logout working, you will add support to invoke both local and remote APIs.

A local API is an endpoint that is hosted in the same backend as the `JavaScriptClient` application. Local APIs are intended to be APIs that only exist to support the JavaScript frontend, typically by providing UI specific data or aggregating data from other sources. Local APIs are authenticated with the user’s session cookie.

A remote API is an API running in some other host than the `JavaScriptClient` application. This is useful for APIs that are shared by many different applications (e.g. mobile app, other web apps, etc.). Remote APIs are authenticated with an access token. Fortunately, the `JavaScriptClient` application has an access token stored in the user’s session. You will use the BFF proxy feature to accept a call from the JavaScript running in the browser authenticated with the user’s session cookie, retrieve the access token for the user from the user’s session, and then proxy the call to the remote API, sending the access token for authentication.

### Define A Local API

[Section titled “Define A Local API”](#define-a-local-api)

Local APIs can be defined using controllers or with [Minimal API Route Handlers](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis?view=aspnetcore-8.0#route-handlers). For simplicity, this quickstart uses a minimal API with its handler defined directly in `Program.cs`, but you can organize your Local APIs however you like.

Add a handler to `src/JavaScriptClient/Program.cs` for the local API:

```csharp
[Authorize]
static IResult LocalIdentityHandler(ClaimsPrincipal user)
{
    var name = user.FindFirst("name")?.Value ?? user.FindFirst("sub")?.Value;
    return Results.Json(new { message = "Local API Success!", user = name });
}
```

Note

Local APIs often make requests to remote APIs that are authorized with the user’s access token. To get the access token, call the `GetUserAccessTokenAsync` extension method on the `HttpContext`. For example: *var token = await HttpContext.GetUserAccessTokenAsync();*

### Update Routing To Accept Local And Remote API Calls

[Section titled “Update Routing To Accept Local And Remote API Calls”](#update-routing-to-accept-local-and-remote-api-calls)

Next, you need to register both the local API and the BFF proxy for the remote API in the ASP.NET Core routing system. Add the code below to the endpoint configuration code in `src/JavaScriptClient/Program.cs`.

```csharp
  app.MapBffManagementEndpoints();


  // Uncomment this for Controller support
  // app.MapControllers()
  //     .AsBffApiEndpoint();


  app.MapGet("/local/identity", LocalIdentityHandler)
      .AsBffApiEndpoint();


  app.MapRemoteBffApiEndpoint("/remote", new Uri("https://localhost:6001"))
      .WithAccessToken(RequiredTokenType.User);
```

The call to the `AsBffApiEndpoint()` fluent helper method adds BFF support to the local APIs. This includes anti-forgery protection and suppressing login redirects on authentication failures and instead returning 401 and 403 status codes under the appropriate circumstances.

`MapRemoteBffApiEndpoint()` registers the BFF proxy for the remote API and configures it to pass the user’s access token.

### Call The APIs From JavaScript

[Section titled “Call The APIs From JavaScript”](#call-the-apis-from-javascript)

Back in `src/JavaScriptClient/wwwroot/app.js`, implement the two API button event handlers like this:

```js
async function localApi() {
  var req = new Request("/local/identity", {
    headers: new Headers({
      "X-CSRF": "1",
    }),
  });


  try {
    var resp = await fetch(req);


    let data;
    if (resp.ok) {
      data = await resp.json();
    }
    log("Local API Result: " + resp.status, data);
  } catch (e) {
    log("error calling local API");
  }
}


async function remoteApi() {
  var req = new Request("/remote/identity", {
    headers: new Headers({
      "X-CSRF": "1",
    }),
  });


  try {
    var resp = await fetch(req);


    let data;
    if (resp.ok) {
      data = await resp.json();
    }
    log("Remote API Result: " + resp.status, data);
  } catch (e) {
    log("error calling remote API");
  }
}
```

The path for the local API is exactly what you set in the call to `MapGet` in `src/JavaScriptClient/Program.cs`.

The path for the remote API uses a “/remote” prefix to indicate that the BFF proxy should be used, and the remaining path is what’s then passed when invoking the remote API (“/identity” in this case).

Notice both API calls require an *‘X-CSRF’: ‘1’* header, which acts as the anti-forgery token.

Note

See the [client credentials quickstart](/identityserver/quickstarts/1-client-credentials/) for information on how to create the remote API used in the code above.

## Run And Test The API Calls

[Section titled “Run And Test The API Calls”](#run-and-test-the-api-calls)

At this point, you should be able to run the `JavaScriptClient` application and invoke the APIs. The local API should return something like this:

![showing a successful JavaScript fetch call to an API endpoint](/_astro/jsbff_local_api.D6JHzKis_2gRn2k.webp)

And the remote API should return something like this:

![showing a successful JavaScript call to a remote api hosted in BFF](/_astro/jsbff_remote_api.BmDJGtvt_Z20u7nQ.webp)

You now have the start of a JavaScript client application that uses IdentityServer for sign-in, sign-out, and authenticating calls to local and remote APIs, using `Duende.BFF`.
-----
# JavaScript Applications Without A Backend

> Learn how to build a client-side JavaScript application that interacts directly with IdentityServer for authentication and API access without a backend server.

Note

We recommend you do the quickstarts in order. If you’d like to start here, begin from a copy of the [reference implementation of Quickstart 3](https://github.com/DuendeSoftware/Samples/tree/main/IdentityServer/v8/Quickstarts/3_AspNetCoreAndApis). Throughout this quickstart, paths are written relative to the base `quickstart` directory created in part 1, which is the root directory of the reference implementation. You will also need to [install the IdentityServer templates](/identityserver/quickstarts/0-overview/#preparation).

This quickstart will show how to build a browser-based JavaScript client application without a backend. This means your application has no server-side code that can support the frontend application code, and thus all OpenID Connect/OAuth protocol interactions occur from the JavaScript code running in the browser. Also, invoking the API will be performed directly from the JavaScript in the browser.

**This design has security concerns. It is no longer recommended.** See [overview](/identityserver/quickstarts/javascript-clients/) for details. The current best practice uses the [“BFF” pattern](/identityserver/quickstarts/javascript-clients/js-with-backend/).

In this quickstart the user will log in to IdentityServer, invoke an API with an access token issued by IdentityServer, and logout of IdentityServer. All of this will be driven from the JavaScript running in the browser.

## New Project For The JavaScript Client

[Section titled “New Project For The JavaScript Client”](#new-project-for-the-javascript-client)

Create a new project for the JavaScript application. Beyond being able to serve your application’s HTML and javascript, there are no requirements on the backend. You could use anything from an empty ASP.NET Core application to a Node.js application. This quickstart will use an ASP.NET Core application.

Create a new ASP.NET Core web application and add it to the solution by running the following commands from the `src` directory:

```console
dotnet new web -n JavaScriptClient
cd ..
dotnet sln add ./src/JavaScriptClient
```

### Modify Hosting

[Section titled “Modify Hosting”](#modify-hosting)

Modify the `JavaScriptClient` project to run on `https://localhost:5003`. Its `Properties/launchSettings.json` should look like this:

```json
{
  "$schema": "https://json.schemastore.org/launchsettings.json",
  "profiles": {
    "JavaScriptClient": {
      "commandName": "Project",
      "dotnetRunMessages": true,
      "launchBrowser": true,
      "applicationUrl": "https://localhost:5003",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      }
    }
  }
}
```

### Add Static File Middleware

[Section titled “Add Static File Middleware”](#add-static-file-middleware)

Given that this project is designed to run client-side, all we need ASP.NET Core to do is to serve up the static HTML and JavaScript files that will make up our application. The static file middleware is designed to do this.

Register the static file middleware in `src/JavaScriptClient/Program.cs`. The entire file should look like this:

```csharp
var builder = WebApplication.CreateBuilder(args);
var app = builder.Build();


app.UseDefaultFiles();
app.UseStaticFiles();
app.Run();
```

This middleware will now serve up static files from the application’s `src/JavaScriptClient/wwwroot` directory. This is where we will put our HTML and JavaScript files. If that directory does not exist in your project, create it now.

### Reference oidc-client

[Section titled “Reference oidc-client”](#reference-oidc-client)

In the prior [web application quickstart](/identityserver/quickstarts/3-api-access/), we used a .NET library to handle the OpenID Connect protocol. In this quickstart, we need a similar library in the `JavaScriptClient` project, except one that works in JavaScript and is designed to run in the browser. The [oidc-client library](https://github.com/IdentityModel/oidc-client-js) is one such library. It is available via [NPM](https://github.com/IdentityModel/oidc-client-js), or as a [direct download](https://github.com/IdentityModel/oidc-client-js/tree/release/dist) from GitHub.

*`NPM`*

If you want to use NPM to download `oidc-client`, then run these commands from the `src/JavaScriptClient` directory:

```console
npm i oidc-client
copy node_modules/oidc-client/dist/* wwwroot
```

This downloads the latest `oidc-client` package locally, and then copies the relevant JavaScript files into `src/JavaScriptClient/wwwroot` so they can be served by your application.

**Manual download**

If you want to download the `oidc-client` JavaScript files manually, browse to [the GitHub repository](https://github.com/IdentityModel/oidc-client-js/tree/release/dist) and download the JavaScript files. Once downloaded, copy them into `src/JavaScriptClient/wwwroot` so they can be served by your application.

### Add HTML And JavaScript Files

[Section titled “Add HTML And JavaScript Files”](#add-html-and-javascript-files)

Next, add HTML and JavaScript files to the `src/JavaScriptClient/wwwroot` directory. You will need two HTML files and one JavaScript file (in addition to the `oidc-client.js` library). Add `index.html`, `callback.html`, and `app.js` to `wwwroot`.

*`index.html`*

This will be the main page in your application. It contains

* buttons for the user to login, logout, and call the API
* a `
` container used to show messages to the user
* `



```

*`app.js`*

This will contain the main code for your application. First, add a helper function to display messages in the `
`:

```js
function log() {
    document.getElementById("results").innerText = "";


    Array.prototype.forEach.call(arguments, function (msg) {
        if (typeof msg !== "undefined") {
            if (msg instanceof Error) {
                msg = "Error: " + msg.message;
            } else if (typeof msg !== "string") {
                msg = JSON.stringify(msg, null, 2);
            }
            document.getElementById("results").innerText += msg + "\r\n";
        }
    });
}
```

Next, add code to register `click` event handlers to the three buttons:

```js
document.getElementById("login").addEventListener("click", login, false);
document.getElementById("api").addEventListener("click", api, false);
document.getElementById("logout").addEventListener("click", logout, false);
```

Next, you will set up the `UserManager` class from the `oidc-client` library to manage the OpenID Connect protocol. It requires similar configuration that was necessary in the `WebClient` (albeit with different values). Add this code to configure and instantiate the `UserManager`:

```js
var config = {
    authority: "https://localhost:5001",
    client_id: "js",
    redirect_uri: "https://localhost:5003/callback.html",
    response_type: "code",
    scope: "openid profile api1",
    post_logout_redirect_uri: "https://localhost:5003/index.html",
};
var mgr = new Oidc.UserManager(config);
```

Next, use the `UserManager.getUser` function to determine if the user is logged into the JavaScript application. It uses a JavaScript `Promise` to return the results asynchronously. The returned `User` object has a `profile` property which contains the claims for the user. There’s also an event called `UserSignedOut` that can be handled to detect if the user signs out of the token server while the SPA application is being used (presumably in a different tab). Add this code to detect the user’s session status in the JavaScript application:

```js
mgr.events.addUserSignedOut(function () {
    log("User signed out of IdentityServer");
});


mgr.getUser().then(function (user) {
    if (user) {
        log("User logged in", user.profile);
    } else {
        log("User not logged in");
    }
});
```

Next, implement the `login`, `api`, and `logout` functions. The `UserManager` provides a `signinRedirect` to log the user in, and a `signoutRedirect` to log the user out. The `User` object that we obtained above also has an `access_token` property which can be used to authenticate to a web API. The `access_token` will be passed to the web API via the `Authorization` header with the `Bearer` scheme. Add this code to implement those three functions in your application:

```js
function login() {
    mgr.signinRedirect();
}


function api() {
    mgr.getUser().then(function (user) {
        var url = "https://localhost:6001/identity";


        var xhr = new XMLHttpRequest();
        xhr.open("GET", url);
        xhr.onload = function () {
            log(xhr.status, JSON.parse(xhr.responseText));
        };
        xhr.setRequestHeader("Authorization", "Bearer " + user.access_token);
        xhr.send();
    });
}


function logout() {
    mgr.signoutRedirect();
}
```

Note

See the [client credentials quickstart](/identityserver/quickstarts/1-client-credentials/) for information on how to create the remote API used in the code above.

*`callback.html`*

This HTML file is the designated `redirect_uri` page once the user has logged into IdentityServer. It will complete the OpenID Connect protocol sign-in handshake with IdentityServer. The code for this is all provided by the `UserManager` class we used earlier. Once the sign-in is complete, we can then redirect the user back to the main `index.html` page. Add this code to complete the signin process:

```html



    
    






```

## Add JavaScript Client Registration To IdentityServer

[Section titled “Add JavaScript Client Registration To IdentityServer”](#add-javascript-client-registration-to-identityserver)

Now that the client application is ready to go, you need to define a configuration entry in IdentityServer for the new JavaScript client.

In the IdentityServer project locate the client configuration in `src/IdentityServer/Config.cs`. Add a new `Client` to the list for your new JavaScript application. It should have the configuration listed below:

```csharp
// JavaScript Client
new Client
{
    ClientId = "js",
    ClientName = "JavaScript Client",
    AllowedGrantTypes = GrantTypes.Code,
    RequireClientSecret = false,


    RedirectUris =           { "https://localhost:5003/callback.html" },
    PostLogoutRedirectUris = { "https://localhost:5003/index.html" },
    AllowedCorsOrigins =     { "https://localhost:5003" },


    AllowedScopes =
    {
        IdentityServerConstants.StandardScopes.OpenId,
        IdentityServerConstants.StandardScopes.Profile,
        "api1"
    }
}
```

## Allowing Ajax Calls To The Web API With CORS

[Section titled “Allowing Ajax Calls To The Web API With CORS”](#allowing-ajax-calls-to-the-web-api-with-cors)

One last bit of configuration that is necessary is to configure CORS in the `Api` project. This will allow Ajax calls to be made from `https://localhost:5003` to `https://localhost:6001`.

**Configure CORS**

Add the CORS service to the dependency injection system in `src/Api/Program.cs`:

Program.cs

```csharp
builder.Services.AddCors(options =>
{
    // this defines a CORS policy called "default"
    options.AddPolicy("default", policy =>
    {
        policy.WithOrigins("https://localhost:5003")
            .AllowAnyHeader()
            .AllowAnyMethod();
    });
});
```

Then add the CORS middleware to the pipeline in `src/Api/Program.cs`.

Program.cs

```csharp
app.UseHttpsRedirection();
app.UseCors("default");
```

## Run The JavaScript Application

[Section titled “Run The JavaScript Application”](#run-the-javascript-application)

Now you should be able to run the JavaScript client application:

![Showing a user is not logged in to a JavaScript client](/_astro/jsclient_not_logged_in.Cf2cwMl__Z1P5MVv.webp)

Click the “Login” button to sign the user in. Once the user is returned back to the JavaScript application, you should see their profile information:

![Showing claims after a client has logged in](/_astro/jsclient_logged_in.4rNpJI6d_jlHrN.webp)

And click the “API” button to invoke the web API:

![Showing the API results from a JavaScript fetch](/_astro/jsclient_api_results.rd6LEavw_ZJdfFC.webp)

And finally click “Logout” to sign the user out.

![Showing the IdentityServer Logged Out view](/_astro/jsclient_signed_out.DI52Y-Hj_HinB.webp)

You now have the start of a JavaScript client application that uses IdentityServer for sign-in, sign-out, and authenticating calls to web APIs.

Note

Some browsers limit cross-site interactions (especially in iframes). In Safari, Firefox, or Brave you will notice that some important features will not work such as silent token renewal and check session monitoring.
-----
# Models

> Reference documentation for the models and interfaces used in Dynamic Client Registration (DCR), including request/response objects and validation context.

## DynamicClientRegistrationRequest

[Section titled “DynamicClientRegistrationRequest”](#dynamicclientregistrationrequest)

Represents a dynamic client registration request. The parameters that are supported include a subset of the parameters [defined by IANA](https://www.iana.org/assignments/oauth-parameters/oauth-parameters.xhtml#client-metadata), and custom properties needed by IdentityServer.

```csharp
public class DynamicClientRegistrationRequest
```

#### Public Members

[Section titled “Public Members”](#public-members)

| name                                                  | description                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| ----------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `AbsoluteRefreshTokenLifetime { get; set; }`          | The absolute lifetime of refresh tokens, in seconds. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `AccessTokenLifetime { get; set; }`                   | The lifetime of access tokens, in seconds. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `AccessTokenType { get; set; }`                       | The type of access tokens this client will create. Either “Jwt” or “Reference”. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `AllowedCorsOrigins { get; set; }`                    | List of allowed CORS origins for JavaScript clients. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `AllowedIdentityTokenSigningAlgorithms { get; set; }` | List of signing algorithms to use when signing identity tokens. If not set, will use the server’s default signing algorithm. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `AllowRememberConsent { get; set; }`                  | Boolean value specifying whether a user’s consent can be remembered in flows initiated by this client. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |
| `AuthorizationCodeLifetime { get; set; }`             | The lifetime of authorization codes, in seconds. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                |
| `BackChannelLogoutSessionRequired { get; set; }`      | Boolean value specifying whether the RP requires that a `sid` (session ID) claim should be included in the Logout Token to identify the RP session with the OP when using the `backchannel_logout_uri`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `BackChannelLogoutUri { get; set; }`                  | RP URL that will cause the RP to log itself out when receiving a Logout Token from the OP.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 |
| `ClientName { get; set; }`                            | Human-readable string name of the client to be presented to the end-user during authorization.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `ClientUri { get; set; }`                             | Web page providing information about the client.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                           |
| `ConsentLifetime { get; set; }`                       | The lifetime of consent, in seconds. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `CoordinateLifetimeWithUserSession { get; set; }`     | When enabled, the client’s token lifetimes (e.g. refresh tokens) will be tied to the user’s session lifetime. This means when the user logs out, any revokable tokens will be removed. When using server-side sessions, expired sessions will also remove any revokable tokens, and backchannel logout will be triggered. This client’s setting overrides the global `CoordinateClientLifetimesWithUserSession` configuration setting. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                          |
| `DefaultMaxAge { get; set; }`                         | Default maximum authentication age. This is stored as the `UserSsoLifetime` property of the IdentityServer client model.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `EnableLocalLogin { get; set; }`                      | Boolean value specifying if local logins are enabled when this client uses interactive flows. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                   |
| `Extensions { get; set; }`                            | Custom client metadata fields to include in the serialization.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `FrontChannelLogoutSessionRequired { get; set; }`     | Boolean value specifying whether the RP requires that a `sid` (session ID) query parameter should be included to identify the RP session with the OP when using the `frontchannel_logout_uri`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `FrontChannelLogoutUri { get; set; }`                 | RP URL that will cause the RP to log itself out when rendered in an iframe by the OP.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `GrantTypes { get; set; }`                            | List of OAuth 2.0 grant type strings that the client can use at the token endpoint. Valid values are `"authorization_code"`, `"client_credentials"`, `"refresh_token"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `IdentityProviderRestrictions { get; set; }`          | List of external IdPs that can be used with this client. If the list is empty, all IdPs are allowed. Defaults to empty. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `IdentityTokenLifetime { get; set; }`                 | The lifetime of identity tokens, in seconds. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `InitiateLoginUri { get; set; }`                      | URI using the https scheme that a third party can use to initiate a login by the relying party.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `Jwks { get; set; }`                                  | JWK Set document which contains the client’s public keys. The `JwksUri` and `Jwks` parameters MUST NOT both be present in the same request or response.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `JwksUri { get; set; }`                               | URL to a JWK Set document which contains the client’s public keys. The `JwksUri` and `Jwks` parameters MUST NOT both be present in the same request or response. The default validator must be extended to make use of the `JwksUri`. The default implementation ignores this property.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `LogoUri { get; set; }`                               | Logo for the client. If present, the server should display this image to the end-user during approval.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                     |
| `PostLogoutRedirectUris { get; set; }`                | List of post-logout redirection URIs for use in the end session endpoint.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `RedirectUris { get; set; }`                          | List of redirection URI strings for use in redirect-based flows such as the authorization code flow. Clients using flows with redirection must register their redirection URI values.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `RefreshTokenExpiration { get; set; }`                | The type of expiration for refresh tokens. Either `"sliding"` or `"absolute"`. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `RefreshTokenUsage { get; set; }`                     | The usage type for refresh tokens. Either `"OneTimeOnly"` or `"ReUse"`. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         |
| `RequireClientSecret { get; set; }`                   | Boolean value specifying if a client secret is needed to request tokens at the token endpoint. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  |
| `RequireConsent { get; set; }`                        | Boolean value specifying whether consent is required in user-centric flows initiated by this client. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            |
| `RequireSignedRequestObject { get; set; }`            | Boolean value specifying whether authorization requests must be protected as signed request objects and provided through either the request or request\_uri parameters.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    |
| `Scope { get; set; }`                                 | String containing a space-separated list of scope values that the client can use when requesting access tokens. If omitted, the configuration API will register a client with the scopes set by the `DynamicClientRegistrationValidator.SetDefaultScopes` method, which defaults to no scopes.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `SlidingRefreshTokenLifetime { get; set; }`           | The sliding lifetime of refresh tokens, in seconds. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             |
| `SoftwareId { get; set; }`                            | A unique identifier string (e.g., a Universally Unique Identifier (UUID)) assigned by the client developer or software publisher used by registration endpoints to identify the client software to be dynamically registered. Unlike `"client_id"`, which is issued by the authorization server and SHOULD vary between instances, the `"software_id"` SHOULD remain the same for all instances of the client software. The `"software_id"` SHOULD remain the same across multiple updates or versions of the same piece of software. The value of this field is not intended to be human-readable and is usually opaque to the client and authorization server. The default validator must be extended to make use of the `SoftwareId`. The default implementation ignores this property. |
| `SoftwareStatement { get; set; }`                     | A software statement containing client metadata values about the client software as claims. This is a string value containing the entire signed JWT. The default validator must be extended to make use of the software statement. The default implementation ignores this property.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       |
| `SoftwareVersion { get; set; }`                       | A version identifier string for the client software identified by `"software_id"`. The value of the `"software_version"` SHOULD change on any update to the client software identified by the same `"software_id"`. The value of this field is intended to be compared using string equality matching and no other comparison semantics are defined by this specification. The default validator must be extended to make use of the `SoftwareVersion`. The default implementation ignores this property.                                                                                                                                                                                                                                                                                  |
| `TokenEndpointAuthenticationMethod { get; set; }`     | Requested Client Authentication method for the Token Endpoint. The supported options are `"client_secret_post"`, `"client_secret_basic"`, `"client_secret_jwt"`, `"private_key_jwt"`.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                      |
| `UpdateAccessTokenClaimsOnRefresh { get; set; }`      | Boolean value specifying whether access token claims are updated during token refresh. This property is an extension to the Dynamic Client Registration Protocol.                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                          |

## DynamicClientRegistrationResponse

[Section titled “DynamicClientRegistrationResponse”](#dynamicclientregistrationresponse)

Represents the response to a successful dynamic client registration request. This class extends the registration request by adding additional properties that are generated server side and not set by the client.

```csharp
public class DynamicClientRegistrationResponse : DynamicClientRegistrationRequest, IDynamicClientRegistrationResponse
```

#### Public Members

[Section titled “Public Members”](#public-members-1)

| name                                  | description                                                                                        |
| ------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `ClientId { get; set; }`              | Gets or sets the client ID.                                                                        |
| `ClientSecret { get; set; }`          | Gets or sets the client secret.                                                                    |
| `ClientSecretExpiresAt { get; set; }` | Gets or sets the expiration time of the client secret.                                             |
| `ResponseTypes { get; set; }`         | List of the OAuth 2.0 response type strings that the client can use at the authorization endpoint. |

## DynamicClientRegistrationContext

[Section titled “DynamicClientRegistrationContext”](#dynamicclientregistrationcontext)

Represents the context of a dynamic client registration request, including the original DCR request, the client model that is built up through validation and processing, the caller who made the DCR request, and other contextual information.

```csharp
public class DynamicClientRegistrationContext
```

#### Public Members

[Section titled “Public Members”](#public-members-2)

| name                    | description                                                                                                                                               |
| ----------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Caller { get; set; }`  | The ClaimsPrincipal that made the DCR request.                                                                                                            |
| `Client { get; set; }`  | The client model that is built up through validation and processing.                                                                                      |
| `Items { get; set; }`   | A collection where additional contextual information may be stored. This is intended as a place to pass additional custom state between validation steps. |
| `Request { get; set; }` | The original dynamic client registration request.                                                                                                         |

## DynamicClientRegistrationError

[Section titled “DynamicClientRegistrationError”](#dynamicclientregistrationerror)

Represents an error that occurred during validation of a dynamic client registration request. This class implements the appropriate [marker interfaces](#marker-interfaces) so that it can be returned from various points in the validator or processor.

```csharp
public class DynamicClientRegistrationValidationError : IStepResult, IDynamicClientRegistrationResponse, IDynamicClientRegistrationValidationResult
```

#### Public Members

[Section titled “Public Members”](#public-members-3)

| name                             | description                                                                                                                                                                         |
| -------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Error { get; set; }`            | Gets or sets the error code for the error that occurred during validation. Error codes defined by RFC 7591 are defined as constants in the `DynamicClientRegistrationErrors` class. |
| `ErrorDescription { get; set; }` | Gets or sets a human-readable description of the error that occurred during validation.                                                                                             |

## Marker Interfaces

[Section titled “Marker Interfaces”](#marker-interfaces)

#### IDynamicClientRegistrationResponse

[Section titled “IDynamicClientRegistrationResponse”](#idynamicclientregistrationresponse)

Marker interface for the response to a dynamic client registration request. This interface has two implementations; [`DynamicClientRegistrationResponse`](#dynamicclientregistrationresponse) indicates success, while [`DynamicClientRegistrationError`](#dynamicclientregistrationerror) indicates failure.

#### IDynamicClientRegistrationValidationResult

[Section titled “IDynamicClientRegistrationValidationResult”](#idynamicclientregistrationvalidationresult)

Marker interface for the result of validating a dynamic client registration request. This interface has two implementations; [`DynamicClientRegistrationValidatedRequest`](#successfulstep) indicates success, while [`DynamicClientRegistrationError`](#dynamicclientregistrationerror) indicates failure. Note that the `DynamicClientRegistrationError` implements multiple interfaces and can be used throughout the pipeline to convey errors.

#### IStepResult

[Section titled “IStepResult”](#istepresult)

Marker interface for the result of a step in the dynamic client registration validator or processor. This interface has two implementations; [`SuccessfulStep`](#successfulstep) indicates success, while [`DynamicClientRegistrationError`](#dynamicclientregistrationerror) indicates failure. Note that the `DynamicClientRegistrationError` implements multiple interfaces and can be used throughout the pipeline to convey errors.

### IStepResult Convenience Functions

[Section titled “IStepResult Convenience Functions”](#istepresult-convenience-functions)

Your validation or processing steps can return a call to convenience functions in the static class `StepResult` to conveniently construct a success or failure from a step wrapped in a task.

| name                                                                      | description                                                                                                                            |
| ------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------- |
| `static Task Success()`                                      | Indicates that the validation step was completed was completed successfully                                                            |
| `static Task Failure(string errorDescription)`               | Indicates that the validation step failed with the specified error description and the default error code of invalid\_client\_metadata |
| `static Task Failure(string errorDescription, string error)` | Indicates that the validation step failed with the specified error description and error code                                          |

## DynamicClientRegistrationValidatedRequest

[Section titled “DynamicClientRegistrationValidatedRequest”](#dynamicclientregistrationvalidatedrequest)

Represents a successfully validated dynamic client registration request.

```csharp
public class DynamicClientRegistrationValidatedRequest : DynamicClientRegistrationValidationResult
```

## SuccessfulStep

[Section titled “SuccessfulStep”](#successfulstep)

Represents a successful validation step.

```csharp
public class SuccessfulStep : IStepResult
```
-----
# Options

> Reference documentation for the IdentityServer configuration options related to dynamic client registration and secret lifetimes.

The page describes the `IdentityServerConfigurationOptions` class, which provides top-level configuration options for IdentityServer, including the `DynamicClientRegistrationOptions` class for managing dynamic client registration and secret lifetimes.

## IdentityServerConfigurationOptions

[Section titled “IdentityServerConfigurationOptions”](#identityserverconfigurationoptions)

Top-level options for IdentityServer configuration.

```csharp
public class IdentityServerConfigurationOptions
```

### Public Members

[Section titled “Public Members”](#public-members)

| name                                                                           | description                             |
| ------------------------------------------------------------------------------ | --------------------------------------- |
| [`DynamicClientRegistration { get; set; }`](#dynamicclientregistrationoptions) | Options for Dynamic Client Registration |

## DynamicClientRegistrationOptions

[Section titled “DynamicClientRegistrationOptions”](#dynamicclientregistrationoptions)

Options for dynamic client registration.

```csharp
public class DynamicClientRegistrationOptions
```

### Public Members

[Section titled “Public Members”](#public-members-1)

| name                           | description                                                                                                                                               |
| ------------------------------ | --------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `SecretLifetime { get; set; }` | Gets or sets the lifetime of secrets generated for clients. If unset, generated secrets will have no expiration. Defaults to null (secrets never expire). |
-----
# Request Processing

> Understand how dynamic client registration requests are processed, including client ID and secret generation, through the IDynamicClientRegistrationRequestProcessor contract and its default implementation.

The page explains the `IDynamicClientRegistrationRequestProcessor` contract, its default implementation ( `DynamicClientRegistrationRequestProcessor`), and the steps involved in processing a dynamic client registration request, including methods for generating client IDs, secrets, and customizing secret generation.

## IDynamicClientRegistrationRequestProcessor

[Section titled “IDynamicClientRegistrationRequestProcessor”](#idynamicclientregistrationrequestprocessor)

The `IDynamicClientRegistrationValidator` is the contract for the service that processes a dynamic client registration request. It contains a single `ProcessAsync(...)` method.

Conceptually, the request processing step is responsible for setting properties on the `Client` model that are generated by the Configuration API itself. In contrast, the `IDynamicClientRegistrationRequestProcessor` is responsible for checking the validity of the metadata supplied in the registration request, and using that metadata to set properties of a `Client` model. The request processor is also responsible for passing the finished `Client` to the [store](/identityserver/reference/v7/dcr/store/)

### Members

[Section titled “Members”](#members)

| name              | description                                                                                                                                                                                 |
| ----------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ProcessAsync(…)` | Processes a valid dynamic client registration request, setting properties of the client that are not specified in the request, and storing the new client in the IClientConfigurationStore. |

## DynamicClientRegistrationRequestProcessor

[Section titled “DynamicClientRegistrationRequestProcessor”](#dynamicclientregistrationrequestprocessor)

The `DynamicClientRegistrationRequestProcessor` is the default implementation of the `IDynamicClientRegistrationRequestProcessor`. If you need to customize some aspect of Dynamic Client Registration request processing, we recommend that you extend this class and override the appropriate virtual methods.

```csharp
public class DynamicClientRegistrationRequestProcessor : IDynamicClientRegistrationRequestProcessor
```

## Request Processing Steps

[Section titled “Request Processing Steps”](#request-processing-steps)

Each of these virtual methods represents one step of request processing. Each step is passed a [DynamicClientRegistrationContext](/identityserver/reference/v7/dcr/models/#dynamicclientregistrationcontext) and returns a task that returns an [`IStepResult`](/identityserver/reference/v7/dcr/models/#istepresult). The `DynamicClientRegistrationContext` includes the client model that will have its properties set, the DCR request, and other contextual information. The `IStepResult` either represents that the step succeeded or failed.

| name                      | description                                                               |
| ------------------------- | ------------------------------------------------------------------------- |
| `virtual AddClientId`     | Generates a client ID and adds it to the validatedRequest’s client model. |
| `virtual AddClientSecret` | Adds a client secret to a dynamic client registration request.            |

## Secret Generation

[Section titled “Secret Generation”](#secret-generation)

The `AddClientSecret` method is responsible for adding the client’s secret and plaintext of that secret to the context’s `Items` dictionary for later use. If you want to customize secret generation, you can override the GenerateSecret method, which only needs to return a tuple containing the secret and its plaintext.

| name                     | description                                                   |
| ------------------------ | ------------------------------------------------------------- |
| `virtual GenerateSecret` | Generates a secret for a dynamic client registration request. |
-----
# Response Generation

> Reference documentation for dynamic client registration response generation, including interfaces and implementations for handling HTTP responses in the registration process.

## IDynamicClientRegistrationResponseGenerator

[Section titled “IDynamicClientRegistrationResponseGenerator”](#idynamicclientregistrationresponsegenerator)

The `IDynamicClientRegistrationResponseGenerator` interface defines the contract for a service that generates dynamic client registration responses.

```csharp
public interface IDynamicClientRegistrationResponseGenerator
```

### Members

[Section titled “Members”](#members)

| name                       | description                                                              |
| -------------------------- | ------------------------------------------------------------------------ |
| `WriteBadRequestError(…)`  | Writes a bad request error to the HTTP context.                          |
| `WriteContentTypeError(…)` | Writes a content type error to the HTTP response.                        |
| `WriteProcessingError(…)`  | Writes a processing error to the HTTP context.                           |
| `WriteResponse(…)`         | Writes a response object to the HTTP context with the given status code. |
| `WriteSuccessResponse(…)`  | Writes a success response to the HTTP context.                           |
| `WriteValidationError(…)`  | Writes a validation error to the HTTP context.                           |

## DynamicClientRegistrationResponseGenerator

[Section titled “DynamicClientRegistrationResponseGenerator”](#dynamicclientregistrationresponsegenerator)

The `DynamicClientRegistrationResponseGenerator` is the default implementation of the `IDynamicClientRegistrationResponseGenerator`. If you wish to customize a particular aspect of response generation, you can extend this class and override the appropriate methods. You can also set JSON serialization options by overriding its `SerializerOptions` property.

### Members

[Section titled “Members”](#members-1)

| name                              | description                                         |
| --------------------------------- | --------------------------------------------------- |
| `SerializerOptions { get; set; }` | The options used for serializing json in responses. |
-----
# Store

> Reference documentation for the Dynamic Client Registration (DCR) store interfaces and implementations used to manage client configurations in IdentityServer

## IClientConfigurationStore

[Section titled “IClientConfigurationStore”](#iclientconfigurationstore)

The `IClientConfigurationStore` interface defines the contract for a service that communicates with the client configuration data store. It contains a single `AddAsync` method.

```csharp
public interface IClientConfigurationStore
```

### Members

[Section titled “Members”](#members)

| name          | description                               |
| ------------- | ----------------------------------------- |
| `AddAsync(…)` | Adds a client to the configuration store. |

## ClientConfigurationStore

[Section titled “ClientConfigurationStore”](#clientconfigurationstore)

The `ClientConfigurationStore` is the default implementation of the `IClientConfigurationStore`. It uses Entity Framework to communicate with the client configuration store, and is intended to be used when IdentityServer is configured to use the Entity Framework based configuration stores.
-----
# Validation

> Reference documentation for Dynamic Client Registration (DCR) validation process, including validation steps, interfaces, and client property configuration.

## IDynamicClientRegistrationValidator

[Section titled “IDynamicClientRegistrationValidator”](#idynamicclientregistrationvalidator)

The `IDynamicClientRegistrationValidator` is the contract for the service that validates a dynamic client registration request. It contains a single `ValidateAsync(...)` method.

Conceptually, the validation step is responsible for checking the validity of the metadata supplied in the registration request, and using that metadata to set properties of a `Client` model. In contrast, the `IDynamicClientRegistrationRequestProcessor` is responsible for setting properties on the `Client` model that are generated by the Configuration API itself.

### IDynamicClientRegistrationValidator.ValidateAsync

[Section titled “IDynamicClientRegistrationValidator.ValidateAsync”](#idynamicclientregistrationvalidatorvalidateasync)

Validates a dynamic client registration request.

```csharp
public Task ValidateAsync(
    DynamicClientRegistrationContext context)
```

| parameter | description                                   |
| --------- | --------------------------------------------- |
| `context` | Contextual information about the DCR request. |

### Return Value

[Section titled “Return Value”](#return-value)

A task that returns an [`IDynamicClientRegistrationValidationResult`](/identityserver/reference/v7/dcr/models/#idynamicclientregistrationvalidationresult), indicating success or failure.

## DynamicClientRegistrationValidator

[Section titled “DynamicClientRegistrationValidator”](#dynamicclientregistrationvalidator)

```csharp
public class DynamicClientRegistrationValidator : IDynamicClientRegistrationValidator
```

The `DynamicClientRegistrationValidator` class is the default implementation of the `IDynamicClientRegistrationValidator`. If you need to customize some aspect of Dynamic Client Registration validation, we recommend that you extend this class and override the appropriate methods.

## Validation Steps

[Section titled “Validation Steps”](#validation-steps)

Each of these methods represents one step in the validation process. Each step is passed a [`DynamicClientRegistrationContext`](/identityserver/reference/v7/dcr/models/#dynamicclientregistrationcontext) and returns a task that returns an [`IStepResult`](/identityserver/reference/v7/dcr/models/#istepresult). The `DynamicClientRegistrationContext` includes the client model that will have its properties set, the DCR request, and other contextual information. The `IStepResult` either represents that the step succeeded or failed.

The steps are invoked in the same order as they appear in this table.

| name                                | description                                                                                                                                                                                                                                                                                                     |
| ----------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `ValidateSoftwareStatementAsync(…)` | Validates the software statement of the request. The default implementation does nothing, and is included as an extension point.                                                                                                                                                                                |
| `SetGrantTypesAsync(…)`             | Validates requested grant types and uses them to set the allowed grant types of the client.                                                                                                                                                                                                                     |
| `SetRedirectUrisAsync(…)`           | Validates requested redirect URIs and uses them to set the redirect URIs of the client.                                                                                                                                                                                                                         |
| `SetScopesAsync(…)`                 | Validates requested scopes and uses them to set the scopes of the client.                                                                                                                                                                                                                                       |
| `SetDefaultScopes(…)`               | Sets scopes on the client when no scopes are requested. The default implementation sets no scopes and is intended as an extension point.                                                                                                                                                                        |
| `SetSecretsAsync(…)`                | Validates the requested JSON Web Key set to set the secrets of the client.                                                                                                                                                                                                                                      |
| `SetClientNameAsync(…)`             | Validates the requested client name uses it to set the name of the client.                                                                                                                                                                                                                                      |
| `SetLogoutParametersAsync(…)`       | Validates the requested client parameters related to logout and uses them to set the corresponding properties in the client. Those parameters include the post logout redirect URIs, front channel and back channel URIs, and flags for the front and back channel URIs indicating if they require session ids. |
| `SetMaxAgeAsync(…)`                 | Validates the requested default max age and uses it to set the user SSO lifetime of the client.                                                                                                                                                                                                                 |
| `SetUserInterfaceProperties(…)`     | Validates details of the request that control the user interface, including the logo URI, client URI, initiate login URI, enable local login flag, and identity provider restrictions, and uses them to set the corresponding client properties.                                                                |
| `SetPublicClientProperties(…)`      | Validates the requested client parameters related to public clients and uses them to set the corresponding properties in the client. Those parameters include the require client secret flag and the allowed CORS origins.                                                                                      |
| `SetAccessTokenProperties(…)`       | Validates the requested client parameters related to access tokens and uses them to set the corresponding properties in the client. Those parameters include the allowed access token type and access token lifetime.                                                                                           |
| `SetIdTokenProperties(…)`           | Validates the requested client parameters related to id tokens and uses them to set the corresponding properties in the client. Those parameters include the id token lifetime and the allowed id token signing algorithms.                                                                                     |
| `SetServerSideSessionProperties(…)` | Validates the requested client parameters related to server side sessions and uses them to set the corresponding properties in the client. Those parameters include the coordinate lifetime with user session flag.                                                                                             |
-----
# Dependency Injection Extension Methods

> A comprehensive guide to IdentityServer's dependency injection extension methods for configuring services, stores, caching, signing keys and other features.

`AddIdentityServer` return a builder object that provides many extension methods to add IdentityServer specific services to the ASP.NET Core service provider. Here’s a list grouped by feature areas.

Program.cs

```csharp
var idsvrBuilder = builder.Services.AddIdentityServer();
```

Note

Many of the fundamental configuration settings can be set on the options. See the `[IdentityServerOptions](/identityserver/reference/v7/options)` reference for more details.

## Configuration Stores

[Section titled “Configuration Stores”](#configuration-stores)

Several convenience methods are provided for registering custom stores:

* **`AddClientStore`**

  Registers a custom `IClientStore` implementation.

* **`AddCorsPolicyService`**

  Registers a custom `ICorsPolicyService` implementation.

* **`AddResourceStore`**

  Registers a custom `IResourceStore` implementation.

* **`AddIdentityProviderStore`**

  Registers a custom `IIdentityProviderStore` implementation.

The [in-memory configuration stores](/identityserver/data/providers/in-memory/) can be registered in DI with the following extension methods.

* **`AddInMemoryClients`**

  Registers `IClientStore` and `ICorsPolicyService` implementations based on the in-memory collection of `Client` configuration objects.

* **`AddInMemoryIdentityResources`**

  Registers `IResourceStore` implementation based on the in-memory collection of `IdentityResource` configuration objects.

* **`AddInMemoryApiScopes`**

  Registers `IResourceStore` implementation based on the in-memory collection of `ApiScope` configuration objects.

* **`AddInMemoryApiResources`**

  Registers `IResourceStore` implementation based on the in-memory collection of `ApiResource` configuration objects.

## Caching Configuration Data

[Section titled “Caching Configuration Data”](#caching-configuration-data)

Extension methods to enable [caching for configuration data](/identityserver/data/configuration/#caching-configuration-data):

* **`AddInMemoryCaching`**

  To use any of the caches described below, an implementation of `ICache` must be registered in the ASP.NET Core service provider. This API registers a default in-memory implementation of `ICache` that’s based on ASP.NET Core’s `MemoryCache`.

* **`AddClientStoreCache`** Registers a `IClientStore` decorator implementation which will maintain an in-memory cache of `Client` configuration objects. The cache duration is configurable on the `Caching` configuration options on the `IdentityServerOptions`.

* **`AddResourceStoreCache`**

  Registers a `IResourceStore` decorator implementation which will maintain an in-memory cache of `IdentityResource` and `ApiResource` configuration objects. The cache duration is configurable on the `Caching` configuration options on the `IdentityServerOptions`.

* **`AddCorsPolicyCache`**

  Registers a `ICorsPolicyService` decorator implementation which will maintain an in-memory cache of the results of the CORS policy service evaluation. The cache duration is configurable on the `Caching` configuration options on the `IdentityServerOptions`.

* **`AddIdentityProviderStoreCache`**

  Registers a `IIdentityProviderStore` decorator implementation which will maintain an in-memory cache of `IdentityProvider` configuration objects. The cache duration is configurable on the `Caching` configuration options on the `IdentityServerOptions`.

## Test Stores

[Section titled “Test Stores”](#test-stores)

The `TestUser` class models a user, their credentials, and claims in IdentityServer.

Use of `TestUser` is similar to the use of the “in-memory” stores in that it is intended for when prototyping, developing, and/or testing. The use of `TestUser` is not recommended in production.

* **`AddTestUsers`**

  Registers `TestUserStore` based on a collection of `TestUser` objects. `TestUserStore` is e.g. used by the default quickstart UI. Also registers implementations of `IProfileService` and `IResourceOwnerPasswordValidator` that uses the test users as a backing store.

## Signing keys

[Section titled “Signing keys”](#signing-keys)

Duende IdentityServer needs key material to sign tokens. This key material can either be created and [managed automatically](/identityserver/fundamentals/key-management/#automatic-key-management) or [configured statically](/identityserver/fundamentals/key-management/#static-key-management).

Note

We recommend that you use automatic key management. This section covers the configuration methods needed for manual configuration of signing keys, which are usually only needed if your license does not include automatic key management or if you are [migrating](/identityserver/fundamentals/key-management/#migrating-from-static-keys-to-automatic-key-management) from manually managed keys to automatic key management.

Duende IdentityServer supports X.509 certificates (both raw files and a reference to the certificate store), RSA keys and EC keys for token signatures and validation. Each key can be configured with a (compatible) signing algorithm, e.g. RS256, RS384, RS512, PS256, PS384, PS512, ES256, ES384 or ES512.

You can configure the key material with the following methods:

* **`AddSigningCredential`**

  Adds a signing key that provides the specified key material to the various token creation/validation services.

* **`AddDeveloperSigningCredential`**

  Creates temporary key material at startup time. This is for dev scenarios. The generated key will be persisted in the local directory by default (or just kept in memory).

* **`AddValidationKey`**

  Adds a key for validating tokens. They will be used by the internal token validator and will show up in the discovery document.

## Additional services

[Section titled “Additional services”](#additional-services)

The following are convenient to add additional features to your IdentityServer.

* **`AddExtensionGrantValidator`**

  Adds an `IExtensionGrantValidator` implementation for use with extension grants.

* **`AddSecretParser`**

  Adds an `ISecretParser` implementation for parsing client or API resource credentials.

* **`AddSecretValidator`**

  Adds an `ISecretValidator` implementation for validating client or API resource credentials against a credential store.

* **`AddResourceOwnerValidator`**

  Adds an `IResourceOwnerPasswordValidator` implementation for validating user credentials for the resource owner password credentials grant type.

* **`AddProfileService`**

  Adds an`IProfileService` implementation. The default implementation (found in `DefaultProfileService`) relies upon the authentication cookie as the only source of claims for issuing in tokens.

* **`AddAuthorizeInteractionResponseGenerator`**

  Adds an `IAuthorizeInteractionResponseGenerator` implementation to customize logic at authorization endpoint for when a user must be shown a UI for error, login, consent, or any other custom page. The default implementation can be found in the `AuthorizeInteractionResponseGenerator` class, so consider deriving from this existing class if you need to augment the existing behavior.

* **`AddCustomAuthorizeRequestValidator`**

  Adds an `ICustomAuthorizeRequestValidator` implementation to customize request parameter validation at the authorization endpoint.

* **`AddCustomTokenRequestValidator`**

  Adds an `ICustomTokenRequestValidator` implementation to customize request parameter validation at the token endpoint.

* **`AddRedirectUriValidator`**

  Adds an `IRedirectUriValidator` implementation to customize redirect URI validation.

* **`AddAppAuthRedirectUriValidator`**

  Adds an “AppAuth” (OAuth 2.0 for Native Apps) compliant redirect URI validator (does strict validation but also allows `http://127.0.0.1` with random port).

* **`AddJwtBearerClientAuthentication`**

  Adds support for client authentication using JWT bearer assertions.

* **`AddMutualTlsSecretValidators`**

  Adds the X509 secret validators for mutual TLS.

* **`AddIdentityProviderConfigurationValidator`**

  Adds an IdentityProvider configuration validator.

* **`AddBackchannelAuthenticationUserValidator`**

  Adds the backchannel login user validator.

* **`AddBackchannelAuthenticationUserNotificationService`**

  Adds the backchannel login user validator.
-----
# Entity Framework Core Options

> Configuration options available when using Entity Framework Core as the storage implementation for IdentityServer.

If using the [Entity Framework Core store implementation](/identityserver/data/providers/entityframework-core/), you might need to configure those specific options.
-----
# Configuration Options

> Configuration options available when using Entity Framework Core as the configuration store in IdentityServer

## Duende.IdentityServer.EntityFramework.Options.ConfigurationStoreOptions

[Section titled “Duende.IdentityServer.EntityFramework.Options.ConfigurationStoreOptions”](#duendeidentityserverentityframeworkoptionsconfigurationstoreoptions)

These options are configurable when using the Entity Framework Core for the [configuration store](/identityserver/data/configuration/):

You set the options at startup time in your `AddConfigurationStore` method:

Program.cs

```csharp
var builder = services.AddIdentityServer()
    .AddConfigurationStore(options =>
    {
        // configure options here..
    })
```

### Pooling

[Section titled “Pooling”](#pooling)

Settings that affect the DbContext pooling feature of Entity Framework Core.

* **`EnablePooling`**

  Gets or set if EF DbContext pooling is enabled. Defaults to `false`.

* **`PoolSize`**

  Gets or set the pool size to use when DbContext pooling is enabled. If not set, the EF default is used.

### Schema

[Section titled “Schema”](#schema)

Settings that affect the database schema and table names.

* **`DefaultSchema`**

  Gets or sets the default schema. Defaults to `null`.

`TableConfiguration` settings for each individual table (schema and name) managed by this feature:

Identity Resource related tables:

* **`IdentityResource`**
* **`IdentityResourceClaim`**
* **`IdentityResourceProperty`**

API Resource related tables:

* **`ApiResource`**
* **`ApiResourceSecret`**
* **`ApiResourceScope`**
* **`ApiResourceClaim`**
* **`ApiResourceProperty`**

Client related tables:

* **`Client`**
* **`ClientGrantType`**
* **`ClientRedirectUri`**
* **`ClientPostLogoutRedirectUri`**
* **`ClientScopes`**
* **`ClientSecret`**
* **`ClientClaim`**
* **`ClientIdPRestriction`**
* **`ClientCorsOrigin`**
* **`ClientProperty`**

API Scope related tables:

* **`ApiScope`**
* **`ApiScopeClaim`**
* **`ApiScopeProperty`**

Identity provider related tables:

* **`IdentityProvider`**
-----
# Operational Options

> Configure Entity Framework Core operational store options including database schema, pooling settings, and cleanup parameters for persisted grants.

## Duende.IdentityServer.EntityFramework.Options.OperationalStoreOptions

[Section titled “Duende.IdentityServer.EntityFramework.Options.OperationalStoreOptions”](#duendeidentityserverentityframeworkoptionsoperationalstoreoptions)

These options are configurable when using the Entity Framework Core for the [operational store](/identityserver/data/operational/):

You set the options at startup time in your `AddOperationalStore` method:

Program.cs

```csharp
builder.Services.AddIdentityServer()
    .AddOperationalStore(options =>
    {
        // configure options here..
    })
```

### Pooling

[Section titled “Pooling”](#pooling)

Settings that affect the DbContext pooling feature of Entity Framework Core.

* **`EnablePooling`**

  Gets or set if EF DbContext pooling is enabled. Defaults to `false`.

* **`PoolSize`**

  Gets or set the pool size to use when DbContext pooling is enabled. If not set, the EF default is used.

### Schema

[Section titled “Schema”](#schema)

Settings that affect the database schema and table names.

* **`DefaultSchema`**

  Gets or sets the default schema. Defaults to `null`.

`TableConfiguration` settings for each individual table (schema and name) managed by this feature:

* **`PersistedGrants`**
* **`DeviceFlowCodes`**
* **`Keys`**
* **`ServerSideSessions`**

### Persisted Grants Cleanup

[Section titled “Persisted Grants Cleanup”](#persisted-grants-cleanup)

Settings that affect the background cleanup of expired entries (tokens) from the persisted grants table.

* **`EnableTokenCleanup`**

  Gets or sets a value indicating whether stale entries will be automatically cleaned up from the database. This is implemented by periodically connecting to the database (according to the TokenCleanupInterval) from the hosting application. Defaults to `false`.

* **`RemoveConsumedTokens`**

  Gets or sets a value indicating whether consumed tokens will be included in the automatic clean up. Defaults to `false`.

* **`TokenCleanupInterval`**

  Gets or sets the token cleanup interval (in seconds). The default is `3600` (1 hour).

* **`TokenCleanupBatchSize`**

  Gets or sets the number of records to remove per batch operation. The cleanup job will perform multiple batch operations as long as there are more records to remove than the configured `TokenCleanupBatchSize`. Defaults to `100`.

* **`FuzzTokenCleanupStart`**

  The background token cleanup job runs at a configured interval. If multiple nodes run the cleanup job at the same time there will be updated conflicts in the store. To avoid that, the startup time can be fuzzed. The first run is scheduled at a random time between the host startup and the configured TokenCleanupInterval. Subsequent runs are run on the configured TokenCleanupInterval. Defaults to `true`
-----
# Authorize Endpoint

> Documentation for the authorize endpoint which handles browser-based token and authorization code requests, including authentication and consent flows.

The authorize endpoint can be used to request tokens or authorization codes via the browser. This process typically involves authentication of the end-user and optionally consent.

IdentityServer supports a subset of the OpenID Connect and OAuth 2.0 authorize request parameters. For a full list, see [here](https://openid.net/specs/openid-connect-core-1_0.html#authrequest).

### Required Parameters

[Section titled “Required Parameters”](#required-parameters)

* **`client_id`**

  identifier of the client

* **`scope`**

  one or more registered scopes

* **`redirect_uri`**

  must exactly match one of the allowed redirect URIs for that client

* **`response_type`**

  specifies the response type

  * **`id_token`**

  * **`token`**

  * **`id_token token`**

  * **`code`**

  * **`code id_token`**

  * **`code id_token token`**

### Optional Parameters

[Section titled “Optional Parameters”](#optional-parameters)

* **`response_mode`**

  specifies the response mode

  * **`query`**

  * **`fragment`**

  * **`form_post`**

* **`state`**

  echos back the state value on the token response, this is for round tripping state between client and provider, correlating request and response and CSRF/replay protection. (recommended)

* **`nonce`**

  echos back the nonce value in the identity token (for replay protection)

  Required when identity tokens is transmitted via the browser channel

* **`prompt`**

  * **`none`**

    no UI will be shown during the request. If this is not possible (e.g. because the user has to sign in or consent) an error is returned

  * **`login`**

    the login UI will be shown, even if the user is already signed in and has a valid session

  * **`create`**

    the user registration UI will be shown, if the `UserInteraction.CreateAccountUrl` option is set (the option is null by default, which disables support for this prompt value)

* **`code_challenge`**

  sends the code challenge for PKCE

* **`code_challenge_method`**

  * **`plain`**

    indicates that the challenge is using plain text (not recommended)

  * **`S256`**

    indicates the challenge is hashed with SHA256

* **`login_hint`**

  can be used to pre-fill the username field on the login page

* **`ui_locales`**

  gives a hint about the desired display language of the login UI

* **`max_age`**

  if the user’s logon session exceeds the max age (in seconds), the login UI will be shown

* **`acr_values`**

  allows passing in additional authentication related information - IdentityServer special cases the following proprietary acr\_values:

  * **`idp:name_of_idp`**

    bypasses the login/home realm screen and forwards the user directly to the selected identity provider (if allowed per client configuration)

  * **`tenant:name_of_tenant`**

    can be used to pass a tenant name to the login UI

* **`request`**

  instead of providing all parameters as individual query string parameters, you can provide a subset or all them as a JWT

* **`request_uri`**

  URL of a pre-packaged JWT containing request parameters

```text
GET /connect/authorize?
    client_id=client1&
    scope=openid email api1&
    response_type=id_token token&
    redirect_uri=https://myapp/callback&
    state=abc&
    nonce=xyz
```

## .NET Client Library

[Section titled “.NET Client Library”](#net-client-library)

You can use the [Duende IdentityModel](/identitymodel/) client library to programmatically create authorize request URLs from .NET code.

```csharp
var ru = new RequestUrl("https://demo.duendesoftware.com/connect/authorize");


var url = ru.CreateAuthorizeUrl(
    clientId: "client",
    responseType: "code",
    redirectUri: "https://app.com/callback",
    scope: "openid");
```
-----
# Backchannel Authentication Endpoint

> Documentation for the CIBA endpoint which allows clients to initiate backchannel authentication requests for users without browser interaction

The backchannel authentication endpoint is used by a client to initiate a [CIBA](/identityserver/ui/ciba/) request.

Clients must be configured with the `"urn:openid:params:grant-type:ciba"` grant type to use this endpoint. You can use the `OidcConstants.GrantTypes.Ciba` constant rather than hard coding the value for the CIBA grant type.

### Required Parameters

[Section titled “Required Parameters”](#required-parameters)

* **`scope`**

  one or more registered scopes

Note

The client id and a client credential is required to authenticate to the endpoint using any valid form of authentication that has been configured for it (much like the token endpoint).

### Exactly One Of These Values Is Required

[Section titled “Exactly One Of These Values Is Required”](#exactly-one-of-these-values-is-required)

* **`login_hint`**

  hint for the end user to be authenticated. the value used is implementation specific.

* **`id_token_hint`**

  a previously issued id\_token for the end user to be authenticated

* **`login_hint_token`**

  a token containing information for the end user to be authenticated. the details are implementation specific.

Note

To validate these implementation specific values and use them to identity the user that is to be authenticated, you are required to implement the `IBackchannelAuthenticationUserValidator` interface.

### Optional Parameters

[Section titled “Optional Parameters”](#optional-parameters)

* **`binding_message`**

  identifier or message intended to be displayed on both the consumption device and the authentication device

* **`user_code`**

  a secret code, such as a password or pin, that is known only to the user but verifiable by the OP

* **`requested_expiry`**

  a positive integer allowing the client to request the `expires_in` value for the `auth_req_id` the server will return. if not present, then the optional `CibaLifetime` property on the `Client` is used, and if that is not present, then the `DefaultLifetime` on the `CibaOptions` will be used.

* **`acr_values`**

  allows passing in additional authentication related information - IdentityServer special cases the following proprietary acr\_values:

  * **`idp:name_of_idp`**

    bypasses the login/home realm screen and forwards the user directly to the selected identity provider (if allowed per client configuration)

  * **`tenant:name_of_tenant`**

    can be used to pass a tenant name to the login UI

* **`resource`**

  resource indicator identifying the `ApiResource` for which the access token should be restricted to

* **`request`**

  instead of providing all parameters as individual parameters, you can provide all them as a JWT

And a successful response will look something like:

```http
HTTP/1.1 200 OK
Content-Type: application/json
Cache-Control: no-store


{
    "auth_req_id": "1C266114A1BE42528AD104986C5B9AC1",
    "expires_in": 600,
    "interval": 5
}
```

## .NET Client Library

[Section titled “.NET Client Library”](#net-client-library)

You can use the [Duende IdentityModel](/identitymodel/) client library to programmatically interact with the protocol endpoint from .NET code.

```csharp
using Duende.IdentityModel.Client;


var client = new HttpClient();


var cibaResponse = await client.RequestBackchannelAuthenticationAsync(new BackchannelAuthenticationRequest
{
    Address = "https://demo.duendesoftware.com/connect/ciba",
    ClientId = "client1",
    ClientSecret = "secret",
    Scope = "openid api1",
    LoginHint = "alice",
});
```

And with a successful response, it can be used to poll the token endpoint:

```csharp
while (true)
{
    var response = await client.RequestBackchannelAuthenticationTokenAsync(new BackchannelAuthenticationTokenRequest
    {
        Address = "https://demo.duendesoftware.com/connect/token",
        ClientId = "client1",
        ClientSecret = "secret",
        AuthenticationRequestId = cibaResponse.AuthenticationRequestId
    });


    if (response.IsError)
    {
        if (response.Error == OidcConstants.TokenErrors.AuthorizationPending || response.Error == OidcConstants.TokenErrors.SlowDown)
        {
            await Task.Delay(cibaResponse.Interval.Value * 1000);
        }
        else
        {
            throw new Exception(response.Error);
        }
    }
    else
    {
        // success! use response.IdentityToken, response.AccessToken, and response.RefreshToken (if requested)
    }
}
```
-----
# Device Authorization Endpoint

> Documentation for the device authorization endpoint which handles device flow authentication requests and issues device and user codes for authorization.

The device authorization endpoint can be used to request device and user codes. This endpoint is used to start the device flow authorization process.

* **`client_id`**

  client identifier (required)

* **`client_secret`**

  client secret either in the post body, or as a basic authentication header. Optional.

* **`scope`**

  one or more registered scopes. If not specified, a token for all explicitly allowed scopes will be issued

## .NET Client Library

[Section titled “.NET Client Library”](#net-client-library)

You can use the [Duende IdentityModel](/identitymodel/) client library to programmatically interact with the protocol endpoint from .NET code.

```csharp
using Duende.IdentityModel.Client;


var client = new HttpClient();


var response = await client.RequestDeviceAuthorizationAsync(new DeviceAuthorizationRequest
{
    Address = "https://demo.duendesoftware.com/connect/device_authorize",
    ClientId = "device"
});
```
-----
# Discovery Endpoint

> Learn about the discovery endpoint that provides metadata about your IdentityServer configuration, including issuer name, key material, and supported scopes.

The [discovery endpoint](https://openid.net/specs/openid-connect-discovery-1_0.html) can be used to retrieve metadata about your IdentityServer - it returns information like the issuer name, key material, supported scopes etc.

The discovery endpoint is available via `/.well-known/openid-configuration` relative to the base address, e.g.:

```text
https://demo.duendesoftware.com/.well-known/openid-configuration
```

## Issuer Name and Path Base

[Section titled “Issuer Name and Path Base”](#issuer-name-and-path-base)

When your IdentityServer is hosted in an application that uses [ASP.NET Core’s `PathBaseMiddleware`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.extensions.usepathbasemiddleware), the base path will be included in the issuer name and discovery document URLs. For example, if your application is configured with a path base of `/identity`, your configuration will look like this:

Program.cs

```csharp
var builder = WebApplication.CreateBuilder(args);


// 👨‍💻 configure Application Host


var app = builder.Build();
app.UseSerilogRequestLogging();


if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}


// 👋 Configuring the path base
app.UsePathBase("/identity");


app.UseStaticFiles();
app.UseRouting();


app.UseIdentityServer();
app.UseAuthorization();


app.MapRazorPages()
    .RequireAuthorization();


return app;
```

And the discovery document will look like this:

.well-known/openid-configuration

```json
{
  "issuer": "https://localhost:5001/identity",
  "jwks_uri": "https://localhost:5001/identity/.well-known/openid-configuration/jwks",
  "authorization_endpoint": "https://localhost:5001/identity/connect/authorize",
  "token_endpoint": "https://localhost:5001/identity/connect/token",
  "userinfo_endpoint": "https://localhost:5001/identity/connect/userinfo",
  "end_session_endpoint": "https://localhost:5001/identity/connect/endsession",
  "check_session_iframe": "https://localhost:5001/identity/connect/checksession",
  "revocation_endpoint": "https://localhost:5001/identity/connect/revocation",
  "introspection_endpoint": "https://localhost:5001/identity/connect/introspect",
  "device_authorization_endpoint": "https://localhost:5001/identity/connect/deviceauthorization",
  "backchannel_authentication_endpoint": "https://localhost:5001/identity/connect/ciba",
  "pushed_authorization_request_endpoint": "https://localhost:5001/identity/connect/par"
}
```

This can be helpful when configuring IdentityServer in a multi-tenant scenario where the base path is used to identify the tenant.

## .NET Client Library

[Section titled “.NET Client Library”](#net-client-library)

You can use the [Duende IdentityModel](/identitymodel/) client library to programmatically interact with the protocol endpoint from .NET code.

```csharp
var client = new HttpClient();


var disco = await client.GetDiscoveryDocumentAsync("https://demo.duendesoftware.com");
```
-----
# End Session Endpoint

> The end session endpoint enables single sign-out functionality in OpenID Connect, allowing users to terminate their sessions across multiple client applications.

The end session endpoint can be used to trigger single sign-out in the browser ( see [spec](https://openid.net/specs/openid-connect-rpinitiated-1_0.html)).

To use the end session endpoint a client application will redirect the user’s browser to the end session URL. All applications that the user has logged into via the browser during the user’s session can participate in the sign-out.

The URL for the end session endpoint is available via discovery.

* **`id_token_hint`**

  When the user is redirected to the endpoint, they will be prompted if they really want to sign-out. This prompt can be bypassed by a client sending the original `id_token` received from authentication. This is passed as a query string parameter called `id_token_hint`.

* **`post_logout_redirect_uri`**

  If a valid `id_token_hint` is passed, then the client may also send a `post_logout_redirect_uri` parameter. This can be used to allow the user to redirect back to the client after sign-out. The value must match one of the client’s pre-configured `PostLogoutRedirectUris`.

* **`state`**

  If a valid `post_logout_redirect_uri` is passed, then the client may also send a `state` parameter. This will be returned back to the client as a query string parameter after the user redirects back to the client. This is typically used by clients to roundtrip state across the redirect.

```text
GET /connect/endsession?id_token_hint=...&post_logout_redirect_uri=http%3A%2F%2Flocalhost%3A7017%2Findex.html
```

## .NET Client Library

[Section titled “.NET Client Library”](#net-client-library)

You can use the [Duende IdentityModel](/identitymodel/) client library to programmatically create end sessions request URLs from .NET code.

```csharp
var ru = new RequestUrl("https://demo.duendesoftware.com/connect/end_session");


var url = ru.CreateEndSessionUrl(
    idTokenHint: "...",
    postLogoutRedirectUri: "...");
```
-----
# Introspection Endpoint

> Documentation for the RFC 7662 compliant introspection endpoint used to validate reference tokens, JWTs, and refresh tokens.

The introspection endpoint is an implementation of [RFC 7662](https://tools.ietf.org/html/rfc7662).

It can be used to validate reference tokens, JWTs (if the consumer does not have support for appropriate JWT or cryptographic libraries) and refresh tokens. Refresh tokens can only be introspected by the client that requested them.

The introspection endpoint requires authentication. Since the request to the introspection endpoint is typically done by an API, which is not an OAuth client, the [`ApiResource`](/identityserver/fundamentals/resources/api-resources/) is used to configure credentials:

```csharp
new ApiResource("resource1")
{
    Scopes = { "scope1", "scope2" }, // Replace "scope1", "scope2" with the actual scopes required for your API


    ApiSecrets =
    {
        new Secret("secret".Sha256())
    }
}
```

The ID used for authentication is the name of the `ApiResource`, “resource1”, and the secret is the configured secret. The introspection endpoint uses HTTP basic auth to communicate these credentials:

```text
POST /connect/introspect
Content-Type: application/x-www-form-urlencoded
Authorization: Basic xxxyyy


token=
```

A successful response will return a status code of 200, the token claims, the token type, and a flag indicating the token is active:

```json
{
  "iss": "https://localhost:5001",
  "nbf": 1729599599,
  "iat": 1729599599,
  "exp": 1729603199,
  "client_id": "client",
  "jti": "44FD2DE9E9F8E9F4DDD141CD7C244BE9",
  "scope": "api1",
  "token_type": "access_token",
  "active": true
}
```

Unknown or expired tokens will be marked as inactive:

```json
{
  "active": false
}
```

An invalid request will return a 400, an unauthorized request 401.

## JWT Response from Introspection Endpoint v7.3

[Section titled “JWT Response from Introspection Endpoint ”v7.3](#jwt-response-from-introspection-endpoint)

IdentityServer supports [RFC 9701](https://www.rfc-editor.org/rfc/rfc9701.html) to return a JWT response from the introspection endpoint.

To return a JWT response, set the `Accept` header in the HTTP request to `application/token-introspection+jwt`:

```text
POST /connect/introspect
Content-Type: application/x-www-form-urlencoded
Accept: application/token-introspection+jwt
Authorization: Basic xxxyyy


token=
```

A successful response will return a status code of 200 and has a `Content-Type: application/token-introspection+jwt` header, indicating that the response body contains a raw JWT instead. The base64 decoded JWT will have a `typ` claim in the header with the value `token-introspection+jwt`. The token’s payload contains a `token_introspection` JSON object similar to the default response type:

```json
{
  "alg": "RS256",
  "kid": "BE9D78519A8BBCB28A65FADEECF49CBC",
  "typ": "token-introspection+jwt"
}.{
  "iss": "https://localhost:5001",
  "iat": 1729599599,
  "aud": "api1",
  "token_introspection": {
    "iss": "https://localhost:5001",
    "nbf": 1729599599,
    "iat": 1729599599,
    "exp": 1729603199,
    "aud": [ "api1" ],
    "client_id": "client",
    "jti": "44FD2DE9E9F8E9F4DDD141CD7C244BE9",
    "active": true,
    "scope": "api1"
  }
}.[Signature]
```

## .NET Client Library

[Section titled “.NET Client Library”](#net-client-library)

You can use the [Duende IdentityModel](/identitymodel/) client library to programmatically interact with the protocol endpoint from .NET code.

```csharp
using Duende.IdentityModel.Client;


var client = new HttpClient();


var response = await client.IntrospectTokenAsync(new TokenIntrospectionRequest
{
    Address = "https://demo.duendesoftware.com/connect/introspect",
    ClientId = "resource1",
    ClientSecret = "secret",


    Token = "" // Replace with the actual token
});
```
-----
# OAuth Metadata Endpoint

> Learn about the OAuth metadata endpoint that provides information about your IdentityServer configuration, including issuer name, key material, and supported scopes.

The [OAuth Metadata Endpoint](https://www.rfc-editor.org/rfc/rfc8414.html) is a standardized way to retrieve metadata about your IdentityServer.

The discovery endpoint is available via `/.well-known/oauth-authorization-server` relative to the base address, e.g.:

```text
https://demo.duendesoftware.com/.well-known/oauth-authorization-server
```

## Issuer Name and Path Base

[Section titled “Issuer Name and Path Base”](#issuer-name-and-path-base)

When hosting IdentityServer in an application that uses [ASP.NET Core’s `PathBaseMiddleware`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.aspnetcore.builder.extensions.usepathbasemiddleware), the base path will be included in the issuer name and discovery document URLs.

Refer the [Discovery Endpoint](/identityserver/reference/v7/endpoints/discovery/#issuer-name-and-path-base) for more information.
-----
# Revocation Endpoint

> Learn about the revocation endpoint that allows invalidating access and refresh tokens according to RFC 7009 specification.

This endpoint allows revoking access tokens (reference tokens only) and refresh token. It implements the token revocation specification [(RFC 7009)](https://tools.ietf.org/html/rfc7009).

* **`token`**

  the token to revoke (required)

* **`token_type_hint`**

  either `access_token` or `refresh_token` (optional)

```text
POST /connect/revocation HTTP/1.1
Host: server.example.com
Content-Type: application/x-www-form-urlencoded
Authorization: Basic czZCaGRSa3F0MzpnWDFmQmF0M2JW


token=...&token_type_hint=refresh_token
```

## .NET Client Library

[Section titled “.NET Client Library”](#net-client-library)

You can use the [Duende IdentityModel](/identitymodel/) client library to programmatically interact with the protocol endpoint from .NET code.

```csharp
using Duende.IdentityModel.Client;


var client = new HttpClient();


var result = await client.RevokeTokenAsync(new TokenRevocationRequest
{
    Address = "https://demo.duendesoftware.com/connect/revocation",
    ClientId = "client",
    ClientSecret = "secret",


    Token = token
});
```
-----
# Token Endpoint

> Documentation for the token endpoint that enables programmatic token requests using various grant types and parameters in Duende IdentityServer.

The token endpoint can be used to programmatically request tokens.

Duende IdentityServer supports a subset of the OpenID Connect and OAuth 2.0 token request parameters. For a full list, see [here](https://openid.net/specs/openid-connect-core-1_0.html#tokenrequest).

### Required Parameters

[Section titled “Required Parameters”](#required-parameters)

* **`client_id`**

  client identifier; not necessary in body if it is present in the authorization header

* **`grant_type`**

  * **`authorization_code`**

  * **`client_credentials`**

  * **`password`**

  * **`refresh_token`**

  * **`urn:ietf:params:oauth:grant-type:device_code`**

  * ***extension grant***

### Optional Parameters

[Section titled “Optional Parameters”](#optional-parameters)

* **`client_secret`**

  client secret for confidential/credentials clients - either in the post body, or as a basic authentication header.

* **`scope`**

  one or more registered scopes. If not specified, a token for all explicitly allowed scopes will be issued.

* **`redirect_uri`**

  required for the `authorization_code` grant type

* **`code`**

  the authorization code (required for `authorization_code` grant type)

* **`code_verifier`**

  PKCE proof key

* **`username`**

  resource owner username (required for `password` grant type)

* **`password`**

  resource owner password (required for `password` grant type)

* **`acr_values`**

  allows passing in additional authentication related information. Duende IdentityServer special cases the following proprietary acr\_values

  * **`tenant:name_of_tenant`**

    can be used to pass a tenant name to the token endpoint

* **`refresh_token`**

  the refresh token (required for `refresh_token` grant type)

* **`device_code`**

  the device code (required for `urn:ietf:params:oauth:grant-type:device_code` grant type)

* **`auth_req_id`**

  the backchannel authentication request id (required for `urn:openid:params:grant-type:ciba` grant type)

```text
POST /connect/token
CONTENT-TYPE application/x-www-form-urlencoded


    client_id=client1&
    client_secret=secret&
    grant_type=authorization_code&
    code=hdh922&
    redirect_uri=https://myapp.com/callback
```

## .NET Client Library

[Section titled “.NET Client Library”](#net-client-library)

You can use the [Duende IdentityModel](/identitymodel/) client library to programmatically interact with the protocol endpoint from .NET code.

```csharp
using Duende.IdentityModel.Client;


var client = new HttpClient();


var response = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest
{
    Address = TokenEndpoint,


    ClientId = "client",
    ClientSecret = "secret",


    Code = "...",
    CodeVerifier = "...",
    RedirectUri = "https://app.com/callback"
});
```
-----
# UserInfo Endpoint

> Reference documentation for the UserInfo endpoint, which allows retrieval of authenticated user claims using a valid access token.

The UserInfo endpoint can be used to retrieve claims about a user ( see [spec](https://openid.net/specs/openid-connect-core-1_0.html#userinfo)).

The caller needs to send a valid access token. Depending on the granted scopes, the UserInfo endpoint will return the mapped claims (at least the `openid` scope is required).

```text
GET /connect/userinfo
Authorization: Bearer 
```

```text
HTTP/1.1 200 OK
Content-Type: application/json


{
    "sub": "248289761001",
    "name": "Bob Smith",
    "given_name": "Bob",
    "family_name": "Smith"
}
```

## .NET Client Library

[Section titled “.NET Client Library”](#net-client-library)

You can use the [Duende IdentityModel](/identitymodel/) client library to programmatically interact with the protocol endpoint from .NET code.

```csharp
using Duende.IdentityModel.Client;


var client = new HttpClient();


var disco = await client.GetDiscoveryDocumentAsync("https://localhost:5001");


var token = await client.RequestAuthorizationCodeTokenAsync(new AuthorizationCodeTokenRequest
{
    Address = disco.TokenEndpoint,


    ClientId = "client",
    ClientSecret = "secret",


    Code = "...",
    CodeVerifier = "...",
    RedirectUri = "https://app.com/callback"
});


var userInfo = await client.GetUserInfoAsync(new UserInfoRequest
{
    Address = disco.UserInfoEndpoint,
    Token = token.AccessToken
});
```
-----
# API Resource

> Reference documentation for the ApiResource class which models an API in Duende IdentityServer, including its properties and configuration options.

## Duende.IdentityServer.Models.ApiResource

[Section titled “Duende.IdentityServer.Models.ApiResource”](#duendeidentityservermodelsapiresource)

This class models an API.

* **`Enabled`**

  Indicates if this resource is enabled and can be requested. Defaults to true.

* **`Name`**

  The unique name of the API. This value is used for authentication with introspection and will be added to the audience of the outgoing access token.

* **`DisplayName`**

  This value can be used e.g. on the consent screen.

* **`Description`**

  This value can be used e.g. on the consent screen.

* **`RequireResourceIndicator`**

  Indicates if this API resource requires the resource indicator to request it, and expects access tokens issued to it will only ever contain this API resource as the audience.

* **`ApiSecrets`**

  The API secret is used for the introspection endpoint. The API can authenticate with introspection using the API name and secret.

* **`AllowedAccessTokenSigningAlgorithms`**

  List of allowed signing algorithms for access token. If empty, will use the server default signing algorithm.

* **`UserClaims`**

  List of associated user claim types that should be included in the access token.

* **`Scopes`**

  List of API scope names. You need to create those using [ApiScope](/identityserver/reference/v7/models/api-scope/).

## Defining API resources In appsettings.json

[Section titled “Defining API resources In appsettings.json”](#defining-api-resources-in-appsettingsjson)

The `AddInMemoryApiResource` extensions method also supports adding API resources from the ASP.NET Core configuration file:

```plaintext
"IdentityServer": {
    "IssuerUri": "urn:sso.company.com",
    "ApiResources": [
        {
            "Name": "resource1",
            "DisplayName": "Resource #1",


            "Scopes": [
                "resource1.scope1",
                "shared.scope"
            ]
        },
        {
            "Name": "resource2",
            "DisplayName": "Resource #2",


            "UserClaims": [
                "name",
                "email"
            ],


            "Scopes": [
                "resource2.scope1",
                "shared.scope"
            ]
        }
    ]
}
```

Then pass the configuration section to the `AddInMemoryApiResource` method:

Program.cs

```csharp
idsvrBuilder.AddInMemoryApiResources(configuration.GetSection("IdentityServer:ApiResources"))
```
-----
# API Scope

> Reference documentation for the ApiScope class which models an OAuth scope in Duende IdentityServer, including its properties and configuration options.

## Duende.IdentityServer.Models.ApiScope

[Section titled “Duende.IdentityServer.Models.ApiScope”](#duendeidentityservermodelsapiscope)

This class models an OAuth scope.

* **`Enabled`**

  Indicates if this resource is enabled and can be requested. Defaults to true.

* **`Name`**

  The unique name of the API. This value is used for authentication with introspection and will be added to the audience of the outgoing access token.

* **`DisplayName`**

  This value can be used e.g. on the consent screen.

* **`Description`**

  This value can be used e.g. on the consent screen.

* **`UserClaims`**

  List of associated user claim types that should be included in the access token.

## Defining API Scope In appsettings.json

[Section titled “Defining API Scope In appsettings.json”](#defining-api-scope-in-appsettingsjson)

The `AddInMemoryApiResource` extension method also supports adding clients from the ASP.NET Core configuration file:

```json
{
  "IdentityServer": {
    "IssuerUri": "urn:sso.company.com",
    "ApiScopes": [
      {
        "Name": "IdentityServerApi"
      },
      {
        "Name": "resource1.scope1"
      },
      {
        "Name": "resource2.scope1"
      },
      {
        "Name": "scope3"
      },
      {
        "Name": "shared.scope"
      },
      {
        "Name": "transaction",
        "DisplayName": "Transaction",
        "Description": "A transaction"
      }
    ]
  }
}
```

Then pass the configuration section to the `AddInMemoryApiScopes` method:

Program.cs

```csharp
idsvrBuilder.AddInMemoryApiScopes(configuration.GetSection("IdentityServer:ApiScopes"))
```
-----
# Backchannel User Login Request

> Reference documentation for the BackchannelUserLoginRequest class which models the information needed to initiate a user login request for Client Initiated Backchannel Authentication (CIBA).

## Duende.IdentityServer.Models.BackchannelUserLoginRequest

[Section titled “Duende.IdentityServer.Models.BackchannelUserLoginRequest”](#duendeidentityservermodelsbackchanneluserloginrequest)

Models the information to initiate a user login request for [CIBA](/identityserver/ui/ciba/).

* **`InternalId`**

  The identifier of the request in the store.

* **`Subject`**

  The subject for whom the login request is intended.

* **`BindingMessage`**

  The binding message used in the request.

* **`AuthenticationContextReferenceClasses`**

  The `acr_values` used in the request.

* **`Tenant`**

  The `tenant` value from the `acr_values` used in the request.

* **`IdP`**

  The `idp` value from the `acr_values` used in the request.

* **`RequestedResourceIndicators`**

  The resource indicator values used in the request.

* **`Client`**

  The client that initiated the request.

* **`ValidatedResources`**

  The validated resources (i.e. scopes) used in the request.
-----
# Client

> Reference documentation for the Client class which models an OpenID Connect or OAuth 2.0 client in Duende IdentityServer, including configuration for authentication, tokens, consent, refresh tokens, and advanced features.

## Duende.IdentityServer.Models.Client

[Section titled “Duende.IdentityServer.Models.Client”](#duendeidentityservermodelsclient)

The `Client` class models an OpenID Connect or OAuth 2.0 client - e.g. a native application, a web application or a JS-based application.

```csharp
public static IEnumerable Get()
{
    return new List
    {
        ///////////////////////////////////////////
        // machine to machine client
        //////////////////////////////////////////
        new Client
        {
            ClientId = "machine",
            ClientSecrets = { Configuration["machine.secret"] },


            AllowedGrantTypes = GrantTypes.ClientCredentials,


            AllowedScopes = machineScopes
        },


        ///////////////////////////////////////////
        // web client
        //////////////////////////////////////////
        new Client
        {
            ClientId = "web",


            ClientSecrets = { new Secret(Configuration["web.secret"]) },


            AllowedGrantTypes = GrantTypes.Code,


            RedirectUris = { "https://myapp.com:/signin-oidc" },
            PostLogoutRedirectUris = { "https://myapp.com/signout-callback-oidc" },


            BackChannelLogoutUri = "https://myapp.com/backchannel-logout",


            AllowOfflineAccess = true,
            AllowedScopes = webScopes
        }
    }
}
```

## Basics

[Section titled “Basics”](#basics)

* **`Enabled`**

  Specifies if client is enabled. Defaults to `true`.

* **`ClientId`**

  Unique ID of the client

* **`ClientSecrets`**

  List of client secrets - credentials to access the token endpoint.

* **`RequireClientSecret`**

  Specifies whether this client needs a secret to request tokens from the token endpoint (defaults to `true`)

* **`RequireRequestObject`**

  Specifies whether this client needs to wrap the authorize request parameters in a JWT (defaults to `false`)

* **`AllowedGrantTypes`**

  Specifies the grant types the client is allowed to use. Use the `GrantTypes` class for common combinations.

* **`RequirePkce`**

  Specifies whether clients using an authorization code based grant type must send a proof key (defaults to `true`).

* **`AllowPlainTextPkce`**

  Specifies whether clients using PKCE can use a plain text code challenge (not recommended - and defaults to `false`)

* **`RedirectUris`**

  Specifies the allowed URIs to return tokens or authorization codes to

* **`AllowedScopes`**

  By default, a client has no access to any resources - specify the allowed resources by adding the corresponding scopes names

* **`AllowOfflineAccess`**

  Specifies whether this client can request refresh tokens (be requesting the `offline_access` scope)

* **`AllowAccessTokensViaBrowser`**

  Specifies whether this client is allowed to receive access tokens via the browser. This is useful to harden flows that allow multiple response types (e.g. by disallowing a hybrid flow client that is supposed to use *code id\_token* to add the `token` response type and thus leaking the token to the browser).

* **`Properties`**

  Dictionary to hold any custom client-specific values as needed.

## Authentication / Session Management

[Section titled “Authentication / Session Management”](#authentication--session-management)

* **`PostLogoutRedirectUris`**

  Specifies allowed URIs to redirect to after logout.

* **`FrontChannelLogoutUri`**

  Specifies logout URI at client for HTTP based front-channel logout.

* **`FrontChannelLogoutSessionRequired`**

  Specifies if the user’s session id should be sent to the FrontChannelLogoutUri. Defaults to true.

* **`BackChannelLogoutUri`**

  Specifies logout URI at client for HTTP based back-channel logout.

* **`BackChannelLogoutSessionRequired`**

  Specifies if the user’s session id should be sent in the request to the BackChannelLogoutUri. Defaults to true.

* **`EnableLocalLogin`**

  Specifies if this client can use local accounts, or external IdPs only. Defaults to `true`.

* **`IdentityProviderRestrictions`**

  Specifies which external IdPs can be used with this client (if list is empty all IdPs are allowed). Defaults to empty.

* **`UserSsoLifetime`**

  The maximum duration (in seconds) since the last time the user authenticated. Defaults to `null`. You can adjust the lifetime of a session token to control when and how often a user is required to reenter credentials instead of being silently authenticated, when using a web application.

* **`AllowedCorsOrigins`**

  If specified, will be used by the default CORS policy service implementations (In-Memory and EF) to build a CORS policy for JavaScript clients.

* **`CoordinateLifetimeWithUserSession`** (added in v6.1)

  When enabled, the client’s token lifetimes (e.g. refresh tokens) will be tied to the user’s session lifetime. This means when the user logs out, any revokable tokens will be removed. If using server-side sessions, expired sessions will also remove any revokable tokens, and backchannel logout will be triggered. This client’s setting overrides the global `CoordinateClientLifetimesWithUserSession` configuration setting.

## Token

[Section titled “Token”](#token)

* **`IdentityTokenLifetime`**

  Lifetime to identity token in seconds (defaults to 300 seconds / 5 minutes)

* **`AllowedIdentityTokenSigningAlgorithms`**

  List of allowed signing algorithms for identity token. If empty, will use the server default signing algorithm.

* **`AccessTokenLifetime`**

  Lifetime of access token in seconds (defaults to 3600 seconds / 1 hour)

* **`AuthorizationCodeLifetime`**

  Lifetime of authorization code in seconds (defaults to 300 seconds / 5 minutes)

* **`AccessTokenType`**

  Specifies whether the access token is a reference token or a self-contained JWT token (defaults to `Jwt`).

* **`IncludeJwtId`**

  Specifies whether JWT access tokens should have an embedded unique ID (via the `jti` claim). Defaults to `true`.

* **`Claims`**

  Allows settings claims for the client (will be included in the access token).

* **`AlwaysSendClientClaims`**

  If set, the client claims will be sent for every flow. If not, only for client credentials flow (default is `false`)

* **`AlwaysIncludeUserClaimsInIdToken`**

  When requesting both an id token and access token, should the user claims always be added to the id token instead of requiring the client to use the userinfo endpoint. Default is `false`.

* **`ClientClaimsPrefix`**

  If set, the prefix client claim types will be prefixed with. Defaults to `client_`. The intent is to make sure they don’t accidentally collide with user claims.

* **`PairWiseSubjectSalt`**

  Salt value used in pair-wise subjectId generation for users of this client. Currently not implemented.

## Refresh Token

[Section titled “Refresh Token”](#refresh-token)

* **`AbsoluteRefreshTokenLifetime`**

  Maximum lifetime of a refresh token in seconds. Defaults to 2592000 seconds / 30 days.

  Setting this to 0 has the following effect:

  * When `RefreshTokenExpiration` is set to `Absolute`, the behavior is the same as when no refresh tokens are used.
  * When `RefreshTokenExpiration` is set to `Sliding`, refresh tokens only expire after the `SlidingRefreshTokenLifetime` has passed.

* **`SlidingRefreshTokenLifetime`**

  Sliding lifetime of a refresh token in seconds. Defaults to 1296000 seconds / 15 days.

* **`RefreshTokenUsage`**

  * **`ReUse`**

    the refresh token handle will stay the same when refreshing tokens. This is the default.

  * **`OneTimeOnly`**

    the refresh token handle will be updated when refreshing tokens.

* **`RefreshTokenExpiration`**

  * **`Absolute`**

    the refresh token will expire on a fixed point in time (specified by the `AbsoluteRefreshTokenLifetime`). This is the default.

  * **`Sliding`**

    when refreshing the token, the lifetime of the refresh token will be renewed (by the amount specified in `SlidingRefreshTokenLifetime`). The lifetime will not exceed `AbsoluteRefreshTokenLifetime`.

* **`UpdateAccessTokenClaimsOnRefresh`**

  Gets or sets a value indicating whether the access token (and its claims) should be updated on a refresh token request.

## Consent Screen

[Section titled “Consent Screen”](#consent-screen)

Consent screen specific settings.

* **`RequireConsent`**

  Specifies whether a consent screen is required. Defaults to `false`.

* **`AllowRememberConsent`**

  Specifies whether user can choose to store consent decisions. Defaults to `true`.

* **`ConsentLifetime`**

  Lifetime of a user consent in seconds. Defaults to null (no expiration).

* **`ClientName`**

  Client display name (used for logging and consent screen).

* **`ClientUri`**

  URI to further information about client.

* **`LogoUri`**

  URI to client logo.

## Cross Device Flows

[Section titled “Cross Device Flows”](#cross-device-flows)

Settings used in the CIBA and OAuth device flows.

* **`PollingInterval`**

  Maximum polling interval for the client in cross device flows. If the client polls more frequently than the polling interval during those flows, it will receive a `slow_down` error response. Defaults to `null`, which means the throttling will use the global default appropriate for the flow (`IdentityServerOptions.Ciba.DefaultPollingInterval` or `IdentityServerOptions.DeviceFlow.Interval`).

#### Device Flow

[Section titled “Device Flow”](#device-flow)

Device flow specific settings.

* **`UserCodeType`**

  Specifies the type of user code to use for the client. Otherwise, falls back to default.

* **`DeviceCodeLifetime`**

  Lifetime to device code in seconds (defaults to 300 seconds / 5 minutes)

#### CIBA

[Section titled “CIBA”](#ciba)

Client initiated backchannel authentication specific settings.

* **`CibaLifetime`**

  Specifies the backchannel authentication request lifetime in seconds. Defaults to `null`.

## DPoP

[Section titled “DPoP”](#dpop)

Added in 6.3.0.

Settings specific to the Demonstration of Proof-of-Possession at the Application Layer ([DPoP](/identityserver/tokens/pop/)) feature.

* **`RequireDPoP`**

  Specifies whether a DPoP (Demonstrating Proof-of-Possession) token is required to be used by this client. Defaults to `false`.

* **`DPoPValidationMode`**

  Enum setting to control validation for the DPoP proof token expiration. This supports both the client generated ‘iat’ value and/or the server generated ‘nonce’ value. Defaults to `DPoPTokenExpirationValidationMode.Iat`, which only validates the ‘iat’ value.

* **`DPoPClockSkew`**

  Clock skew used in validating the client’s DPoP proof token ‘iat’ claim value. Defaults to *5 minutes*.

## Third-Party Initiated Login

[Section titled “Third-Party Initiated Login”](#third-party-initiated-login)

Added in 6.3.0.

* **`InitiateLoginUri`**

  An optional URI that can be used to [initiate login](https://openid.net/specs/openid-connect-core-1_0.html#thirdpartyinitiatedlogin) from the IdentityServer host or a third party. This is most commonly used to create a client application portal within the IdentityServer host. Defaults to null.

## Pushed Authorization Requests

[Section titled “Pushed Authorization Requests”](#pushed-authorization-requests)

Added in 7.0.0

* **`RequirePushedAuthorization`**

  Controls if this client requires PAR. PAR is required if either the global configuration is enabled or if the client’s flag is enabled (this can’t be used to opt out of the global configuration). This defaults to `false`, which means the global configuration will be used.

* **`PushedAuthorizationLifetime`**

  Controls the lifetime of pushed authorization requests for a client. If this lifetime is set, it takes precedence over the global configuration. This defaults to `null`, which means the global configuration is used.
-----
# Grant Validation Result

> Reference documentation for the GrantValidationResult class which models the outcome of grant validation for extension grants and resource owner password grants in Duende IdentityServer.

## Duende.IdentityServer.Validation.GrantValidationResult

[Section titled “Duende.IdentityServer.Validation.GrantValidationResult”](#duendeidentityservervalidationgrantvalidationresult)

The `GrantValidationResult` class models the outcome of grant validation for [extensions grants](/identityserver/tokens/extension-grants/) and [resource owner password grants](/identityserver/tokens/password-grant/).

It models either a successful validation result with claims (e.g. subject ID) or an invalid result with an error code and message, e.g.:

```csharp
public class ExtensionGrantValidator : IExtensionGrantValidator
{
    public Task ValidateAsync(ExtensionGrantValidationContext context)
    {
        // some validation steps


        if (success)
        {
            context.Result = new GrantValidationResult(
                subject: "818727",
                authenticationMethod: "custom",
                claims: extraClaims);
        }
        else
        {
            // custom error message
            context.Result = new GrantValidationResult(
                TokenRequestErrors.InvalidGrant,
                "invalid custom credential");
        }


        return Task.CompletedTask;
    }
}
```

It also allows passing additional custom values that will be included in the token response, e.g.:

```csharp
context.Result = new GrantValidationResult(
    subject: "818727",
    authenticationMethod: "custom",
    customResponse: new Dictionary
    {
        { "some_data", "some_value" }
    });
```

This will result in the following token response:

```json
{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 360,
  "some_data": "some_value"
}
```
-----
# Identity Resource

> Reference documentation for the IdentityResource class which models an identity resource in Duende IdentityServer, including standard and custom identity resources and their properties.

## Duende.IdentityServer.Models.IdentityResource

[Section titled “Duende.IdentityServer.Models.IdentityResource”](#duendeidentityservermodelsidentityresource)

This class models an identity resource.

```csharp
public static readonly IEnumerable IdentityResources =
    new[]
    {
        // some standard scopes from the OIDC spec
        new IdentityResources.OpenId(),
        new IdentityResources.Profile(),
        new IdentityResources.Email(),


        // custom identity resource with some associated claims
        new IdentityResource("custom.profile",
            userClaims: new[] { JwtClaimTypes.Name, JwtClaimTypes.Email, "location", JwtClaimTypes.Address })
    };
```

* **`Enabled`**

  Indicates if this resource is enabled and can be requested. Defaults to true.

* **`Name`**

  The unique name of the identity resource. This is the value a client will use for the scope parameter in the authorize request.

* **`DisplayName`**

  This value will be used e.g. on the consent screen.

* **`Description`**

  This value will be used e.g. on the consent screen.

* **`Required`**

  Specifies whether the user can de-select the scope on the consent screen (if the consent screen wants to implement such a feature). Defaults to false.

* **`Emphasize`**

  Specifies whether the consent screen will emphasize this scope (if the consent screen wants to implement such a feature). Use this setting for sensitive or important scopes. Defaults to false.

* **`ShowInDiscoveryDocument`**

  Specifies whether this scope is shown in the discovery document. Defaults to `true`.

* **`UserClaims`**

  List of associated user claim types that should be included in the identity token.
-----
# Identity Provider

> Reference documentation for identity provider models in Duende IdentityServer, including OidcProvider for external OpenID Connect providers, IdentityProviderName, and the base IdentityProvider class.

## Duende.IdentityServer.Models.OidcProvider

[Section titled “Duende.IdentityServer.Models.OidcProvider”](#duendeidentityservermodelsoidcprovider)

The `OidcProvider` models an external OpenID Connect provider for use in the [dynamic providers](/identityserver/ui/login/dynamicproviders/) feature. Its properties map to the Open ID Connect options class from ASP.NET Core, and those properties include:

* **`Enabled`**

  Specifies if provider is enabled. Defaults to `true`.

* **`Scheme`**

  Scheme name for the provider.

* **`DisplayName`**

  Display name for the provider.

* **`Type`**

  Protocol type of the provider. Defaults to `"oidc"` for the `OidcProvider`.

* **`Authority`**

  The base address of the OIDC provider.

* **`ResponseType`**

  The response type. Defaults to `"id_token"`.

* **`ClientId`**

  The client id.

* **`ClientSecret`**

  The client secret. By default, this is the plaintext client secret and great consideration should be taken if this value is to be stored as plaintext in the store. It is possible to store this in a protected way and then unprotect when loading from the store either by implementing a custom `IIdentityProviderStore` or registering a custom `IConfigureNamedOptions`.

* **`Scope`**

  Space separated list of scope values.

* **`GetClaimsFromUserInfoEndpoint`**

  Indicates if userinfo endpoint is to be contacted. Defaults to true.

* **`UsePkce`**

  Indicates if PKCE should be used. Defaults to true.

#### Duende.IdentityServer.Models.IdentityProviderName

[Section titled “Duende.IdentityServer.Models.IdentityProviderName”](#duendeidentityservermodelsidentityprovidername)

The `IdentityProviderName` models the display name of an identity provider.

* **`Enabled`**

  Specifies if provider is enabled. Defaults to `true`.

* **`Scheme`**

  Scheme name for the provider.

* **`DisplayName`**

  Display name for the provider.

#### Duende.IdentityServer.Models.IdentityProvider

[Section titled “Duende.IdentityServer.Models.IdentityProvider”](#duendeidentityservermodelsidentityprovider)

The `IdentityProvider` is a base class to model arbitrary identity providers, which `OidcProvider` derives from. This leaves open the possibility for extensions to the dynamic provider feature to support other protocol types (as distinguished by the `Type` property).
-----
# License Usage Summary

> Reference documentation for the LicenseUsageSummary class which provides detailed information about clients, issuers, and features used in Duende IdentityServer for self-auditing and license compliance.

## Duende.IdentityServer.Licensing.LicenseUsageSummary

[Section titled “Duende.IdentityServer.Licensing.LicenseUsageSummary”](#duendeidentityserverlicensinglicenseusagesummary)

Added in 7.1

The `LicenseUsageSummary` class allows developers to get a detailed summary of clients, issuers, and features used during the lifetime of an active .NET application for self-auditing purposes.

* **`LicenseEdition`**

  Indicates the current IdentityServer instance’s license edition.

* **`ClientsUsed`**

  A `string` collection of clients used with the current IdentityServer instance.

* **`IssuersUsed`**

  A `string` collection of issuers used with the current IdentityServer instance.

* **`FeaturesUsed`**

  A `string` collection of features has been used since the IdentityServer instance ran.

## Register LicenseUsageSummary Services

[Section titled “Register LicenseUsageSummary Services”](#register-licenseusagesummary-services)

To make the `LicenseUsageSummary` class available in your application, you’ll need to make sure it is registered in the service collection at startup. You can do this by calling the `AddLicenseSummary()` extension method when registering IdentityServer:

Program.cs

```csharp
builder.Services.AddIdentityServer()
    .AddLicenseSummary();
```

## Using LicenseUsageSummary with .NET Lifetime Events

[Section titled “Using LicenseUsageSummary with .NET Lifetime Events”](#using-licenseusagesummary-with-net-lifetime-events)

In .NET, an [`IHost`](https://learn.microsoft.com/en-us/dotnet/api/microsoft.extensions.hosting.ihostapplicationlifetime) implementation allows developers to subscribe to application lifetime events, including **Application Started**, **Application Stopped**, and **Application Stopping**. IdentityServer tracks usage metrics internally and that information may be accessed by developers at any time during the application’s lifetime from the application’s service collection using the following code snippet.

```csharp
// from a valid services scope
app.Services.GetRequiredService();
```

For self-auditing purposes, we recommend using the `IHost` lifetime event `ApplicationStopping` as shown in the example below.

Note, `LicenseUsageSummary` is *`read-only`*.

```csharp
app.Lifetime.ApplicationStopping.Register(() =>
{
  var usage = app.Services.GetRequiredService();
  // Todo: Substitue a different logging mechanism
  Console.Write(Summary(usage));
});
```

Developers may also use common dependency injection techniques such as property or constructor injection.

```csharp
// An ASP.NET Core MVC Controller
public class MyController : Controller
{
    public MyController(LicenseUsageSummary summary)
    {
        // use the summary information
    }
}
```

Developers can use the license usage summary to determine if their organization is within their current licensing tier or if they need to make adjustments to stay within compliance of [Duende licensing terms](https://duendesoftware.com/products/identityserver).
-----
# Secrets

> Reference documentation for secret handling in Duende IdentityServer, including the ISecretParser interface for extracting secrets from HTTP requests, the ParsedSecret class, and the ISecretValidator interface.

## Duende.IdentityServer.Validation.ISecretParser

[Section titled “Duende.IdentityServer.Validation.ISecretParser”](#duendeidentityservervalidationisecretparser)

Parses a secret from the raw HTTP request.

```csharp
public interface ISecretParser
{
    /// 
    /// Tries to find a secret on the context that can be used for authentication
    /// 
    /// The HTTP context.
    /// A parsed secret
    Task ParseAsync(HttpContext context);


    /// 
    /// Returns the authentication method name that this parser implements
    /// 
    /// The authentication method.
    string AuthenticationMethod { get; }
}
```

* **`AuthenticationMethod`**

  The name of the authentication method that this parser registers for. This value must be unique and will be displayed in the discovery document.

* **`ParseAsync`**

  The job of this method is to extract the secret from the HTTP request and parse it into a `ParsedSecret`

#### Duende.IdentityServer.Model.ParsedSecret

[Section titled “Duende.IdentityServer.Model.ParsedSecret”](#duendeidentityservermodelparsedsecret)

Represents a parsed secret.

```csharp
/// 
/// Represents a secret extracted from the HttpContext
/// 
public class ParsedSecret
{
    /// 
    /// Gets or sets the identifier associated with this secret
    /// 
    /// 
    /// The identifier.
    /// 
    public string Id { get; set; }


    /// 
    /// Gets or sets the credential to verify the secret
    /// 
    /// 
    /// The credential.
    /// 
    public object Credential { get; set; }


    /// 
    /// Gets or sets the type of the secret
    /// 
    /// 
    /// The type.
    /// 
    public string Type { get; set; }


    /// 
    /// Gets or sets additional properties.
    /// 
    /// 
    /// The properties.
    /// 
    public Dictionary Properties { get; set; } = new Dictionary();
}
```

The parsed secret is forwarded to the registered secret validator. The validator will typically inspect the `Type` property to determine if this secret is something that can be validated by that validator instance. If yes, it will know how to cast the `Credential` object into a format that is understood.

#### Duende.IdentityServer.Validation.ISecretValidator

[Section titled “Duende.IdentityServer.Validation.ISecretValidator”](#duendeidentityservervalidationisecretvalidator)

Validates a parsed secret.

```csharp
public interface ISecretValidator
{
    /// Validates a secret
    /// The stored secrets.
    /// The received secret.
    /// A validation result
    Task ValidateAsync(
      IEnumerable secrets,
      ParsedSecret parsedSecret);
}
```
-----
# IdentityServer Options

> Documentation of all configuration options in Duende IdentityServer, including settings for key management, endpoints, authentication, events, logging, CORS, Content Security Policy, device flow, mutual TLS, dynamic providers, CIBA, server-side sessions, validation and other core features.

#### Duende.IdentityServer.Configuration.IdentityServerOptions

[Section titled “Duende.IdentityServer.Configuration.IdentityServerOptions”](#duendeidentityserverconfigurationidentityserveroptions)

The `IdentityServerOptions` is the central place to configure fundamental settings in Duende IdentityServer.

You set the options when registering IdentityServer at startup time, using a lambda expression in the AddIdentityServer method:

Program.cs

```csharp
var idsvrBuilder = builder.Services.AddIdentityServer(options =>
{
    // configure options here..
})
```

## Main

[Section titled “Main”](#main)

Top-level settings. Available directly on the `IdentityServerOptions` object.

* **`IssuerUri`**

  The name of the token server, used in the discovery document as the `issuer` claim and in JWT tokens and introspection responses as the `iss` claim.

  It is not recommended to set this option. If it is not set (the default), the issuer is inferred from the URL used by clients. This better conforms to the OpenID Connect specification, which requires that issuer values be “identical to the Issuer URL that was directly used to retrieve the configuration information”. It is also more convenient for clients to validate the issuer of tokens, because they will not need additional configuration or customization to know the expected issuer.

  If you need to access IdentityServer on a different address from the expected issuer value, for example internally in a Kubernetes cluster, setting the issuer is a good practice. Note that when doing so, you will need to set the OpenID Connect metadata address manually in your client application to prevent the address derived from the authority from being used.

* **`LowerCaseIssuerUri`**

  Controls the casing of inferred `IssuerUri`s. When set to `false`, the original casing of the IssuerUri in requests is preserved. When set to `true`, the `IssuerUri` is converted to lowercase. Defaults to `true`.

* **`AccessTokenJwtType`**

  The value used for the `typ` header in JWT access tokens. Defaults to `at+jwt`, as specified by the [RFC 9068](https://datatracker.ietf.org/doc/html/rfc9068). If `AccessTokenJwtType` is set to `null` or the empty string, the `typ` header will not be emitted in JWT access tokens.

* **`LogoutTokenJwtType`**

  The value for the `typ` header in back-channel logout tokens. Defaults to “logout+jwt”, as specified by [OpenID Connect Back-Channel Logout 1.0](https://openid.net/specs/openid-connect-backchannel-1_0.html#logouttoken).

* **`EmitScopesAsSpaceDelimitedStringInJwt`**

  Controls the format of scope claims in JWTs and introspection responses. Historically scopes values were emitted as an array in JWT access tokens. [RFC 9068](https://datatracker.ietf.org/doc/html/rfc9068) now specifies a space delimited string instead. Defaults to `false` for backwards compatibility.

* **`EmitStaticAudienceClaim`**

  Emits a static `aud` (audience) claim in all access tokens with the format `{issuer}/resources`. For example, if IdentityServer was running at `https://identity.example.com`, the static `aud` claim’s value would be `https://identity.example.com/resources`. Historically, older versions of IdentityServer produced tokens with a static audience claim in this format. This flag is intended for use when you need to produce backwards-compatible access tokens. Also note that multiple audience claims are possible. If you enable this flag and also configure `ApiResource`s you can have both the static audience and audiences from the API resources. Defaults to `false`.

* **`EmitIssuerIdentificationResponseParameter`**

  Emits the `iss` response parameter on authorize responses, as specified by [RFC 9207](https://datatracker.ietf.org/doc/rfc9207/). Defaults to `true`.

* **`EmitStateHash`**

  Emits the s\_hash claim in identity tokens. The s\_hash claim is a hash of the state parameter that is specified in the OpenID Connect [Financial-grade API Security Profile](https://openid.net/specs/openid-financial-api-part-2-1_0.html). Defaults to `false`.

* **`StrictJarValidation`**

  Strictly validate JWT-secured authorization requests according to [RFC 9101](https://datatracker.ietf.org/doc/rfc9101/). When enabled, JWTs used to secure authorization requests must have the `typ` header value `oauth-authz-req+jwt` and JWT-secured authorization requests must have the HTTP `content-type` header value `application/oauth-authz-req+jwt`. This might break older OIDC conformant request objects. Defaults to `false`.

* **`ValidateTenantOnAuthorization`** Specifies if a user’s `tenant` claim is compared to the tenant `acr_values` parameter value to determine if the login page is displayed. Defaults to `false`.

## Key management

[Section titled “Key management”](#key-management)

Automatic key management settings. Available on the `KeyManagement` property of the `IdentityServerOptions` object.

* **`Enabled`**

  Enables automatic key management. Defaults to true.

* **`SigningAlgorithms`**

  The signing algorithms for which automatic key management will manage keys.

  This option is configured with a list of objects containing a Name property, which is the name of a supported signing algorithm, and a UseX509Certificate property, which is a flag indicating if the signing key should be wrapped in an X.509 certificate.

  The first algorithm in the collection will be used as the default for clients that do not specify `AllowedIdentityTokenSigningAlgorithms`.

  The supported signing algorithm names are `RS256`, `RS384`, `RS512`, `PS256`, `PS384`, `PS512`, `ES256`, `ES384`, and `ES512`.

  X.509 certificates are not supported for `ES256`, `ES384`, and `ES512` keys.

  Defaults to `RS256` without an X.509 certificate.

Note

*X.509 certificates* have an expiration date, but IdentityServer does not use this data to validate the certificate and throw an exception. If a certificate has expired then you must decide whether to continue using it or replace it with a new certificate.

* **`RsaKeySize`** Key size (in bits) of RSA keys. The signing algorithms that use RSA keys (`RS256`, `RS384`, `RS512`, `PS256`, `PS384`, and `PS512`) will generate an RSA key of this length. Defaults to 2048.

* **`RotationInterval`**

  Age at which keys will no longer be used for signing, but will still be used in discovery for validation. Defaults to 90 days.

* **`PropagationTime`**

  Time expected to propagate new keys to all servers, and time expected all clients to refresh discovery. Defaults to 14 days.

* **`RetentionDuration`**

  Duration for keys to remain in discovery after rotation. Defaults to 14 days.

* **`DeleteRetiredKeys`**

  Automatically delete retired keys. Defaults to true.

* **`KeyPath`**

  Path for storing keys when using the default file system store. Defaults to the “keys” directory relative to the hosting application.

* **`DataProtectKeys`**

  Automatically protect keys in the storage using data protection. Defaults to true.

* **`KeyCacheDuration`**

  When in normal operation, duration to cache keys from store. Defaults to 24 hours.

* **`InitializationDuration`**

  When no keys have been created yet, this is the window of time considered to be an initialization period to allow all servers to synchronize if the keys are being created for the first time. Defaults to 5 minutes.

* **`InitializationSynchronizationDelay`**

  Delay used when re-loading from the store when the initialization period. It allows other servers more time to write new keys so other servers can include them. Defaults to 5 seconds.

* **`InitializationKeyCacheDuration`**

  Cache duration when within the initialization period. Defaults to 1 minute.

## Endpoints

[Section titled “Endpoints”](#endpoints)

Endpoint settings, including flags to disable individual endpoints and support for the request\_uri JAR parameter. Available on the `Endpoints` property of the `IdentityServerOptions` object.

* **`EnableAuthorizeEndpoint`**

  Enables the authorize endpoint. Defaults to true.

* **`EnableTokenEndpoint`**

  Enables the token endpoint. Defaults to true.

* **`EnableDiscoveryEndpoint`**

  Enables the discovery endpoint. Defaults to true.

* **`EnableUserInfoEndpoint`**

  Enables the user info endpoint. Defaults to true.

* **`EnableEndSessionEndpoint`**

  Enables the end session endpoint. Defaults to true.

* **`EnableCheckSessionEndpoint`**

  Enables the check session endpoint. Defaults to true.

* **`EnableTokenRevocationEndpoint`**

  Enables the token revocation endpoint. Defaults to true.

* **`EnableIntrospectionEndpoint`**

  Enables the introspection endpoint. Defaults to true.

* **`EnableDeviceAuthorizationEndpoint`**

  Enables the device authorization endpoint. Defaults to true.

* **`EnableBackchannelAuthenticationEndpoint`**

  Enables the backchannel authentication endpoint. Defaults to true.

* **`EnablePushedAuthorizationEndpoint`**

  Enables the pushed authorization endpoint. Defaults to true.

* **`EnableJwtRequestUri`** Enables the `request_uri` parameter for JWT-Secured Authorization Requests. This allows the JWT to be passed by reference. Disabled by default, due to the security implications of enabling the request\_uri parameter (see [RFC 9101 section 10.4](https://datatracker.ietf.org/doc/rfc9101/)).

## Discovery

[Section titled “Discovery”](#discovery)

Discovery settings, including flags to toggle sections of the discovery document and settings to add custom entries to it. Available on the `Discovery` property of the `IdentityServerOptions` object.

If you want to take full control over the rendering of the discovery and jwks documents, you can implement the `IDiscoveryResponseGenerator` interface (or derive from our default implementation).

* **`ShowEndpoints`**

  Shows endpoints (authorization\_endpoint, token\_endpoint, etc.) in the discovery document. Defaults to true.

* **`ShowKeySet`**

  Shows the jwks\_uri in the discovery document and enables the jwks endpoint. Defaults to true.

* **`ShowIdentityScopes`**

  Includes IdentityResources in the supported\_scopes of the discovery document. Defaults to true.

* **`ShowApiScopes`**

  Includes ApiScopes in the supported\_scopes of the discovery document. Defaults to true.

* **`ShowClaims`**

  Shows claims\_supported in the discovery document. Defaults to true.

* **`ShowResponseTypes`**

  Shows response\_types\_supported in the discovery document. Defaults to true.

* **`ShowResponseModes`**

  Shows response\_modes\_supported in the discovery document. Defaults to true.

* **`ShowGrantTypes`**

  Shows grant\_types\_supported in the discovery document. Defaults to true.

* **`ShowExtensionGrantTypes`**

  Includes extension grant types in the grant\_types\_supported of the discovery document. Defaults to true.

* **`ShowTokenEndpointAuthenticationMethods`**

  Shows token\_endpoint\_auth\_methods\_supported in the discovery document. Defaults to true.

* **`CustomEntries`** Adds custom elements to the discovery document. For example:

Program.cs

```csharp
var idsvrBuilder = builder.Services.AddIdentityServer(options =>
{
    options.Discovery.CustomEntries.Add("my_setting", "foo");
    options.Discovery.CustomEntries.Add("my_complex_setting",
        new
        {
            foo = "foo",
            bar = "bar"
        });
});
```

* **`ExpandRelativePathsInCustomEntries`** Expands paths in custom entries that begin with ”\~/” into absolute paths below the IdentityServer base address. Defaults to true. In the following example, if IdentityServer’s base address is `https://localhost:5001`, then `my_custom_endpoint`’s value will be expanded to `https://localhost:5001/custom`.

```csharp
options.Discovery.CustomEntries.Add("my_custom_endpoint", "~/custom");
```

## Authentication

[Section titled “Authentication”](#authentication)

Login/logout related settings. Available on the `Authentication` property of the `IdentityServerOptions`

* **`CookieAuthenticationScheme`** Sets the cookie authentication scheme configured by the host used for interactive users. If not set, the scheme will be inferred from the host’s default authentication scheme. This setting is typically used when AddPolicyScheme is used in the host as the default scheme.

* **`CookieLifetime`**

  The authentication cookie lifetime (only effective if the IdentityServer-provided cookie handler is used). Defaults to 10 hours.

* **`CookieSlidingExpiration`**

  Specifies if the cookie should be sliding or not (only effective if the IdentityServer-provided cookie handler is used). Defaults to false.

* **`CookieSameSiteMode`**

  Specifies the SameSite mode for the internal cookies. Defaults to None.

* **`RequireAuthenticatedUserForSignOutMessage`**

  Indicates if user must be authenticated to accept parameters to end session endpoint. Defaults to false.

* **`CheckSessionCookieName`**

  The name of the cookie used for the check session endpoint. Defaults to the constant `IdentityServerConstants.DefaultCheckSessionCookieName`, which has the value “idsrv.session”.

* **`CheckSessionCookieDomain`**

  The domain of the cookie used for the check session endpoint. Defaults to `null`.

* **`CheckSessionCookieSameSiteMode`**

  The SameSite mode of the cookie used for the check session endpoint. Defaults to None.

* **`RequireCspFrameSrcForSignout`**

  Enables all content security policy headers on the end session endpoint. For historical reasons, this option’s name mentions `frame-src`, but the content security policy headers on the end session endpoint also include other fetch directives, including a *default-src ‘none’* directive, which prevents most resources from being loaded by the end session endpoint, and a `style-src` directive that specifies the hash of the expected style on the page.

* **`CoordinateClientLifetimesWithUserSession`** (added in `v6.1`) When enabled, all clients’ token lifetimes (e.g. refresh tokens) will be tied to the user’s session lifetime. This means when the user logs out, any revokable tokens will be removed. If using server-side sessions, expired sessions will also remove any revokable tokens, and backchannel logout will be triggered. An individual client can override this setting with its own `CoordinateLifetimeWithUserSession` configuration setting.

## Events

[Section titled “Events”](#events)

Configures which [events](/identityserver/diagnostics/events/) should be raised at the registered event sink.

* **`RaiseSuccessEvents`**

  Enables success events. Defaults to false. Success events include all the events whose names are postfixed with “SuccessEvent”. In general, they are raised when properly formed and valid requests are processed without errors.

* **`RaiseFailureEvents`**

  Enables failure events. Defaults to false. Failure events include all the events whose names are postfixed with “FailureEvent”. In general, they are raised when an action has failed because of incorrect or badly formed parameters in a request. They indicate that the user or client calling IdentityServer has done something wrong and are analogous to a 400: bad request error.

* **`RaiseErrorEvents`**

  Enables Error events. Defaults to false. Error events are raised when an error has occurred, either because of invalid configuration or an unhandled exception. They indicate that there is something wrong within the token server or its configuration and are analogous to a 500: internal server error.

* **`RaiseInformationEvents`**

  Enables Information events. Defaults to false. Information events are emitted when an action has occurred that is of informational interest, but that is neither a success nor a failure. For example, when the end user grants, denies, or revokes consent, that is considered an information event, because these events capture a valid choice of the user rather than success or failure.

## Logging

[Section titled “Logging”](#logging)

Logging related settings, including filters that will remove sensitive values and unwanted exceptions from logs. Available on the `Logging` property of the `IdentityServerOptions` object.

* **`AuthorizeRequestSensitiveValuesFilter`**

  Collection of parameter names passed to the authorize endpoint that are considered sensitive and will be redacted in logs. Note that authorization parameters pushed to the Pushed Authorization Request (PAR) endpoint are eventually handled by the authorize request pipeline. This filter should be configured to exclude sensitive values wether or not they are pushed, and usually should be set to the same value as `PushedAuthorizationSensitiveValuesFilter`. Defaults to `client_secret`, `client_assertion`, `id_token_hint`. The default value was changed in version 7.2.2 to include `client_secret` and `client_assertion`.

* **`PushedAuthorizationSensitiveValuesFilter`**

  Collection of parameter names passed to the Pushed Authorization Request (PAR) endpoint that are considered sensitive and will be redacted in logs. Note that authorization parameters pushed to the PAR endpoint are eventually handled by the authorize request pipeline. This filter should be configured to exclude sensitive values that are pushed, and usually should be set to the same value as `AuthorizeRequestSensitiveValuesFilter`. Defaults to `client_secret`, `client_assertion`, `id_token_hint`.

* **`TokenRequestSensitiveValuesFilter`**

  Collection of parameter names passed to the token endpoint that are considered sensitive and will be redacted in logs. In `v7.0` and earlier, defaults to `client_secret`, `password`, `client_assertion`, `refresh_token`, and `device_code`. In `v7.1`, `subject_token` is also excluded.

* **`BackchannelAuthenticationRequestSensitiveValuesFilter`**

  Collection of parameter names passed to the backchannel authentication endpoint that are considered sensitive and will be redacted in logs. Defaults to `client_secret`, `client_assertion`, and `id_token_hint`.

* **`UnhandledExceptionLoggingFilter`** (added in `v6.2`)

  A function that is called when the IdentityServer middleware detects an unhandled exception, and is used to determine if the exception is logged. The arguments to the function are the HttpContext and the Exception. It should return true to log the exception, and false to suppress. The default is to suppress logging of cancellation-related exceptions when the `CancellationToken` on the `HttpContext` has requested cancellation. Such exceptions are thrown when Http requests are canceled, which is an expected occurrence. Logging them creates unnecessary noise in the logs. In `v7.0` and earlier, only `TaskCanceledException`s were filtered. Beginning in `v7.1`, `OperationCanceledException`s are filtered as well.

## InputLengthRestrictions

[Section titled “InputLengthRestrictions”](#inputlengthrestrictions)

Settings that control the allowed length of various protocol parameters, such as client id, scope, redirect URI etc. Available on the `InputLengthRestrictions` property of the `IdentityServerOptions` object.

* **`ClientId`**

  Max length for ClientId. Defaults to 100.

* **`ClientSecret`**

  Max length for external client secrets. Defaults to 100.

* **`Scope`**

  Max length for scope. Defaults to 300.

* **`RedirectUri`**

  Max length for redirect\_uri. Defaults to 400.

* **`Nonce`**

  Max length for nonce. Defaults to 300.

* **`UiLocale`**

  Max length for ui\_locale. Defaults to 100.

* **`LoginHint`**

  Max length for login\_hint. Defaults to 100.

* **`AcrValues`**

  Max length for acr\_values. Defaults to 300.

* **`GrantType`**

  Max length for grant\_type. Defaults to 100.

* **`UserName`**

  Max length for username. Defaults to 100.

* **`Password`**

  Max length for password. Defaults to 100.

* **`CspReport`**

  Max length for CSP reports. Defaults to 2000.

* **`IdentityProvider`**

  Max length for external identity provider name. Defaults to 100.

* **`ExternalError`**

  Max length for external identity provider errors. Defaults to 100.

* **`AuthorizationCode`**

  Max length for authorization codes. Defaults to 100.

* **`DeviceCode`**

  Max length for device codes. Defaults to 100.

* **`RefreshToken`**

  Max length for refresh tokens. Defaults to 100.

* **`TokenHandle`**

  Max length for token handles. Defaults to 100.

* **`Jwt`**

  Max length for JWTs. Defaults to 51200.

* **`CodeChallengeMinLength`**

  Min length for the code challenge. Defaults to 43.

* **`CodeChallengeMaxLength`**

  Max length for the code challenge. Defaults to 128.

* **`CodeVerifierMinLength`**

  Min length for the code verifier. Defaults to 43.

* **`CodeVerifierMaxLength`**

  Max length for the code verifier. Defaults to 128.

* **`ResourceIndicatorMaxLength`**

  Max length for resource indicator parameter. Defaults to 512.

* **`BindingMessage`**

  Max length for binding\_message. Defaults to 100.

* **`UserCode`**

  Max length for user\_code. Defaults to 100.

* **`IdTokenHint`**

  Max length for id\_token\_hint. Defaults to 4000.

* **`LoginHintToken`**

  Max length for login\_hint\_token. Defaults to 4000.

* **`AuthenticationRequestId`** Max length for auth\_req\_id. Defaults to 100.

## UserInteraction

[Section titled “UserInteraction”](#userinteraction)

User interaction settings, including urls for pages in the UI, names of parameters to those pages, and other settings related to interactive flows. Available on the `UserInteraction` property of the `IdentityServerOptions` object.

* **`LoginUrl`**, **`LogoutUrl`**, **`ConsentUrl`**, **`ErrorUrl`**, **`DeviceVerificationUrl`**

  Sets the URLs for the login, logout, consent, error and device verification pages.

* **`CreateAccountUrl`** (added in `v6.3`)

  Sets the URL for the create account page, which is used by OIDC requests that include the `prompt=create` parameter. When this option is set, including the `prompt=create` parameter will cause the user to be redirected to the specified url. `create` will also be added to the discovery document’s `prompt_values_supported` array to announce support for this feature. When this option is not set, the `prompt=create` parameter is ignored, and `create` is not added to discovery. Defaults to `null`.

* **`LoginReturnUrlParameter`**

  Sets the name of the return URL parameter passed to the login page. Defaults to `returnUrl`.

* **`LogoutIdParameter`**

  Sets the name of the logout message id parameter passed to the logout page. Defaults to `logoutId`.

* **`ConsentReturnUrlParameter`**

  Sets the name of the return URL parameter passed to the consent page. Defaults to `returnUrl`.

* **`ErrorIdParameter`**

  Sets the name of the error message id parameter passed to the error page. Defaults to `errorId`.

* **`CustomRedirectReturnUrlParameter`**

  Sets the name of the return URL parameter passed to a custom redirect from the authorization endpoint. Defaults to `returnUrl`.

* **`DeviceVerificationUserCodeParameter`**

  Sets the name of the user code parameter passed to the device verification page. Defaults to `userCode`.

* **`CookieMessageThreshold`**

  Certain interactions between IdentityServer and some UI pages require a cookie to pass state and context (any of the pages above that have a configurable “message id” parameter). Since browsers have limits on the number of cookies and their size, this setting is used to prevent too many cookies being created. The value sets the maximum number of message cookies of any type that will be created. The oldest message cookies will be purged once the limit has been reached. This effectively indicates how many tabs can be opened by a user when using IdentityServer. Defaults to 2.

* **`AllowOriginInReturnUrl`**

  Flag that allows return URL validation to accept full URL that includes the IdentityServer origin. Defaults to `false`.

* **`PromptValuesSupported`** (added in `v7.0.7`)

  The collection of OIDC prompt modes supported and that will be published in discovery. By default, this includes all values in `Constants.SupportedPromptModes`. If the `CreateAccountUrl` option is set, then the “create” value is also included. If additional prompt values are added, a customized [`IAuthorizeInteractionResponseGenerator"`](/identityserver/ui/custom/) is also required to handle those values.

## Caching

[Section titled “Caching”](#caching)

Caching settings for the stores. Available on the `Caching` property of the `IdentityServerOptions` object. These settings only apply if the respective caching has been enabled in the services configuration in startup.

* **`ClientStoreExpiration`**

  Cache duration of client configuration loaded from the client store. Defaults to 15 minutes.

* **`ResourceStoreExpiration`**

  Cache duration of identity and API resource configuration loaded from the resource store. Defaults to 15 minutes.

* **`CorsExpiration`**

  Cache duration of CORS configuration loaded from the CORS policy service. Defaults to 15 minutes.

* **`IdentityProviderCacheDuration`**

  Cache duration of identity provider configuration loaded from the identity provider store. Defaults to 60 minutes.

* **`CacheLockTimeout`**

  The timeout for concurrency locking in the default cache. Defaults to 60 seconds.

## CORS

[Section titled “CORS”](#cors)

CORS settings for IdentityServer’s endpoints. Available on the `Cors` property of the `IdentityServerOptions` object. The underlying CORS implementation is provided from ASP.NET Core, and as such it is automatically registered in the dependency injection system.

* **`CorsPolicyName`**

  Name of the CORS policy that will be evaluated for CORS requests into IdentityServer. Defaults to `IdentityServer`. The policy provider that handles this is implemented in terms of the `ICorsPolicyService` registered in the dependency injection system. If you wish to customize the set of CORS origins allowed to connect, then it is recommended that you provide a custom implementation of `ICorsPolicyService`.

* **`CorsPaths`**

  The endpoints within IdentityServer where CORS is supported. Defaults to the discovery, user info, token, and revocation endpoints.

* **`PreflightCacheDuration`**

  Indicates the value to be used in the preflight `Access-Control-Max-Age` response header. Defaults to `null` indicating no caching header is set on the response.

## Content Security Policy

[Section titled “Content Security Policy”](#content-security-policy)

Settings for Content Security Policy (CSP) headers that IdentityServer emits. Available on the `Csp` property of the `IdentityServerOptions` object.

* **`Level`**

  The level of CSP to use. CSP Level 2 is used by default, but this can be changed to `CspLevel.One` to accommodate older browsers.

* **`AddDeprecatedHeader`** Indicates if the older `X-Content-Security-Policy` CSP header should also be emitted in addition to the standards-based header value. Defaults to `true`.

## Device Flow

[Section titled “Device Flow”](#device-flow)

OAuth device flow settings. Available on the `DeviceFlow` property of the `IdentityServerOptions` object.

* **`DefaultUserCodeType`**

  The user code type to use, unless set at the client level. Defaults to `Numeric`, a 9-digit code.

* **`Interval`**

  The maximum frequency in seconds that a client may poll the token endpoint in the device flow. Defaults to `5`.

## Mutual TLS

[Section titled “Mutual TLS”](#mutual-tls)

[Mutual TLS](/identityserver/tokens/client-authentication/) settings. Available on the `MutualTls` property of the `IdentityServerOptions` object.

Program.cs

```csharp
var builder = services.AddIdentityServer(options =>
{
    options.MutualTls.Enabled = true;


    // use mtls subdomain
    options.MutualTls.DomainName = "mtls";


    options.MutualTls.AlwaysEmitConfirmationClaim = true;
})
```

* **`Enabled`**

  Specifies if MTLS support should be enabled. Defaults to `false`.

* **`ClientCertificateAuthenticationScheme`**

  Specifies the name of the authentication handler for X.509 client certificates. Defaults to `Certificate`.

* **`DomainName`**

  Specifies either the name of the subdomain or full domain for running the MTLS endpoints. MTLS will use path-based endpoints if not set (the default). Use a simple string (e.g. “mtls”) to set a subdomain, use a full domain name (e.g. “identityserver-mtls.io”) to set a full domain name. When a full domain name is used, you also need to set the `IssuerUri` to a fixed value.

* **`AlwaysEmitConfirmationClaim`**

  Specifies whether a cnf claim gets emitted for access tokens if a client certificate was present. Normally the cnf claims only gets emitted if the client used the client certificate for authentication, setting this to true, will set the claim regardless of the authentication method. Defaults to false.

## PersistentGrants

[Section titled “PersistentGrants”](#persistentgrants)

Shared settings for persisted grants behavior.

* **`DataProtectData`**

  Data protect the persisted grants “data” column. Defaults to `true`. If your database is already protecting data at rest, then you can consider disabling this.

* **`DeleteOneTimeOnlyRefreshTokensOnUse`** (added in `v6.3`)

  When Refresh tokens that are configured with RefreshTokenUsage.OneTime are used, this option controls if they will be deleted immediately or retained and marked as consumed. The default is on - immediately delete.

## Dynamic Providers

[Section titled “Dynamic Providers”](#dynamic-providers)

Settings for [dynamic providers](/identityserver/ui/login/dynamicproviders/). Available on the `DynamicProviders` property of the `IdentityServerOptions` object.

* **`PathPrefix`**

  Prefix in the pipeline for callbacks from external providers. Defaults to “/federation”.

* **`SignInScheme`**

  Scheme used for signin. Defaults to the constant `IdentityServerConstants.ExternalCookieAuthenticationScheme`, which has the value “idsrv.external”.

* **`SignOutScheme`** Scheme for signout. Defaults to the constant `IdentityServerConstants.DefaultCookieAuthenticationScheme`, which has the value “idsrv”.

## CIBA

[Section titled “CIBA”](#ciba)

[CIBA](/identityserver/ui/ciba/) settings. Available on the `Ciba` property of the `IdentityServerOptions` object.

* **`DefaultLifetime`**

  The default lifetime of the pending authentication requests in seconds. Defaults to 300.

* **`DefaultPollingInterval`** The maximum frequency in seconds that a client may poll the token endpoint in the CIBA flow. Defaults to 5.

## Server-Side Sessions

[Section titled “Server-Side Sessions”](#server-side-sessions)

Settings for [server-side sessions](/identityserver/ui/server-side-sessions/). Added in `v6.1`. Available on the `ServerSideSessions` property of the `IdentityServerOptions` object.

* **`UserDisplayNameClaimType`**

  Claim type used for the user’s display name. Unset by default due to possible PII concerns. If used, this would commonly be `JwtClaimTypes.Name`, `JwtClaimType.Email` or a custom claim.

* **`RemoveExpiredSessions`**

  Enables periodic cleanup of expired sessions. Defaults to true.

* **`RemoveExpiredSessionsFrequency`**

  Frequency that expired sessions will be removed. Defaults to 10 minutes.

* **`RemoveExpiredSessionsBatchSize`**

  Number of expired session records to be removed at a time. Defaults to 100.

* **`ExpiredSessionsTriggerBackchannelLogout`**

  If enabled, when server-side sessions are removed due to expiration, back-channel logout notifications will be sent. This will, in effect, tie a user’s session lifetime at a client to their session lifetime at IdentityServer. Defaults to false.

* **`FuzzExpiredSessionRemovalStart`**

  The background session cleanup job runs at a configured interval. If multiple nodes run the cleanup job at the same time update conflicts might occur in the store. To reduce the propability of that happening, the startup time can be fuzzed. The first run is scheduled at a random time between the host startup and the configured RemoveExpiredSessionsFrequency. Subsequent runs are run on the configured RemoveExpiredSessionsFrequency. Defaults to `true`.

## Validation

[Section titled “Validation”](#validation)

* **`InvalidRedirectUriPrefixes`**

  Collection of URI scheme prefixes that should never be used as custom URI schemes in the `redirect_uri` passed to tha authorize endpoint or the `post_logout_redirect_uri` passed to the end\_session endpoint. Defaults to *\[“javascript:”, “file:”, “data:”, “mailto:”, “ftp:”, “blob:”, “about:”, “ssh:”, “tel:”, “view-source:”, “ws:”, “wss:”]*.

## DPoP

[Section titled “DPoP”](#dpop)

Added in 6.3.0.

Demonstration of Proof-of-Possession settings. Available on the `DPoP` property of the `IdentityServerOptions` object.

* **`ProofTokenValidityDuration`**

  Duration that DPoP proof tokens are considered valid. Defaults to *1 minute*.

* **`ServerClockSkew`** Clock skew used in validating DPoP proof token expiration using a server-generated nonce value. Defaults to `0`.

## Pushed Authorization Requests

[Section titled “Pushed Authorization Requests”](#pushed-authorization-requests)

[Pushed Authorization Requests (PAR)](/identityserver/tokens/par/) settings. Added in `v7.0`. Available on the `PushedAuthorization` property of the `IdentityServerOptions` object.

* **`Required`**

  Causes PAR to be required globally. Defaults to `false`.

* **`Lifetime`**

  Controls the lifetime of pushed authorization requests. The pushed authorization request’s lifetime begins when the request to the PAR endpoint is received, and is validated until the authorize endpoint returns a response to the client application. Note that user interaction, such as entering credentials or granting consent, may need to occur before the authorize endpoint can do so. Setting the lifetime too low will likely cause login failures for interactive users, if pushed authorization requests expire before those users complete authentication. Some security profiles, such as the FAPI 2.0 Security Profile recommend an expiration within 10 minutes to prevent attackers from pre-generating requests. To balance these constraints, this lifetime defaults to 10 minutes.

## Diagnostics

[Section titled “Diagnostics”](#diagnostics)

[Diagnostic data](/identityserver/diagnostics/data/) settings. Added in `v7.3`. Available on the `Diagnostics` property of the `IdentityServerOptions` object.

* **`LogFrequency`**

  Frequency at which the diagnostic data is logged. Defaults to 1 hour.

* **`ChunkSize`**

  Maximum size of diagnostic data log message chunks in kilobytes. Defaults to 8160 bytes. 8 KB is a conservative limit for the max size of a log message that is imposed by some logging tools. We take 32 bytes less than that to allow for additional formatting of the log message.

## Preview Features

[Section titled “Preview Features”](#preview-features)

Preview Features settings. Available on the `Preview` property of the `IdentityServerOptions` object.

Note

Duende IdentityServer may ship preview features, which can be configured using preview options. Note that preview features can be removed and may break in future releases.

#### Discovery Document Cache

[Section titled “Discovery Document Cache”](#discovery-document-cache)

In large deployments of Duende IdentityServer, where a lot of concurrent users attempt to consume the [discovery endpoint](/identityserver/reference/v7/endpoints/discovery/) to retrieve metadata about your IdentityServer, you can increase throughput by enabling the discovery document cache preview using the *`EnableDiscoveryDocumentCache`* flag. This will cache discovery document information for the duration specified in the *`DiscoveryDocumentCacheDuration`* option.

It’s best to keep the cache time low if you use the *`CustomEntries`* element on the discovery document or implement a custom *`IDiscoveryResponseGenerator`*.

#### Strict Audience Validation

[Section titled “Strict Audience Validation”](#strict-audience-validation)

When using [*private key JWT*](/identityserver/tokens/client-authentication/#private-key-jwts), there is a theoretical vulnerability where a Relying Party trusting multiple OpenID Providers could be attacked if one of the OpenID Providers is malicious or compromised.

The OpenID Foundation proposed a two-part fix: strictly validate the audience and set an explicit `typ` header in the authentication JWT.

You can [enable strict audience validation in Duende IdentityServer](/identityserver/tokens/client-authentication/#strict-audience-validation) using the *`StrictClientAssertionAudienceValidation`* flag, which strictly validates that the audience is equal to the issuer and validates the token’s `typ` header.
-----
# Scope Parser

> Reference documentation for IScopeParser, which parses raw scope strings from authorization and token requests into structured ParsedScopeValue objects.

The `IScopeParser` interface is responsible for parsing the raw `scope` parameter from OAuth/OIDC requests into individual, structured scope values. While the default implementation treats scopes as simple space-delimited strings, custom implementations enable **parameterized scopes** - scopes that carry dynamic data like transaction IDs, tenant identifiers, or resource-specific parameters.

## When to Use

[Section titled “When to Use”](#when-to-use)

Use a custom scope parser when:

* Scopes carry runtime parameters (e.g., `transaction:abc123`, `tenant:acme`)
* You need to validate scope structure before further processing
* Scope values follow a convention that requires extraction of embedded data

## Interface

[Section titled “Interface”](#interface)

#### Duende.IdentityServer.Validation.IScopeParser

[Section titled “Duende.IdentityServer.Validation.IScopeParser”](#duendeidentityservervalidationiscopeparser)

```csharp
/// 
/// Allows parsing raw scopes values into structured scope values.
/// 
public interface IScopeParser
{
    /// 
    /// Parses the requested scopes.
    /// 
    ParsedScopesResult ParseScopeValues(IEnumerable scopeValues);
}
```

### ParseScopeValues

[Section titled “ParseScopeValues”](#parsescopevalues)

Receives the scope values as a collection (already split from the space-delimited request parameter) and returns a `ParsedScopesResult` containing:

* **`ParsedScopes`** - A collection of `ParsedScopeValue` objects, each with:

  * `RawValue` - The original scope string segment
  * `ParsedName` - The scope name (e.g., `"transaction"`)
  * `ParsedParameter` - The extracted parameter, if any (e.g., `"123"`)

* **`Errors`** - Any parsing errors encountered

* **`Succeeded`** - Whether parsing completed without errors

## Default Implementation

[Section titled “Default Implementation”](#default-implementation)

The `DefaultScopeParser` iterates over the scope values and creates a `ParsedScopeValue` for each. Override the virtual `ParseScopeValue` method to add custom parsing logic for parameterized scopes:

```csharp
public class ParameterizedScopeParser : DefaultScopeParser
{
    public ParameterizedScopeParser(ILogger logger) : base(logger)
    { }


    public override void ParseScopeValue(ParseScopeContext scopeContext)
    {
        // Custom parsing logic here
        // Call base.ParseScopeValue(scopeContext) for standard scopes
    }
}
```

### DefaultScopeParser.ParseScopeContext

[Section titled “DefaultScopeParser.ParseScopeContext”](#defaultscopeparserparsescopecontext)

The `ParseScopeContext` is a nested class within `DefaultScopeParser` that provides:

| Member                             | Description                                               |
| ---------------------------------- | --------------------------------------------------------- |
| `RawValue`                         | The original scope string being parsed                    |
| `ParsedName`                       | The parsed scope name (read after `SetParsedValues`)      |
| `ParsedParameter`                  | The parsed parameter value (read after `SetParsedValues`) |
| `Error`                            | The error message if parsing failed                       |
| `Ignore`                           | Whether this scope should be excluded from results        |
| `Succeeded`                        | `true` if not ignored and no error                        |
| `SetParsedValues(name, parameter)` | Sets the parsed scope name and parameter                  |
| `SetIgnore()`                      | Marks this scope to be excluded from results              |
| `SetError(message)`                | Marks this scope as invalid with an error message         |

## Common Scenarios

[Section titled “Common Scenarios”](#common-scenarios)

### Transaction or Resource IDs

[Section titled “Transaction or Resource IDs”](#transaction-or-resource-ids)

Scopes like `transaction:abc123` or `document:read:456` embed resource identifiers:

```csharp
public override void ParseScopeValue(ParseScopeContext scopeContext)
{
    const string prefix = "transaction:";


    if (scopeContext.RawValue.StartsWith(prefix))
    {
        var parameter = scopeContext.RawValue.Substring(prefix.Length);
        if (!string.IsNullOrEmpty(parameter))
        {
            scopeContext.SetParsedValues("transaction", parameter);
            return;
        }
        scopeContext.SetError("transaction scope requires a parameter");
        return;
    }


    base.ParseScopeValue(scopeContext);
}
```

Access the parsed parameter downstream in your `IProfileService`:

```csharp
var txScope = context.RequestedResources.ParsedScopes
    .FirstOrDefault(x => x.ParsedName == "transaction");


if (txScope?.ParsedParameter != null)
{
    context.IssuedClaims.Add(new Claim("transaction_id", txScope.ParsedParameter));
}
```

### Multi-Tenant Scopes

[Section titled “Multi-Tenant Scopes”](#multi-tenant-scopes)

Encode tenant context in scopes like `tenant:acme:read`:

```csharp
public override void ParseScopeValue(ParseScopeContext scopeContext)
{
    // Pattern: tenant:{tenant_id}:{permission}
    if (scopeContext.RawValue.StartsWith("tenant:"))
    {
        var parts = scopeContext.RawValue.Split(':', 3);
        if (parts.Length == 3)
        {
            // Store as "tenant:{permission}" with tenant ID as parameter
            scopeContext.SetParsedValues($"tenant:{parts[2]}", parts[1]);
            return;
        }
    }


    base.ParseScopeValue(scopeContext);
}
```

### Dynamic Scope Validation

[Section titled “Dynamic Scope Validation”](#dynamic-scope-validation)

Reject malformed or unauthorized scope patterns early:

```csharp
public override void ParseScopeValue(ParseScopeContext scopeContext)
{
    // Reject scopes with invalid characters
    if (scopeContext.RawValue.Contains("..") || scopeContext.RawValue.Contains("//"))
    {
        scopeContext.SetError("Invalid scope format");
        return;
    }


    // Ignore internal/debug scopes in production
    if (scopeContext.RawValue.StartsWith("debug:"))
    {
        scopeContext.SetIgnore();
        return;
    }


    base.ParseScopeValue(scopeContext);
}
```

## Registration

[Section titled “Registration”](#registration)

Register your custom scope parser in `ConfigureServices`:

```csharp
builder.Services.AddIdentityServer()
    .AddScopeParser();
```

## See Also

[Section titled “See Also”](#see-also)

[API Scopes](/identityserver/fundamentals/resources/api-scopes/)Learn about defining and using API scopes for access control

[Resource Validator](/identityserver/reference/v7/validators/resource-validator/)Validate whether requested scopes are allowed for a client
-----
# Response Generators

> An overview of IdentityServer's response generation pattern and customization options for protocol endpoint responses.

IdentityServer’s endpoints follow a pattern of abstraction in which a response generator uses a validated input model to produce a response model. The response model is a type that represents the data that will be returned from the endpoint. The response model is then wrapped in a result model, which is a type that facilitates serialization by an implementation of `IHttpResponseWriter`.

Customization of protocol endpoint responses is possible in both the response generators and response writers. Response generator customization is appropriate when you want to change the “business logic” of an endpoint and is typically accomplished by overriding virtual methods in the default response generator. Response writer customization is appropriate when you want to change the serialization, encoding, or headers of the HTTP response and is accomplished by registering a custom implementation of the `IHttpResponseWriter`.
-----
# Authorize Interaction Response Generator

> Documentation for the IAuthorizeInteractionResponseGenerator interface which determines if a user must log in or consent when making requests to the authorization endpoint.

#### Duende.IdentityServer.ResponseHandling.IAuthorizeInteractionResponseGenerator

[Section titled “Duende.IdentityServer.ResponseHandling.IAuthorizeInteractionResponseGenerator”](#duendeidentityserverresponsehandlingiauthorizeinteractionresponsegenerator)

The `IAuthorizeInteractionResponseGenerator` interface models the logic for determining if user must log in or consent when making requests to the authorization endpoint.

Note

If a custom implementation of `IAuthorizeInteractionResponseGenerator` is desired, then it’s [recommended](/identityserver/ui/custom/#built-in-authorizeinteractionresponsegenerator) to derive from the built-in `AuthorizeInteractionResponseGenerator` to inherit all the default logic pertaining to log in and consent semantics.

## IAuthorizeInteractionResponseGenerator APIs

[Section titled “IAuthorizeInteractionResponseGenerator APIs”](#iauthorizeinteractionresponsegenerator-apis)

* **`ProcessInteractionAsync`**

  Returns the `InteractionResponse` based on the `ValidatedAuthorizeRequest` an and optional `ConsentResponse` if the user was shown a consent page.

## InteractionResponse

[Section titled “InteractionResponse”](#interactionresponse)

* **`IsLogin`**

  Specifies if the user must log in.

* **`IsConsent`**

  Specifies if the user must consent.

* **`IsCreateAccount`**

  Added in `v6.3`.

  Specifies if the user must create an account.

* **`IsError`**

  Specifies if the user must be shown an error page.

* **`Error`**

  The error to display on the error page.

* **`ErrorDescription`**

  The description of the error to display on the error page.

* **`IsRedirect`**

  Specifies if the user must be redirected to a custom page for custom processing.

* **`RedirectUrl`**

  The URL for the redirect to the page for custom processing.
-----
# IHttpResponseWriter

> Documentation for the IHttpResponseWriter interface, a low-level abstraction for customizing serialization, encoding, and HTTP headers in protocol endpoint responses.

The `IHttpResponseWriter` interface is the contract for services that can produce HTTP responses for `IEndpointResult`s. This is a low-level abstraction that is intended to be used if you need to customize the serialization, encoding, or HTTP headers in a response from a protocol endpoint.

#### Duende.IdentityServer.Hosting.IHttpResponseWriter

[Section titled “Duende.IdentityServer.Hosting.IHttpResponseWriter”](#duendeidentityserverhostingihttpresponsewriter)

```csharp
/// 
/// Contract for a service that writes appropriate http responses for  objects.
/// 
public interface IHttpResponseWriter
    where T : IEndpointResult
{
    /// 
    /// Writes the endpoint result to the HTTP response.
    /// 
    Task WriteHttpResponse(T result, HttpContext context);
}
```

#### Duende.IdentityServer.Hosting.IEndpointResult

[Section titled “Duende.IdentityServer.Hosting.IEndpointResult”](#duendeidentityserverhostingiendpointresult)

```csharp
/// 
/// An  is the object model that describes the
/// results that will returned by one of the protocol endpoints provided by
/// IdentityServer, and can be executed to produce an HTTP response.
/// 
public interface IEndpointResult
{
    /// 
    /// Executes the result to write an http response.
    /// 
    /// The HTTP context.
    Task ExecuteAsync(HttpContext context);
}
```
-----
# Token Response Generator

> Documentation for the ITokenResponseGenerator interface and its implementation, which generates responses to valid token endpoint requests with customization options for different token flows.

## Duende.IdentityServer.ResponseHandling.ITokenResponseGenerator

[Section titled “Duende.IdentityServer.ResponseHandling.ITokenResponseGenerator”](#duendeidentityserverresponsehandlingitokenresponsegenerator)

The `ITokenResponseGenerator` interface is the contract for the service that generates responses to valid requests to the token endpoint. A response in this context refers to an object model that describes the content that will be serialized and transmitted in the HTTP response.

The default implementation is the `TokenResponseGenerator` class. You can customize the behavior of the token endpoint by providing your own implementation of the `ITokenResponseGenerator` to the ASP.NET Core service provider.

To create a customized implementation of `ITokenResponseGenerator`, we recommend that you create a class that derives from the default implementation. Your custom implementation should override the appropriate virtual methods of the default implementation and add your custom behavior to those overrides, possibly calling the base methods first and then manipulating their results.

## ITokenResponseGenerator

[Section titled “ITokenResponseGenerator”](#itokenresponsegenerator)

The `ITokenResponseGenerator` contains a single method to process validated token requests and return token responses.

* **`ProcessInteractionAsync`**

  Returns the `TokenResponse` based on the `ValidatedTokenRequest`.

## TokenResponseGenerator

[Section titled “TokenResponseGenerator”](#tokenresponsegenerator)

The default implementation of the `ITokenResponseGenerator` contains virtual methods that can be overridden to customize particular behavior for particular token requests.

* **`ProcessAsync`**

  Returns the `TokenResponse` for any `TokenRequestValidationResult`.

* **`ProcessClientCredentialsRequestAsync`**

  Returns the `TokenResponse` for a `TokenRequestValidationResult` from the client credentials flow.

* **`ProcessPasswordRequestAsync`**

  Returns the `TokenResponse` for a `TokenRequestValidationResult` from the resource owner password flow.

* **`ProcessAuthorizationCodeRequestAsync`**

  Returns the `TokenResponse` for a `TokenRequestValidationResult` from the authorization code flow.

* **`ProcessRefreshTokenRequestAsync`**

  Returns the `TokenResponse` for a `TokenRequestValidationResult` from the refresh token flow.

* **`ProcessDeviceCodeRequestAsync`**

  Returns the `TokenResponse` for a `TokenRequestValidationResult` from the device code flow.

* **`ProcessCibaRequestAsync`**

  Returns the `TokenResponse` for a `TokenRequestValidationResult` from the CIBA flow.

* **`ProcessExtensionGrantRequestAsync`**

  Returns the `TokenResponse` for a `TokenRequestValidationResult` from an extension grant.

* **`CreateAccessTokenAsync`**

  Creates an access token and optionally a refresh token.

* **`CreateIdTokenFromRefreshTokenRequestAsync`**

  Creates an ID token in a refresh token request.

## TokenResponse

[Section titled “TokenResponse”](#tokenresponse)

The `TokenResponse` class represents the data that will be included in the body of the response returned from the token endpoint. It contains properties for the various tokens that can be returned, the scope and expiration of the access token, and a mechanism for adding custom properties to the result. Omitting property values will cause the entire property to be absent from the response.

* **`IdentityToken`**

  The identity token.

* **`AccessToken`**

  The access token.

* **`RefreshToken`**

  The refresh token.

* **`AccessTokenLifetime`**

  The access token lifetime in seconds.

* **`Scope`**

  The scope.

* **`Custom`**

  A dictionary of strings to objects that will be serialized to json and added to the token response.
-----
# Backchannel Authentication Interaction Service

> Documentation for the IBackchannelAuthenticationInteractionService interface which provides services for accessing and completing CIBA login requests.

#### Duende.IdentityServer.Services.IBackchannelAuthenticationInteractionService

[Section titled “Duende.IdentityServer.Services.IBackchannelAuthenticationInteractionService”](#duendeidentityserverservicesibackchannelauthenticationinteractionservice)

The `IBackchannelAuthenticationInteractionService` interface provides services for a user to access or complete a login requests for [CIBA](/identityserver/ui/ciba/). It is available from the dependency injection system and would normally be injected as a constructor parameter into your MVC controllers for the user interface of IdentityServer.

## IBackchannelAuthenticationInteractionService APIs

[Section titled “IBackchannelAuthenticationInteractionService APIs”](#ibackchannelauthenticationinteractionservice-apis)

* **`GetPendingLoginRequestsForCurrentUserAsync`**

  Returns a collection of [BackchannelUserLoginRequest](/identityserver/reference/v7/models/ciba-login-request/) objects which represent pending login requests for the current user.

* **`GetLoginRequestByInternalIdAsync`**

  Returns the [BackchannelUserLoginRequest](/identityserver/reference/v7/models/ciba-login-request/) object for the id.

* **`CompleteLoginRequestAsync`**

  Completes the login request with the provided `CompleteBackchannelLoginRequest` response for the current user or the subject passed.

### CompleteBackchannelLoginRequest

[Section titled “CompleteBackchannelLoginRequest”](#completebackchannelloginrequest)

Models the data needed for a user to complete a backchannel authentication request.

* **`InternalId`**

  The internal store id for the request.

* **`ScopesValuesConsented`**

  Gets or sets the scope values consented to. Setting any scopes grants the login request. Leaving the scopes null or empty denies the request.

* **`Description`**

  Gets or sets the optional description to associate with the consent.

* **`Subject`**

  The subject for which the completion is being made. This allows more claims to be associated with the request that was identified on the backchannel authentication request. If not provided, then the `IUserSession` service will be consulting to obtain the current subject.

* **`SessionId`**

  The session id to associate with the completion request if the Subject is provided. If the Subject is not provided, then this property is ignored in favor of the session id provided by the `IUserSession` service.
-----
# Backchannel Authentication User Notification Service

> Documentation for the IBackchannelAuthenticationUserNotificationService interface which is used to notify users when a CIBA login request has been made.

#### Duende.IdentityServer.Services.IBackchannelAuthenticationUserNotificationService

[Section titled “Duende.IdentityServer.Services.IBackchannelAuthenticationUserNotificationService”](#duendeidentityserverservicesibackchannelauthenticationusernotificationservice)

The `IBackchannelAuthenticationUserNotificationService` interface is used to contact users when a [CIBA](/identityserver/ui/ciba/) login request has been made. To use CIBA, you are expected to implement this interface and register it in the ASP.NET Core service provider.

## IBackchannelAuthenticationUserNotificationService APIs

[Section titled “IBackchannelAuthenticationUserNotificationService APIs”](#ibackchannelauthenticationusernotificationservice-apis)

* **`SendLoginRequestAsync`**

  Sends a notification for the user to login via the [BackchannelUserLoginRequest](/identityserver/reference/v7/models/ciba-login-request/) parameter.
-----
# Device Flow Interaction Service

> Documentation for the IDeviceFlowInteractionService interface which provides services for user interfaces to communicate with IdentityServer during device flow authorization.

#### Duende.IdentityServer.Services.IDeviceFlowInteractionService

[Section titled “Duende.IdentityServer.Services.IDeviceFlowInteractionService”](#duendeidentityserverservicesideviceflowinteractionservice)

The `IDeviceFlowInteractionService` interface is intended to provide services to be used by the user interface to communicate with Duende IdentityServer during device flow authorization. It is available from the dependency injection system and would normally be injected as a constructor parameter into your MVC controllers for the user interface of IdentityServer.

## IDeviceFlowInteractionService APIs

[Section titled “IDeviceFlowInteractionService APIs”](#ideviceflowinteractionservice-apis)

* **`GetAuthorizationContextAsync(string userCode)`**

  Returns the `DeviceFlowAuthorizationRequest` based on the `userCode` passed to the login or consent pages.

* **`HandleRequestAsync(string userCode, ConsentResponse consent)`**

  Completes device authorization for the given `userCode`.

## DeviceFlowAuthorizationRequest

[Section titled “DeviceFlowAuthorizationRequest”](#deviceflowauthorizationrequest)

* **`Client`**

  The client that initiated the device authorization request.

* **`ValidatedResources`**

  The validated resources (scopes and resource indicators) requested by the client.

## DeviceFlowInteractionResult

[Section titled “DeviceFlowInteractionResult”](#deviceflowinteractionresult)

* **`IsError`**

  Specifies if the authorization request errored.

* **`IsAccessDenied`**

  Gets or sets a value indicating whether the user denied access.

* **`ErrorDescription`**

  Error description upon failure.

* **`Failure(string errorDescription = null)`** *(static method)*

  Creates a `DeviceFlowInteractionResult` indicating failure with an optional error description.
-----
# IdentityServer Interaction Service

> Documentation for the IIdentityServerInteractionService interface which provides services for user interfaces to communicate with IdentityServer for authorization, consent, logout, and other user interactions.

#### Duende.IdentityServer.Services.IIdentityServerInteractionService

[Section titled “Duende.IdentityServer.Services.IIdentityServerInteractionService”](#duendeidentityserverservicesiidentityserverinteractionservice)

The `IIdentityServerInteractionService` interface is intended to provide services to be used by the user interface to communicate with IdentityServer, mainly pertaining to user interaction. It is available from the dependency injection system and would normally be injected as a constructor parameter into your MVC controllers for the user interface of IdentityServer.

## IIdentityServerInteractionService APIs

[Section titled “IIdentityServerInteractionService APIs”](#iidentityserverinteractionservice-apis)

* **`GetAuthorizationContextAsync`**

  Returns the `AuthorizationRequest` based on the `returnUrl` passed to the login or consent pages.

* **`IsValidReturnUrl`**

  Indicates if the `returnUrl` is a valid URL for redirect after login or consent.

* **`GetErrorContextAsync`**

  Returns the `ErrorMessage` based on the `errorId` passed to the error page.

* **`GetLogoutContextAsync`**

  Returns the `LogoutRequest` based on the `logoutId` passed to the logout page.

* **`CreateLogoutContextAsync`**

  Used to create a `logoutId` if there is not one presently. This creates a cookie capturing all the current state needed for signout and the `logoutId` identifies that cookie. This is typically used when there is no current `logoutId` and the logout page must capture the current user’s state needed for sign-out prior to redirecting to an external identity provider for signout. The newly created `logoutId` would need to be roundtripped to the external identity provider at signout time, and then used on the signout callback page in the same way it would be on the normal logout page.

* **`GrantConsentAsync`**

  Accepts a `ConsentResponse` to inform IdentityServer of the user’s consent to a particular `AuthorizationRequest`.

* **`DenyAuthorizationAsync`**

  Accepts a `AuthorizationError` to inform IdentityServer of the error to return to the client for a particular `AuthorizationRequest`.

* **`GetAllUserGrantsAsync`**

  Returns a collection of `Grant` for the user. These represent a user’s consent or a clients access to a user’s resource.

* **`RevokeUserConsentAsync`**

  Revokes all of a user’s consents and grants for a client.

* **`RevokeTokensForCurrentSessionAsync`**

  Revokes all of a user’s consents and grants for clients the user has signed in to during their current session.

## Returned models

[Section titled “Returned models”](#returned-models)

The above methods return various models.

### AuthorizationRequest

[Section titled “AuthorizationRequest”](#authorizationrequest)

* **`Client`**

  The client that initiated the request.

* **`RedirectUri`**

  The URI to redirect the user to after successful authorization.

* **`DisplayMode`**

  The display mode passed from the authorization request.

* **`UiLocales`**

  The UI locales passed from the authorization request.

* **`IdP`** The external identity provider requested. This is used to bypass home realm discovery (HRD). This is provided via the “idp:” prefix to the `acr_values` parameter on the authorize request.

* **`Tenant`**

  The tenant requested. This is provided via the “tenant:” prefix to the `acr_values` parameter on the authorize request.

* **`LoginHint`**

  The expected username the user will use to login. This is requested from the client via the `login_hint` parameter on the authorize request.

* **`PromptMode`**

  The prompt mode requested from the authorization request.

* **`AcrValues`**

  The acr values passed from the authorization request.

* **`ValidatedResources`**

  The `ResourceValidationResult` which represents the validated resources from the authorization request.

* **`Parameters`**

  The entire parameter collection passed to the authorization request.

* **`RequestObjectValues`**

  The validated contents of the request object (if present).

### ResourceValidationResult

[Section titled “ResourceValidationResult”](#resourcevalidationresult)

* **`Resources`**

  The resources of the result.

* **`ParsedScopes`**

  The parsed scopes represented by the result.

* **`RawScopeValues`**

  The original (raw) scope values represented by the validated result.

### ErrorMessage

[Section titled “ErrorMessage”](#errormessage)

* **`Error`**

  The error code.

* **`ErrorDescription`**

  The error description.

* **`DisplayMode`**

  The display mode passed from the authorization request.

* **`UiLocales`**

  The UI locales passed from the authorization request.

* **`RequestId`**

  The per-request identifier. This can be used to display to the end user and can be used in diagnostics.

* **`ClientId`**

  The client id making the request (if available).

* **`RedirectUri`**

  The redirect URI back to the client (if available).

### LogoutRequest

[Section titled “LogoutRequest”](#logoutrequest)

* **`ClientId`**

  The client identifier that initiated the request.

* **`PostLogoutRedirectUri`**

  The URL to redirect the user to after they have logged out.

* **`SessionId`**

  The user’s current session id.

* **`SignOutIFrameUrl`**

  The URL to render in an `