Token Management

作者 DuendeSoftwarefb32edc51982無授權條款9 個星標收錄於 2026年10月8日更新於 2026年10月8日儲存庫4 週前更新

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.

AI 產生的概覽

指導 .NET 開發者使用 Duende.AccessTokenManagement 快取、更新並附加 OAuth 權杖。

功能
此技能說明 .NET 應用程式使用 Duende.AccessTokenManagement 及其 OpenIdConnect 套件進行權杖管理的模式。內容涵蓋背景工作服務的用戶端認證權杖快取、Web 應用程式的使用者權杖更新與儲存、HttpClientFactory 與 gRPC 整合、DPoP 金鑰綁定、API 之間的權杖委派、動態用戶端設定、自訂權杖存放區、Blazor Server 處理,以及用戶端判斷提示。產出的是設定與程式碼指引,而非可直接執行的成品。
適用情境
適用於建置使用用戶端認證或代表已登入使用者呼叫 API 的 .NET 背景工作服務或 ASP.NET Core 應用程式。也適合將 Duende.AccessTokenManagement 與 IHttpClientFactory 整合、設定權杖快取、加入 DPoP,或是在登出時撤銷更新權杖的情境。
執行需求
需要 .NET 環境以及 Duende.AccessTokenManagement 或 Duende.AccessTokenManagement.OpenIdConnect 套件,還需要 OAuth/OIDC 授權伺服器以及用戶端認證或 OIDC 提供者。若使用遠端權杖快取,則需選用的 Redis 或其他分散式快取。僅提供文件,不含指令碼。

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 definitionsservices.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 automaticallyservices.AddClientCredentialsHttpClient(    "invoices",    ClientCredentialsClientName.Parse("invoice.client"),    client => { client.BaseAddress = new Uri("https://apis.company.com/invoice/"); });
// ✅ Typed client — identical behaviour, strongly typedservices.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 managementbuilder.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 servicesbuilder.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 tokenbuilder.Services.AddUserAccessTokenHttpClient(    "invoices",    configureClient: client =>    {        client.BaseAddress = new Uri("https://api.company.com/invoices/");    });
// ✅ Typed client using the current user's access tokenbuilder.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 tokenbuilder.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 IUserTokenManagerpublic 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 expiredvar userToken = await HttpContext.GetUserAccessTokenAsync();
// ✅ Client credentials token — re-requested from the token server if expiredvar 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 tokenbuilder.Services.AddGrpcClient<Greeter.GreeterClient>(o =>{    o.Address = new Uri("https://grpc.company.com");}).AddUserAccessTokenHandler();
// ✅ gRPC client using a client credentials tokenbuilder.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 L2builder.Services.AddStackExchangeRedisCache(options =>{    options.Configuration = builder.Configuration.GetConnectionString("Redis");});

Global cache options:

csharp
// ✅ Configure lifetime buffer and key prefixservices.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 implementationservices.AddDistributedMemoryCache(); // development / single-instance only
// ✅ v3: Redis for productionservices.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 typeservices.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 existsvar token = await tokenManager.GetAccessTokenAsync(    ClientCredentialsClientName.Parse("catalog.client"),    new TokenRequestParameters { ForceRenewal = true },    ct: stoppingToken);
// ✅ Override scope per user token requestvar 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 [blocked] 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 [blocked] 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);}
// Registrationservices.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 storagebuilder.Services.AddSingleton<IUserTokenStore, ServerSideSessionUserTokenStore>();

Client Credentials — Replace the Cache Implementation

csharp
// ✅ Override the entire cache with a custom IClientCredentialsTokenCacheservices.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 [blocked] 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 [blocked] 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 [blocked] 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 [blocked] 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:

DocumentDescriptionWhen to Load
docs/dpop.md [blocked]DPoP proof-of-possession token bindingDPoP, DPoPJsonWebKey, IDPoPKeyStore, key generation, key rotation
docs/blazor-server.md [blocked]Blazor Server circuit-scoped token managementBlazor, SignalR circuit, HttpContext null, AddBlazorServerAccessTokenManagement
docs/client-assertions.md [blocked]Client assertions (private_key_jwt) + CVE guidanceIClientAssertionService, JWT client auth, CVE-2025-27370, CVE-2025-27371
docs/customization.md [blocked]ITokenRequestCustomizer & ITokenRetrieverMulti-tenant token params, custom token retrieval, AccessTokenRequestHandler
docs/api-delegation.md [blocked]API-to-API delegation patternsDownstream API calls, forwarding user tokens, service identity

Complete Example: Web App with User and Client Credentials

csharp
// Program.csvar builder = WebApplication.CreateBuilder(args);
// User token management via OIDCbuilder.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 tokenbuilder.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 callapp.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 AddOpenIdConnectAccessTokenManagementoptions.SaveTokens = true;

2. Missing offline_access Scope

csharp
// ❌ No refresh token issued — access token expires and user must re-authenticateoptions.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 expiryprivate 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 transparentlypublic 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 firstvar tokenResult = await tokenManager.GetAccessTokenAsync(...);var token = tokenResult.GetToken(); // throws when Succeeded == false
// ✅ Check success before accessing the token valuevar 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 retryservices.AddHttpClient<CatalogClient>(...)    .AddClientCredentialsTokenHandler("catalog.client");
// ✅ Add the resiliency handler before the token handlerservices.AddHttpClient<CatalogClient>(...)    .AddDefaultAccessTokenResiliency()    .AddClientCredentialsTokenHandler("catalog.client");

7. v3 — Forgetting to Register a Distributed Cache

csharp
// ❌ v3: no cache registered — runtime exception on first token requestservices.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 obtainedvar rsaKey = new RsaSecurityKey(RSA.Create(2048)); // ephemeral — lost on restart
// ✅ Load from stable, secure storagevar jwkJson = configuration["DPoP:JsonWebKey"]; // from Key Vault / secretsservices.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-27371Audience = "https://identity.example.com/connect/token"
// ✅ Audience must be the authorization server's issuer URLAudience = "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 runtimevar token = await HttpContext.GetUserAccessTokenAsync(); // throws NullReferenceException
// ✅ Use AddBlazorServerAccessTokenManagement<T>() with a custom IUserTokenStorebuilder.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 401sservices.AddClientCredentialsTokenManagement(options =>{    options.CacheLifetimeBuffer = 0;});
// ✅ Keep the default (60 s) or set a positive value that accounts for network latencyservices.AddClientCredentialsTokenManagement(options =>{    options.CacheLifetimeBuffer = 60; // default — refresh 60 s before expiry});

Version Reference: v3 → v4

Areav3v4
Client credentials serviceIClientCredentialsTokenManagementServiceIClientCredentialsTokenManager
User token serviceIUserTokenManagementServiceIUserTokenManager
Token result typeTokenResponse — read .ValueTokenResult<T> — use .Succeeded / .GetToken()
Client name typestringClientCredentialsClientName (strongly typed)
Token cacheIDistributedCache (explicit AddDistributedMemoryCache() required)HybridCache (automatic; picks up IDistributedCache as remote L2 tier)
ResiliencyManualAddDefaultAccessTokenResiliency() built into AddClientCredentialsHttpClient

Resources

來源與署名

來源:DuendeSoftware/duende-skills位於skills/token-management提交fb32edc

授權條款: 無授權條款

內容歸原作者所有。SourceWeft 從公開儲存庫中收錄這些內容。

檢舉或申請下架