---
name: token-management
description: Token management patterns using Duende.AccessTokenManagement. Covers client credential token caching, user token refresh, token storage, HttpClientFactory integration, DPoP support, and common configuration pitfalls. Also includes Blazor Server token management.
invocable: false
---

# Token Management

## When to Use This Skill

Use this skill when:
- Building a .NET worker service or daemon that calls APIs using the client credentials flow
- Building an ASP.NET Core web application that calls APIs on behalf of the currently logged-in user
- Integrating `Duende.AccessTokenManagement` or `Duende.AccessTokenManagement.OpenIdConnect` with `IHttpClientFactory`
- Configuring token caching — in-memory, distributed (Redis), or hybrid — for machine-to-machine tokens
- Adding DPoP (Demonstrating Proof-of-Possession) key binding to access tokens
- Implementing API-to-API delegation where a downstream service calls further APIs with either user tokens or client credentials
- Revoking refresh tokens on user sign-out

## Core Principles

1. **Prefer Automatic Over Manual** — Use `IHttpClientFactory`-integrated clients; they acquire, cache, refresh, and attach tokens transparently. Call `GetAccessTokenAsync` manually only when the factory pattern is insufficient.
2. **Never Cache Tokens in Code** — The library owns the cache. Do not store tokens in instance fields, static variables, or application-managed caches. Call the service on every request and let it serve from cache.
3. **`SaveTokens = true` Is Required for User Tokens** — The OIDC handler must persist tokens into the authentication session. This is the most common misconfiguration.
4. **Refresh Tokens Must Be Revoked at Sign-Out** — Call `e.HttpContext.RevokeRefreshTokenAsync()` in `OnSigningOut` to revoke the refresh token at the authorization server, preventing reuse after logout.
5. **v4 Uses `HybridCache`; v3 Uses `IDistributedCache`** — The caching layer changed between major versions. v4's `HybridCache` is two-tier and automatic; v3 requires an explicit `AddDistributedMemoryCache()` or Redis registration.
6. **Resiliency Is Included in `AddClientCredentialsHttpClient`** — This registration adds a once-retry handler for `401 Unauthorized` responses (handles token expiry and DPoP nonce challenges). When using `AddClientCredentialsTokenHandler` directly, add it explicitly.

## Related Skills

- `aspnetcore-authentication` — cookie and OIDC handler setup required for user token management
- `identityserver-configuration` — configuring the authorization server that issues tokens
- `oauth-oidc-protocols` — protocol fundamentals underlying client credentials and refresh token flows
- `duende-bff` — BFF pattern integrates this library automatically for proxied API calls

Docs: https://docs.duendesoftware.com/accesstokenmanagement/

---

## Pattern 1: Machine-to-Machine (Client Credentials) — Worker Services

### Package

```bash
dotnet add package Duende.AccessTokenManagement
```

### Registration

```csharp
// ✅ Register one or more named client definitions
services.AddClientCredentialsTokenManagement()
    .AddClient("catalog.client", client =>
    {
        client.TokenEndpoint = new Uri("https://sts.company.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("invoice.client", client =>
    {
        client.TokenEndpoint = new Uri("https://sts.company.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");
    });
```

Available client options:
- `TokenEndpoint` — URL of the OAuth token endpoint
- `ClientId` / `ClientSecret` — client credentials
- `ClientCredentialStyle` — `AuthorizationHeader` (default) or `PostBody`
- `Scope` — requested scope (optional; overridable per request)
- `Resource` — resource indicator per RFC 8707 (optional)
- `HttpClientName` — custom backchannel HTTP client name from the factory
- `DPoPJsonWebKey` — JWK for DPoP-bound tokens (see Pattern 5)

### Automatic via HttpClientFactory (Recommended)

```csharp
// ✅ Named client — token acquired, cached, and attached automatically
services.AddClientCredentialsHttpClient(
    "invoices",
    ClientCredentialsClientName.Parse("invoice.client"),
    client => { client.BaseAddress = new Uri("https://apis.company.com/invoice/"); });

// ✅ Typed client — identical behaviour, strongly typed
services.AddHttpClient<CatalogClient>(client =>
    {
        client.BaseAddress = new Uri("https://apis.company.com/catalog/");
    })
    .AddClientCredentialsTokenHandler(ClientCredentialsClientName.Parse("catalog.client"));
```

Usage — no token code required at the call site:

```csharp
public sealed class WorkerHttpClient(IHttpClientFactory factory) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // ✅ Token acquired, cached, and refreshed transparently
            var client = factory.CreateClient("invoices");
            var response = await client.GetAsync("list", stoppingToken);
            // ...
        }
    }
}
```

> **Resiliency handler** — `AddClientCredentialsHttpClient` automatically adds a resiliency handler that retries once on `401 Unauthorized`. This covers token expiry and DPoP nonce challenges. When using `AddClientCredentialsTokenHandler` directly, add it explicitly:
>
> ```csharp
> services.AddHttpClient<CatalogClient>(...)
>     .AddDefaultAccessTokenResiliency()
>     .AddClientCredentialsTokenHandler("catalog.client");
> ```

### Manual Token Retrieval (Advanced)

```csharp
// ✅ Inject IClientCredentialsTokenManager (v4)
public sealed class WorkerManual(
    IHttpClientFactory factory,
    IClientCredentialsTokenManager tokenManager) : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            var tokenResult = await tokenManager.GetAccessTokenAsync(
                ClientCredentialsClientName.Parse("catalog.client"),
                ct: stoppingToken);

            if (!tokenResult.Succeeded)
            {
                // log and handle — do not call .GetToken() without checking first
                await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
                continue;
            }

            var token = tokenResult.GetToken();
            var client = factory.CreateClient();
            client.SetBearerToken(token.AccessToken.ToString());
            var response = await client.GetAsync("https://apis.company.com/catalog/list", stoppingToken);
            // ...
        }
    }
}
```

> In v3, the service was `IClientCredentialsTokenManagementService` and the result was read via `.Value`. In v4 it is `IClientCredentialsTokenManager` and the result is `TokenResult<ClientCredentialsToken>` — use `.Succeeded` / `.GetToken()`.

---

## Pattern 2: User Token Management — Web Applications

### Package

```bash
dotnet add package Duende.AccessTokenManagement.OpenIdConnect
```

### Registration

```csharp
// ✅ Full setup: cookie + OIDC handler + token management
builder.Services.AddAuthentication(options =>
    {
        options.DefaultScheme = "cookie";
        options.DefaultChallengeScheme = "oidc";
    })
    .AddCookie("cookie", options =>
    {
        options.Cookie.Name = "web";
        // ✅ Revoke refresh token at sign-out
        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();
        options.Scope.Add("openid");
        options.Scope.Add("profile");
        options.Scope.Add("email");
        options.Scope.Add("invoice");
        options.Scope.Add("offline_access"); // ← required for refresh tokens

        options.GetClaimsFromUserInfoEndpoint = true;
        options.MapInboundClaims = false;

        // ✅ REQUIRED — persists access and refresh tokens into the auth session
        options.SaveTokens = true;
    });

// ✅ Adds all token management services
builder.Services.AddOpenIdConnectAccessTokenManagement();
```

> **`SaveTokens = true` is mandatory.** Without it, the library cannot read or refresh the user's access token. This is the most common misconfiguration causing `InvalidOperationException` at runtime.

### Automatic via HttpClientFactory (Recommended)

```csharp
// ✅ Named client using the current user's access token
builder.Services.AddUserAccessTokenHttpClient(
    "invoices",
    configureClient: client =>
    {
        client.BaseAddress = new Uri("https://api.company.com/invoices/");
    });

// ✅ Typed client using the current user's access token
builder.Services.AddHttpClient<InvoiceClient>(client =>
    {
        client.BaseAddress = new Uri("https://api.company.com/invoices/");
    })
    .AddUserAccessTokenHandler();

// ✅ Named client using a client credentials token (machine-to-machine, user-independent)
builder.Services.AddClientAccessTokenHttpClient(
    "masterdata.client",
    configureClient: client =>
    {
        client.BaseAddress = new Uri("https://api.company.com/masterdata/");
    });

// ✅ Typed client using a client credentials token
builder.Services.AddHttpClient<MasterDataClient>(client =>
    {
        client.BaseAddress = new Uri("https://api.company.com/masterdata/");
    })
    .AddClientAccessTokenHandler();
```

Usage in a controller:

```csharp
public sealed class ApiController(IHttpClientFactory httpClientFactory) : Controller
{
    public async Task<IActionResult> CallApi(CancellationToken ct)
    {
        // ✅ Token attached automatically; refreshed silently if expired
        var client = httpClientFactory.CreateClient("invoices");
        var response = await client.GetAsync("list", ct);
        // ...
    }
}
```

### Manual Token Retrieval (Advanced)

```csharp
// ✅ v4 — inject IUserTokenManager
public sealed class HomeController(
    IHttpClientFactory httpClientFactory,
    IUserTokenManager userTokenManager) : Controller
{
    public async Task<IActionResult> CallApi(CancellationToken ct)
    {
        var token = await userTokenManager.GetAccessTokenAsync(User, ct: ct);
        var client = httpClientFactory.CreateClient();
        client.SetBearerToken(token.Value);
        var response = await client.GetAsync("https://api.company.com/invoices", ct);
        // ...
    }
}
```

`HttpContext` extension methods are also available:

```csharp
// ✅ User access token — refreshed automatically via refresh token if expired
var userToken = await HttpContext.GetUserAccessTokenAsync();

// ✅ Client credentials token — re-requested from the token server if expired
var clientToken = await HttpContext.GetClientAccessTokenAsync();

// ✅ Revoke refresh token explicitly (also wired into OnSigningOut above)
await HttpContext.RevokeRefreshTokenAsync();
```

### gRPC Support

Use `AddUserAccessTokenHandler` and `AddClientAccessTokenHandler` when registering typed gRPC clients:

```csharp
// ✅ gRPC client using the current user's access token
builder.Services.AddGrpcClient<Greeter.GreeterClient>(o =>
{
    o.Address = new Uri("https://grpc.company.com");
})
.AddUserAccessTokenHandler();

// ✅ gRPC client using a client credentials token
builder.Services.AddGrpcClient<Inventory.InventoryClient>(o =>
{
    o.Address = new Uri("https://grpc.company.com");
})
.AddClientAccessTokenHandler();
```

---

## Pattern 3: Token Caching

### v4 — HybridCache (Default)

In v4, client credentials tokens are cached using `HybridCache` (ASP.NET Core 9+). It is two-tier: in-memory L1 + optional remote L2. No explicit registration is required for the default in-memory tier.

```csharp
// ✅ Add a distributed remote cache (e.g., Redis) — HybridCache picks it up automatically as L2
builder.Services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = builder.Configuration.GetConnectionString("Redis");
});
```

Global cache options:

```csharp
// ✅ Configure lifetime buffer and key prefix
services.AddClientCredentialsTokenManagement(options =>
{
    // Cache tokens 60 s before they expire to avoid serving a near-expired token
    options.CacheLifetimeBuffer = 60;
    options.CacheKeyPrefix = "MyApp.ATM::";
});
```

Default cache key format:

```
{CacheKeyPrefix}::{client_name}::hashed({scope})::hashed({resource})
```

`scope` and `resource` values are MD5-hashed to keep key length bounded. Implement `IClientCredentialsCacheKeyGenerator` to supply custom keys when adding custom `TokenRequestParameters`.

### v3 — IDistributedCache

```csharp
// ✅ v3: must explicitly register a distributed cache implementation
services.AddDistributedMemoryCache(); // development / single-instance only

// ✅ v3: Redis for production
services.AddStackExchangeRedisCache(options =>
{
    options.Configuration = "redis.company.com:6379";
});

services.AddClientCredentialsTokenManagement(options =>
{
    options.CacheLifetimeBuffer = 60;
});
```

### Encrypting Cached Tokens (v4)

When sharing a remote cache with other applications, encrypt tokens at rest using a custom `IHybridCacheSerializer<ClientCredentialsToken>`:

```csharp
// ✅ Register the encrypted serializer for the ClientCredentialsToken type
services.AddHybridCache()
    .AddSerializer<ClientCredentialsToken, EncryptedHybridCacheSerializer>();

services.AddDataProtection();

public sealed class EncryptedHybridCacheSerializer : IHybridCacheSerializer<ClientCredentialsToken>
{
    private readonly IDataProtector _protector;

    public EncryptedHybridCacheSerializer(IDataProtectionProvider provider)
    {
        _protector = provider.CreateProtector("ClientCredentialsToken");
    }

    public ClientCredentialsToken Deserialize(ReadOnlySequence<byte> source)
    {
        var unprotected = _protector.Unprotect(source.ToArray());
        return JsonSerializer.Deserialize<ClientCredentialsToken>(unprotected)!;
    }

    public void Serialize(ClientCredentialsToken value, IBufferWriter<byte> target)
    {
        var json = JsonSerializer.SerializeToUtf8Bytes(value);
        target.Write(_protector.Protect(json));
    }
}
```

### Scoping a Custom Cache to This Library Only

```csharp
// ✅ Inject a custom HybridCache only for AccessTokenManagement (uses service keys)
services.AddKeyedSingleton<HybridCache>(
    ServiceProviderKeys.ClientCredentialsTokenCache,
    new MyCustomCacheImplementation());
```

---

## Pattern 4: Configuration Options

### `ClientCredentialsTokenManagementOptions`

```csharp
services.AddClientCredentialsTokenManagement(options =>
{
    options.CacheLifetimeBuffer = 60;            // seconds subtracted from token lifetime in cache
    options.CacheKeyPrefix = "MyApp.ATM::";      // prefix for all cache keys
});
```

### `UserTokenManagementOptions`

```csharp
builder.Services.AddOpenIdConnectAccessTokenManagement(options =>
{
    // Override the OIDC challenge scheme if not using the default
    options.ChallengeScheme = "oidc";

    // Enable separate token stores per OIDC scheme (multi-provider setups)
    options.UseChallengeSchemeScopedTokens = false;

    // Scope and resource sent when requesting client credentials tokens from
    // the configured OIDC provider (cannot be inferred from OIDC metadata)
    options.ClientCredentialsScope = "api1 api2";
    options.ClientCredentialsResource = "urn:myapi";

    // How client credentials are sent to the token endpoint
    options.ClientCredentialStyle = ClientCredentialStyle.PostBody;

    // DPoP key for all user token requests from this application
    options.DPoPJsonWebKey = jwk;
});
```

### Per-Request Parameter Overrides

```csharp
// ✅ Force a fresh token even if a cached one exists
var token = await tokenManager.GetAccessTokenAsync(
    ClientCredentialsClientName.Parse("catalog.client"),
    new TokenRequestParameters { ForceRenewal = true },
    ct: stoppingToken);

// ✅ Override scope per user token request
var token = await userTokenManager.GetAccessTokenAsync(
    User,
    new UserTokenRequestParameters
    {
        Scope = "invoice:write",
        ForceRenewal = false,
        ChallengeScheme = "oidc"
    },
    ct: ct);
```

For `IHttpClientFactory` clients, parameters are wired at registration time:

```csharp
builder.Services.AddUserAccessTokenHttpClient(
    "invoices",
    parameters: new UserTokenRequestParameters { ForceRenewal = true },
    configureClient: client => { client.BaseAddress = new Uri("https://api.company.com/invoices/"); });
```

---

## Pattern 5: DPoP (Demonstrating Proof-of-Possession)

DPoP binds an access token to a client-held asymmetric key, preventing replay attacks even if the token is stolen. Configure it by setting `DPoPJsonWebKey` on the client credentials client or `UserTokenManagementOptions`, and optionally implement `IDPoPKeyStore` for runtime key rotation.

> **Full details in sub-document** — See [`docs/dpop.md`](docs/dpop.md) for JWK generation, per-client configuration, `IDPoPKeyStore`, session size implications, and the key-persistence pitfall.

---

## Pattern 6: API-to-API Token Delegation

An API can either forward the user's access token to a downstream API (when the downstream accepts the same audience) or use a dedicated client credentials token (when the downstream requires a service identity).

> **Full details in sub-document** — See [`docs/api-delegation.md`](docs/api-delegation.md) for both approaches with complete code examples and a decision guide.

---

## Pattern 7: Dynamic Client Configuration

Use `IConfigureNamedOptions<ClientCredentialsClient>` when token endpoint configuration must be resolved at runtime — for example, from OIDC discovery:

```csharp
public sealed class ClientCredentialsConfigureOptions(DiscoveryCache cache)
    : IConfigureNamedOptions<ClientCredentialsClient>
{
    public void Configure(string? name, ClientCredentialsClient options)
    {
        if (name == "catalog.client")
        {
            // ✅ Resolve token endpoint from OIDC discovery document
            var disco = cache.GetAsync().GetAwaiter().GetResult();
            options.TokenEndpoint = new Uri(disco.TokenEndpoint);
            options.ClientId = ClientId.Parse("...");
            options.ClientSecret = ClientSecret.Parse("...");
            options.Scope = Scope.Parse("catalog");
        }
    }

    public void Configure(ClientCredentialsClient options) => Configure(string.Empty, options);
}

// Registration
services.AddClientCredentialsTokenManagement();
services.AddSingleton(new DiscoveryCache("https://sts.company.com"));
services.AddSingleton<IConfigureOptions<ClientCredentialsClient>, ClientCredentialsConfigureOptions>();
```

---

## Pattern 8: Custom Token Storage

### User Tokens — Replace the Default Cookie Session Store

By default, user access and refresh tokens are stored inside the ASP.NET Core authentication cookie. Replace `IUserTokenStore` when this is insufficient — for example, when using server-side sessions:

```csharp
// ✅ Register a custom implementation backed by server-side session storage
builder.Services.AddSingleton<IUserTokenStore, ServerSideSessionUserTokenStore>();
```

### Client Credentials — Replace the Cache Implementation

```csharp
// ✅ Override the entire cache with a custom IClientCredentialsTokenCache
services.AddSingleton<IClientCredentialsTokenCache, MyCustomTokenCache>();
```

---

## Pattern 9: Blazor Server Token Management

Blazor Server circuits outlive the initial HTTP request. Once a circuit is established, `HttpContext` is `null`, making the default cookie-based `IUserTokenStore` unusable. Use `AddBlazorServerAccessTokenManagement<T>()` with a persistent `IUserTokenStore` (e.g., database-backed), and capture tokens in `OnTokenValidated` during the initial OIDC flow.

> **Full details in sub-document** — See [`docs/blazor-server.md`](docs/blazor-server.md) for the full `IUserTokenStore` implementation, `OnTokenValidated` setup, and the `HttpContext`-null pitfall.

---

## Pattern 10: Client Assertions (private_key_jwt)

Use `IClientAssertionService` to authenticate with signed JWTs instead of shared client secrets. **CRITICAL**: set the JWT `Audience` to the authorization server's issuer URL — NOT the token endpoint URL. Using the token endpoint URL was the root cause of CVE-2025-27370 and CVE-2025-27371.

> **Full details in sub-document** — See [`docs/client-assertions.md`](docs/client-assertions.md) for the full `IClientAssertionService` implementation, correct audience configuration, and CVE context.

---

## Pattern 11: Custom Token Request Customization

Use `ITokenRequestCustomizer` (v4) to dynamically modify token request parameters per outgoing HTTP request — useful for multi-tenant scenarios where different tenants require different API resources or scopes. Use the `with` expression to create a modified copy of `baseParameters`; do not mutate it.

> **Full details in sub-document** — See [`docs/customization.md`](docs/customization.md) for the full `ITokenRequestCustomizer` implementation and registration pattern.

---

## Pattern 12: Custom Token Retrieval

Implement `AccessTokenRequestHandler.ITokenRetriever` to completely replace the default token retrieval logic with custom selection or caching behavior.

> **Full details in sub-document** — See [`docs/customization.md`](docs/customization.md) for the full `ITokenRetriever` implementation and `AddHttpMessageHandler` registration pattern.

---

## Sub-Documents

Load these sub-documents when the user's question specifically targets one of these areas:

| Document | Description | When to Load |
|----------|-------------|--------------|
| [docs/dpop.md](docs/dpop.md) | DPoP proof-of-possession token binding | DPoP, `DPoPJsonWebKey`, `IDPoPKeyStore`, key generation, key rotation |
| [docs/blazor-server.md](docs/blazor-server.md) | Blazor Server circuit-scoped token management | Blazor, SignalR circuit, `HttpContext` null, `AddBlazorServerAccessTokenManagement` |
| [docs/client-assertions.md](docs/client-assertions.md) | Client assertions (private_key_jwt) + CVE guidance | `IClientAssertionService`, JWT client auth, CVE-2025-27370, CVE-2025-27371 |
| [docs/customization.md](docs/customization.md) | `ITokenRequestCustomizer` & `ITokenRetriever` | Multi-tenant token params, custom token retrieval, `AccessTokenRequestHandler` |
| [docs/api-delegation.md](docs/api-delegation.md) | API-to-API delegation patterns | Downstream API calls, forwarding user tokens, service identity |

---

## Complete Example: Web App with User and Client Credentials

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

// User token management via OIDC
builder.Services.AddAuthentication(options =>
{
    options.DefaultScheme = "cookie";
    options.DefaultChallengeScheme = "oidc";
})
.AddCookie("cookie", options =>
{
    options.Cookie.Name = "web";
    options.Events.OnSigningOut = async e =>
    {
        await e.HttpContext.RevokeRefreshTokenAsync();
    };
})
.AddOpenIdConnect("oidc", options =>
{
    options.Authority = "https://identity.example.com";
    options.ClientId = "web_app";
    options.ClientSecret = "secret";
    options.ResponseType = "code";
    options.Scope.Add("api1");
    options.Scope.Add("offline_access");
    options.SaveTokens = true;
});

builder.Services.AddOpenIdConnectAccessTokenManagement();

// User token HTTP client — attaches the logged-in user's access token
builder.Services.AddUserAccessTokenHttpClient("user-api",
    configureClient: client =>
    {
        client.BaseAddress = new Uri("https://api.example.com");
    });

// Client credentials for service-to-service (v4 types)
builder.Services.AddClientCredentialsTokenManagement()
    .AddClient(ClientCredentialsClientName.Parse("service-client"), client =>
    {
        client.TokenEndpoint = new Uri("https://identity.example.com/connect/token");
        client.ClientId = ClientId.Parse("web_app_service");
        client.ClientSecret = ClientSecret.Parse("service_secret");
        client.Scope = Scope.Parse("backend.api");
    });

builder.Services.AddClientCredentialsHttpClient("service-api",
    ClientCredentialsClientName.Parse("service-client"),
    configureClient: client =>
    {
        client.BaseAddress = new Uri("https://backend.example.com");
    });

var app = builder.Build();

app.UseAuthentication();
app.UseAuthorization();

// User-context API call
app.MapGet("/user-data", async (IHttpClientFactory factory) =>
{
    var client = factory.CreateClient("user-api");
    var response = await client.GetAsync("/data");
    return Results.Ok(await response.Content.ReadAsStringAsync());
}).RequireAuthorization();

// Service-to-service API call (no user context needed)
app.MapGet("/backend-data", async (IHttpClientFactory factory) =>
{
    var client = factory.CreateClient("service-api");
    var response = await client.GetAsync("/internal/data");
    return Results.Ok(await response.Content.ReadAsStringAsync());
});

app.Run();
```

---

## Common Pitfalls

### 1. Missing `SaveTokens = true` for User Tokens

```csharp
// ❌ Tokens never stored in session — library throws InvalidOperationException at runtime
.AddOpenIdConnect("oidc", options =>
{
    // SaveTokens not set — defaults to false
});

// ✅ Always set it when using AddOpenIdConnectAccessTokenManagement
options.SaveTokens = true;
```

### 2. Missing `offline_access` Scope

```csharp
// ❌ No refresh token issued — access token expires and user must re-authenticate
options.Scope.Add("openid");
options.Scope.Add("profile");
// offline_access missing

// ✅
options.Scope.Add("offline_access");
```

### 3. Not Revoking Refresh Tokens at Sign-Out

```csharp
// ❌ Refresh token remains valid at the authorization server after sign-out
.AddCookie("cookie", options =>
{
    // No OnSigningOut handler — refresh token never revoked
});

// ✅
.AddCookie("cookie", options =>
{
    options.Events.OnSigningOut = async e =>
    {
        await e.HttpContext.RevokeRefreshTokenAsync();
    };
});
```

### 4. Caching Tokens Manually Alongside the Library

```csharp
// ❌ Double-caching — your cache won't stay in sync; stale token after expiry
private string? _cachedToken;

public async Task<string> GetToken()
{
    if (_cachedToken != null) return _cachedToken;
    var token = await _tokenManager.GetAccessTokenAsync(...);
    _cachedToken = token.AccessToken.ToString(); // never invalidated
    return _cachedToken;
}

// ✅ Call GetAccessTokenAsync every time — the library serves from cache transparently
public async Task<string> GetToken(CancellationToken ct)
{
    var result = await _tokenManager
        .GetAccessTokenAsync(ClientCredentialsClientName.Parse("my.client"), ct: ct)
        .GetToken();
    return result.AccessToken.ToString();
}
```

### 5. Calling `.GetToken()` Without Checking `Succeeded`

```csharp
// ❌ .GetToken() throws InvalidOperationException when token retrieval fails;
// the actual error is swallowed unless you inspect Succeeded first
var tokenResult = await tokenManager.GetAccessTokenAsync(...);
var token = tokenResult.GetToken(); // throws when Succeeded == false

// ✅ Check success before accessing the token value
var tokenResult = await tokenManager.GetAccessTokenAsync(...);
if (!tokenResult.Succeeded)
{
    logger.LogError("Failed to obtain access token");
    return Problem("Authentication failure", statusCode: StatusCodes.Status503ServiceUnavailable);
}
var token = tokenResult.GetToken();
```

### 6. Using `AddClientCredentialsTokenHandler` Without Resiliency

```csharp
// ❌ A 401 from an expired token is returned directly to the caller — no retry
services.AddHttpClient<CatalogClient>(...)
    .AddClientCredentialsTokenHandler("catalog.client");

// ✅ Add the resiliency handler before the token handler
services.AddHttpClient<CatalogClient>(...)
    .AddDefaultAccessTokenResiliency()
    .AddClientCredentialsTokenHandler("catalog.client");
```

### 7. v3 — Forgetting to Register a Distributed Cache

```csharp
// ❌ v3: no cache registered — runtime exception on first token request
services.AddClientCredentialsTokenManagement()
    .AddClient("catalog.client", client => { /* ... */ });
// Missing: services.AddDistributedMemoryCache();

// ✅ v3: always register a distributed cache (in-memory for dev, Redis for prod)
services.AddDistributedMemoryCache();
```

### 8. Regenerating DPoP Keys on Every Process Restart

```csharp
// ❌ New key generated on every restart — all previously issued DPoP-bound tokens
// become unusable, causing 401 errors until new tokens are obtained
var rsaKey = new RsaSecurityKey(RSA.Create(2048)); // ephemeral — lost on restart

// ✅ Load from stable, secure storage
var jwkJson = configuration["DPoP:JsonWebKey"]; // from Key Vault / secrets
services.AddClientCredentialsTokenManagement()
    .AddClient("my.client", client =>
    {
        client.DPoPJsonWebKey = jwkJson;
    });
```

### 9. Setting Client Assertion Audience to the Token Endpoint URL

```csharp
// ❌ Audience set to the token endpoint — security vulnerability
// Root cause of CVE-2025-27370 and CVE-2025-27371
Audience = "https://identity.example.com/connect/token"

// ✅ Audience must be the authorization server's issuer URL
Audience = "https://identity.example.com"
```

> CVE-2025-27370 and CVE-2025-27371 were caused by this exact mistake. Authorization servers that accept both values allow token endpoint confusion attacks.

### 10. Using `HttpContext` to Access Tokens in Blazor Server Components

```csharp
// ❌ HttpContext is null after circuit establishment — this will fail at runtime
var token = await HttpContext.GetUserAccessTokenAsync(); // throws NullReferenceException

// ✅ Use AddBlazorServerAccessTokenManagement<T>() with a custom IUserTokenStore
builder.Services.AddOpenIdConnectAccessTokenManagement()
    .AddBlazorServerAccessTokenManagement<ServerSideTokenStore>();
// Capture tokens in OnTokenValidated (see Pattern 9)
```

### 11. Setting `CacheLifetimeBuffer` to 0

```csharp
// ❌ Buffer set to 0 — tokens served until exact expiry; a token may expire
// in transit between retrieval and use at the API, causing unnecessary 401s
services.AddClientCredentialsTokenManagement(options =>
{
    options.CacheLifetimeBuffer = 0;
});

// ✅ Keep the default (60 s) or set a positive value that accounts for network latency
services.AddClientCredentialsTokenManagement(options =>
{
    options.CacheLifetimeBuffer = 60; // default — refresh 60 s before expiry
});
```

---

## Version Reference: v3 → v4

| Area | v3 | v4 |
|---|---|---|
| Client credentials service | `IClientCredentialsTokenManagementService` | `IClientCredentialsTokenManager` |
| User token service | `IUserTokenManagementService` | `IUserTokenManager` |
| Token result type | `TokenResponse` — read `.Value` | `TokenResult<T>` — use `.Succeeded` / `.GetToken()` |
| Client name type | `string` | `ClientCredentialsClientName` (strongly typed) |
| Token cache | `IDistributedCache` (explicit `AddDistributedMemoryCache()` required) | `HybridCache` (automatic; picks up `IDistributedCache` as remote L2 tier) |
| Resiliency | Manual | `AddDefaultAccessTokenResiliency()` built into `AddClientCredentialsHttpClient` |

---

## Resources

- [Access Token Management Overview](https://docs.duendesoftware.com/accesstokenmanagement/)
- [Service Workers / Background Tasks](https://docs.duendesoftware.com/accesstokenmanagement/workers/)
- [Web Applications (User Tokens)](https://docs.duendesoftware.com/accesstokenmanagement/web-apps/)
- [Blazor Server](https://docs.duendesoftware.com/accesstokenmanagement/blazor-server/)
- [Advanced: Client Credentials Options](https://docs.duendesoftware.com/accesstokenmanagement/advanced/client-credentials/)
- [Advanced: User Token Options](https://docs.duendesoftware.com/accesstokenmanagement/advanced/user-tokens/)
- [Advanced: Client Assertions](https://docs.duendesoftware.com/accesstokenmanagement/advanced/client-assertions/)
- [Advanced: DPoP](https://docs.duendesoftware.com/accesstokenmanagement/advanced/dpop/)
- [Advanced: Extensibility](https://docs.duendesoftware.com/accesstokenmanagement/advanced/extensibility/)
- [v3 → v4 Upgrade Guide](https://docs.duendesoftware.com/accesstokenmanagement/upgrading/atm-v3-to-v4/)
- [NuGet: Duende.AccessTokenManagement](https://www.nuget.org/packages/Duende.AccessTokenManagement/)
- [NuGet: Duende.AccessTokenManagement.OpenIdConnect](https://www.nuget.org/packages/Duende.AccessTokenManagement.OpenIdConnect/)
- [GitHub: DuendeSoftware/foss (access-token-management)](https://github.com/DuendeSoftware/foss/tree/main/access-token-management)
