Modern OpenID Connect & OAuth 2.0 client library for .NET 10 and .NET Standard 2.0
Install
| Product | Versions |
|---|---|
| .NET | |
| .NET Standard |
README
IdentityModel for .NET
Modern OpenID Connect and OAuth 2.0 client library for .NET 10 and .NET Standard 2.0.
The package provides net10.0 and netstandard2.0 assets. Native AOT and trimming
analysis are supported by the net10.0 build; the .NET Standard asset uses internal
compatibility shims for APIs introduced after .NET Standard 2.0. Request options are
target-specific: net10.0 exposes System.Net.Http.HttpRequestOptions, while
netstandard2.0 exposes the library-owned Duende.IdentityModel.Protocol.RequestOptions.
In both assets, values are copied onto the outgoing HttpRequestMessage (Options on
.NET 5+, Properties on .NET Standard 2.0).
Installation
dotnet add package Duende.Labs.IdentityModel
Note This package is experimental/preview. For production scenarios, consider Duende.IdentityModel.
Typed client
IdentityModel uses named HttpClient instances and named options. Register an authority once:
services.AddIdentityModelClient(options =>
{
options.Authority = new Uri("https://demo.duendesoftware.com");
options.DefaultClientCredential = new ClientCredential("m2m", "secret");
options.Scope = "api";
});
Inject IIdentityModelClientFactory and asynchronously create an initialized client:
public sealed class Worker(IIdentityModelClientFactory factory)
{
public async Task RunAsync(CancellationToken ct)
{
var creation = await factory.CreateAsync(ct);
if (!creation.IsSuccess)
{
// Inspect creation.Error.
return;
}
var client = creation.Response;
if (!client.SupportsClientCredentials)
{
return;
}
var token = await client.RequestClientCredentialsTokenAsync(ct);
}
}
The factory retrieves and validates discovery, resolves protocol endpoints, and returns an
initialized client. Its endpoints and capabilities only change when you call
RefreshDiscoveryAsync. Calls do not require callers to copy endpoint URIs out of discovery or
construct endpoint-bearing request objects.
Capabilities
The initialized client exposes capabilities such as SupportsClientCredentials,
SupportsUserInfo, SupportsDeviceAuthorization, and SupportsPushedAuthorization.
Capability validation is strict by default. Strict mode checks endpoint presence and relevant
advertised metadata, including grant types and client authentication methods. An unsupported call
fails locally with ProtocolErrorType.UnsupportedFeature.
Providers with incomplete discovery documents can use relaxed mode:
services.AddIdentityModelClient(options =>
{
options.Authority = new Uri("https://provider.example.com");
options.CapabilityValidationMode = CapabilityValidationMode.Relaxed;
});
Relaxed mode requires a resolved endpoint but does not block a call because other advertised metadata is missing.
Endpoint overrides and limited clients
Explicit endpoint overrides take precedence over discovery and may be absolute or relative to the authority:
services.AddIdentityModelClient(options =>
{
options.Authority = new Uri("https://provider.example.com");
options.TokenEndpoint = new Uri("/oauth/token", UriKind.Relative);
});
If discovery fails but one or more explicit endpoints are usable, creation succeeds with a limited
client. DiscoveryError preserves the failure and only explicitly configured capabilities are
available. Creation fails when discovery fails and no usable override exists.
Named clients
services.AddIdentityModelClient("customers", options => { /* ... */ });
services.AddIdentityModelClient("partners", options => { /* ... */ });
var customers = await factory.CreateAsync("customers", ct);
Each factory call uses the current named options and creates a fresh client. The factory does not retain typed clients. Discovery documents are cached by URL, so clients that share an authority share one cached document, and each client still validates it against its own policy.
Per-call customization
Overloads accept a transform for operation-specific values:
var result = await client.RequestClientCredentialsTokenAsync(
request => request with { Scope = "api.read" },
ct);
The client supplies endpoint and configured defaults before invoking the transform.
HTTP handlers and resilience
AddIdentityModelClient returns the backing IHttpClientBuilder, so handlers apply to discovery
and protocol requests:
services
.AddIdentityModelClient(options => { /* ... */ })
.AddHttpMessageHandler<MyHandler>()
.AddStandardResilienceHandler();
Discovery and JSON Web Key Set caching
Discovery documents and JSON Web Key Sets are cached through
HybridCache, so creating a
client or reading keys usually doesn't hit the authority. Concurrent requests for the same document
share one fetch, and a failed fetch or a document that fails validation is never cached.
Entries are shared by URL across clients. The client that writes an entry on a miss or refresh sets
its lifetime, so give named clients that share an authority or JWKS URL the same cache
durations.
| Option | Default | Description |
|---|---|---|
CacheDiscovery |
true |
Cache the discovery document |
DiscoveryCacheDuration |
24 hours | How long a discovery document stays cached |
CacheJsonWebKeySet |
true |
Cache the JSON Web Key Set |
JsonWebKeySetCacheDuration |
1 hour | How long a key set stays cached |
JsonWebKeySetRefreshInterval |
5 minutes | Minimum time between forced key set refreshes for one URL. TimeSpan.Zero removes the limit |
When a key rotates before the cache expires, call RefreshJsonWebKeySetAsync to fetch the set again
and replace the cache entry. A token's kid is attacker-controlled, so forced refreshes are limited
to one per JsonWebKeySetRefreshInterval. Within that window the call returns the cached set.
RefreshDiscoveryAsync fetches discovery again, replaces the cache entry, and updates the client's
endpoints and Supports* flags in place. It also turns a limited client into a fully discovered one
once the authority is reachable. A failed refresh leaves the client unchanged.
var keys = await client.GetJsonWebKeySetAsync(ct);
if (!keys.Response.Keys.Any(key => key.Kid == kid))
{
keys = await client.RefreshJsonWebKeySetAsync(ct);
}
AddIdentityModelClient registers the default HybridCache. Register an IDistributedCache, such
as Redis, to share cached documents across instances. To keep IdentityModel's entries apart from the
rest of your application, register a keyed HybridCache under
IdentityModelCacheKeys.DiscoveryCache or IdentityModelCacheKeys.JsonWebKeySetCache. Entries are
tagged with IdentityModelCacheKeys.DiscoveryTag and IdentityModelCacheKeys.JsonWebKeySetTag, so
RemoveByTagAsync clears them.
Prefer RFC 9111 HTTP caching? Turn off the built-in caches and add a caching handler to the
IHttpClientBuilder. The BuiltInCaching sample shows the built-in
caches in action. The Caching sample shows RFC 9111 HTTP caching with
DamianH.HttpHybridCacheHandler, which honours the server's caching headers.
HybridCache reports trim and AOT warnings from its reflection-based JSON fallback serializer (dotnet/extensions#5624). IdentityModel only caches strings, which HybridCache serializes without reflection, so these warnings are safe to ignore in Native AOT apps.
Configuration binding
services.AddIdentityModelClient(builder.Configuration.GetSection("IdentityProvider"));
Recognized keys include Authority, ClientId, ClientSecret, ClientCredentialStyle, Scope,
CapabilityValidationMode, the cache settings (CacheDiscovery, DiscoveryCacheDuration,
CacheJsonWebKeySet, JsonWebKeySetCacheDuration, JsonWebKeySetRefreshInterval), and endpoint overrides. Durations use the
invariant TimeSpan format, for example 01:00:00. Values are read explicitly to preserve trimming
and Native AOT compatibility.
Features
The typed client supports:
- Discovery and JSON Web Keys
- Client credentials, authorization code, and refresh token requests
- The obsolete password grant (
DUENDEIM0001), only to migrate existing applications - Token introspection and revocation
- UserInfo
- Device authorization, device token requests, and device token polling
- Pushed Authorization Requests and Rich Authorization Requests
- CIBA backchannel authentication, token requests, and token polling
- Dynamic client registration and RFC 7592 registration management
Generic SetBearerToken helpers remain available for setting an OAuth bearer token on an
HttpClient or HttpRequestMessage.
License
IdentityModel is open source under the Apache 2.0 license.
Changes from Previous Version
Adoption Assessment
A verified publisher signal is available. Assess fit from the package's supported frameworks, license and dependency graph rather than popularity alone.
Related Guides & Workflows
Learn how to evaluate, integrate, or audit this package in your .NET project.