Identityserver Token Lifecycle

作者 DuendeSoftwarefb32edc51982无许可证9 个星标收录于 2026年10月8日更新于 2026年10月8日仓库4周前更新

Guide for implementing token types, refresh token management, token exchange (RFC 8693), extension grants, IProfileService claims customization, and token lifetime best practices in Duende IdentityServer.

AI 生成的概览

指导在 Duende IdentityServer 中实现令牌类型、刷新令牌、令牌交换、声明定制与生命周期。

功能
说明 Duende IdentityServer 如何签发身份令牌、访问令牌和刷新令牌,以及如何在 JWT 与引用令牌之间选择。内容涵盖刷新令牌轮换、滑动过期、重放检测、清理设置、通过 RFC 8693 扩展授权验证器实现令牌交换、IProfileService 声明定制、内部令牌签发和生命周期建议。产出为配置与 C# 实现指导,以及决策表和反模式清单。
适用场景
在为 Duende IdentityServer 部署配置令牌格式、生命周期或刷新令牌行为时使用。在实现用于模拟或委托的令牌交换,或定制令牌与 userinfo 响应中包含哪些声明时使用。在审查令牌相关配置以排查常见陷阱时使用。
运行要求
无需脚本或附带资源,代理只需 SKILL.md 中的说明。代码示例面向 .NET/C# 的 Duende IdentityServer 与 Duende.IdentityModel,文中引用了厂商文档链接,但运行本身不依赖它。

IdentityServer Token Types, Refresh Tokens, and Token Exchange

When to Use This Skill

  • Choosing between JWT and reference access tokens for a client
  • Configuring refresh token rotation, sliding expiration, or replay detection
  • Implementing token exchange (RFC 8693) for impersonation or delegation
  • Building an extension grant validator (IExtensionGrantValidator)
  • Customizing which claims appear in identity tokens, access tokens, or userinfo responses via IProfileService
  • Setting token lifetime policies for access tokens and refresh tokens
  • Issuing internal tokens from extensibility code via IIdentityServerTools
  • Understanding identity tokens vs access tokens and their intended audiences

Docs: https://docs.duendesoftware.com/identityserver/tokens

Token Types Overview

Duende IdentityServer issues three primary token types:

Token TypePurposeAudienceFormat
Identity TokenCommunicates authentication event to the clientClient application only (aud claim)Always JWT
Access TokenAuthorizes access to a protected resource (API)API / Resource ServerJWT or Reference
Refresh TokenObtains new access tokens without user interactionToken endpoint onlyOpaque handle

Key Principles

  • Identity tokens are solely for the client application that initiated the authentication. Never send an identity token to an API.
  • Access tokens are for APIs. They contain client ID, scopes, expiration, and optionally user claims.
  • Refresh tokens enable long-lived API access by allowing the client to request new access tokens silently.

Identity Tokens

Identity tokens are JWTs that describe "what happened at the token service". They contain:

  • iss — the issuer (your IdentityServer URL)
  • sub — the authenticated user's unique identifier
  • aud — the client that requested authentication
  • auth_time — when the user authenticated
  • amr — authentication method (e.g., pwd)
  • idp — identity provider used (e.g., local)
  • sid — the session ID
  • nonce — ensures the token is consumed only once at the client
json
{  "iss": "https://localhost:5001",  "nbf": 1609932802,  "iat": 1609932802,  "exp": 1609933102,  "aud": "web_app",  "amr": ["pwd"],  "nonce": "63745529591...I3ZTIyOTZmZTNj",  "sid": "F6E6F2EDE86EB8731EF609A4FE40ED89",  "auth_time": 1609932794,  "idp": "local",  "sub": "88421113",  "name": "Bob"}

Access Tokens: JWT vs Reference

JWT Access Tokens

All claims are embedded in the token. The API validates the token by checking the signature using the issuer's public keys (from the JWKS endpoint). JWTs cannot be revoked before their expiration — the only invalidation mechanism is waiting for exp.

json
{  "iss": "https://localhost:5001",  "exp": 1609936401,  "aud": "urn:resource1",  "scope": "openid resource1.scope1 offline_access",  "client_id": "web_app",  "sub": "88421113",  "jti": "2C56A356A306E64AFC7D2C6399E23A17"}

Reference Access Tokens

Reference tokens are pointers to token data stored in the persisted grant store. The API must call the introspection endpoint to validate the token. Reference tokens support immediate revocation by deleting the stored data.

csharp
// Configure a client to use reference tokensclient.AccessTokenType = AccessTokenType.Reference;

The API consuming reference tokens must have a secret configured on the ApiResource:

csharp
var api = new ApiResource("api1"){    ApiSecrets = { new Secret("secret".Sha256()) },    Scopes = { "read", "write" }};

Decision Matrix: JWT vs Reference Tokens

CriterionJWTReference
RevocabilityNo (expires naturally)Yes (immediate, delete from store)
API call to validateNo (self-contained)Yes (introspection endpoint)
Network dependencyNone at validation timeRequires IdentityServer availability
Token sizeLarger (contains all claims)Small (just a handle)
Performance at scaleBetter (no server call)Introspection adds latency
Best forHigh-throughput APIs, microservicesSensitive APIs needing revocation

Token Revocation (RFC 7009)

Revocation via the /connect/revocation endpoint applies only to reference access tokens and refresh tokens — the tokens that are persisted in the operational (persisted grant) store. A JWT access token is stateless and is not stored server-side by default, so there is no revocation state to deactivate: a JWT simply remains valid until its exp. If you need to invalidate access tokens immediately (logout, compromise, entitlement change), issue reference tokens (AccessTokenType.Reference).

Controlling Token Format Per Client

csharp
// Set on the Client modelclient.AccessTokenType = AccessTokenType.Jwt;       // defaultclient.AccessTokenType = AccessTokenType.Reference;  // reference tokens

Refresh Tokens

Refresh tokens allow clients to obtain new access tokens without user interaction. They are supported for authorization code, hybrid, and resource owner password credential flows.

Requesting Refresh Tokens

The client must:

  1. Have AllowOfflineAccess = true on its configuration
  2. Request the offline_access scope in the authorize request
POST /connect/tokenContent-Type: application/x-www-form-urlencoded
    client_id=client&    client_secret=secret&    grant_type=refresh_token&    refresh_token=hdh922

Using Duende.IdentityModel:

csharp
using Duende.IdentityModel.Client;
var client = new HttpClient();
var response = await client.RequestRefreshTokenAsync(new RefreshTokenRequest{    Address = TokenEndpoint,    ClientId = "client",    ClientSecret = "secret",    RefreshToken = "..."});

Refresh Token Lifetime Settings

SettingDescriptionRecommendation
AbsoluteRefreshTokenLifetimeMaximum lifetime regardless of activity (seconds)Set based on security policy (e.g., 30 days)
SlidingRefreshTokenLifetimeExtends token life on each use, up to the absolute limitUse for "remember me" scenarios (e.g., 1 day sliding)
RefreshTokenExpirationAbsolute or SlidingUse Sliding with a reasonable absolute cap

Rotation (OneTime vs ReUse)

Configured via RefreshTokenUsage on the client:

ModeBehaviorTrade-offs
ReUse (default since v7.0)Same refresh token is reused across requestsRobust to network failures, lower DB pressure
OneTimeNew refresh token issued on each use; old one consumedLimited security benefit, risk of losing token on network failure

Why ReUse is the default: Rotating tokens on every use has limited security benefits regardless of client type. Reusable tokens are robust to network failures — if a one-time-use token is used but the response is lost, the client cannot recover without forcing a new login. Reusable tokens also have better performance since they avoid extra writes to the persisted grant store.

Accepting Consumed Tokens (Network Failure Resilience)

To make one-time-use tokens more resilient, subclass DefaultRefreshTokenService and override AcceptConsumedTokenAsync:

csharp
public class ResilientRefreshTokenService : DefaultRefreshTokenService{    protected override Task<bool> AcceptConsumedTokenAsync(RefreshToken refreshToken)    {        // Allow consumed tokens for a short grace period        var consumedAt = refreshToken.ConsumedTime ?? DateTime.UtcNow;        if (DateTime.UtcNow - consumedAt < TimeSpan.FromSeconds(30))        {            return Task.FromResult(true);        }        return Task.FromResult(false);    }}

Register it:

csharp
builder.Services.TryAddTransient<IRefreshTokenService, ResilientRefreshTokenService>();

Important: For this to work, PersistentGrantOptions.DeleteOneTimeOnlyRefreshTokensOnUse must be false so consumed tokens are marked rather than deleted.

Replay Detection

If a consumed refresh token is reused, it could indicate a replay attack. You can extend AcceptConsumedTokenAsync to revoke all access for the user/client:

  • Delete all refresh tokens for the user/client
  • Revoke reference access tokens
  • End the user's server-side session
  • Send back-channel logout notifications
  • Alert the user

Caution: This is disruptive and can produce false positives from network failures or client bugs.

Token Cleanup Configuration

csharp
builder.Services.AddIdentityServer()    .AddOperationalStore(options =>    {        options.EnableTokenCleanup = true;        options.TokenCleanupInterval = 3600;           // seconds (default: 1 hour)        options.RemoveConsumedTokens = true;            // also clean consumed tokens        options.ConsumedTokenCleanupDelay = 300;        // wait 5 min after consumption    });

Token Exchange (RFC 8693)

Token exchange allows translating between token types. Common use cases: impersonation, delegation, SAML-to-JWT conversion.

Implementing Token Exchange

Implement IExtensionGrantValidator:

csharp
public class TokenExchangeGrantValidator : IExtensionGrantValidator{    private readonly ITokenValidator _validator;
    public TokenExchangeGrantValidator(ITokenValidator validator)    {        _validator = validator;    }
    public string GrantType => OidcConstants.GrantTypes.TokenExchange;
    public async Task ValidateAsync(ExtensionGrantValidationContext context)    {        context.Result = new GrantValidationResult(TokenRequestErrors.InvalidRequest);
        var customResponse = new Dictionary<string, object>        {            { OidcConstants.TokenResponse.IssuedTokenType, OidcConstants.TokenTypeIdentifiers.AccessToken }        };
        var subjectToken = context.Request.Raw.Get(OidcConstants.TokenRequest.SubjectToken);        var subjectTokenType = context.Request.Raw.Get(OidcConstants.TokenRequest.SubjectTokenType);
        if (string.IsNullOrWhiteSpace(subjectToken)) return;
        if (!string.Equals(subjectTokenType, OidcConstants.TokenTypeIdentifiers.AccessToken)) return;
        var validationResult = await _validator.ValidateAccessTokenAsync(subjectToken);        if (validationResult.IsError) return;
        var sub = validationResult.Claims.First(c => c.Type == JwtClaimTypes.Subject).Value;        var clientId = validationResult.Claims.First(c => c.Type == JwtClaimTypes.ClientId).Value;
        // Impersonation: set client_id to the original        context.Request.ClientId = clientId;        context.Result = new GrantValidationResult(            subject: sub,            authenticationMethod: GrantType,            customResponse: customResponse);    }}

Register and configure:

csharp
// Program.csidsvrBuilder.AddExtensionGrantValidator<TokenExchangeGrantValidator>();
// Client configurationclient.AllowedGrantTypes = { OidcConstants.GrantTypes.TokenExchange };

Impersonation vs Delegation

Patternclient_id in new tokenact claimUse case
ImpersonationOriginal front-end clientNot presentAPI1 calls API2 "as if" it were the front-end
DelegationOriginal front-end clientContains { "client_id": "api1" }API2 sees the full call chain

Delegation adds an act claim to preserve the call chain:

csharp
context.Request.ClientId = clientId;
var actor = new { client_id = context.Request.Client.ClientId };var actClaim = new Claim(JwtClaimTypes.Actor,    JsonSerializer.Serialize(actor),    IdentityServerConstants.ClaimValueTypes.Json);
context.Result = new GrantValidationResult(    subject: sub,    authenticationMethod: GrantType,    claims: new[] { actClaim },    customResponse: customResponse);

To emit the act claim in tokens, your profile service must handle it:

csharp
public class ProfileService : IProfileService{    public async Task GetProfileDataAsync(ProfileDataRequestContext context)    {        if (context.Subject.GetAuthenticationMethod() == OidcConstants.GrantTypes.TokenExchange)        {            var act = context.Subject.FindFirst(JwtClaimTypes.Actor);            if (act != null)            {                context.IssuedClaims.Add(act);            }        }    }}

Sensitive Parameter Filtering

Extension grant input parameters are logged by default. Filter sensitive values:

csharp
builder.Services.AddIdentityServer(options =>{    options.Logging.TokenRequestSensitiveValuesFilter.Add("custom_secret_param");});

Claims Customization with IProfileService

The profile service controls which claims are emitted in identity tokens, access tokens, and userinfo responses.

Strategies

StrategyWhen to use
context.AddRequestedClaims(claims)Respects scopes/resources requested by client; supports consent
context.IssuedClaims.AddRange(claims)Always emit claims regardless of request
Custom logic per user/clientConditional claims based on identity

Recommended Pattern

Extend DefaultProfileService and use AddRequestedClaims:

csharp
public class SampleProfileService : DefaultProfileService{    public override async Task GetProfileDataAsync(ProfileDataRequestContext context)    {        var claims = await GetClaimsFromDatabaseAsync(context.Subject);        context.AddRequestedClaims(claims);    }}

Client Claims

Client claims are defined per-client and emitted in access tokens (prefixed with client_ by default):

csharp
var client = new Client{    ClientId = "client",    Claims = { new ClientClaim("customer_id", "123") }    // Emitted as "client_customer_id" in access tokens};

Change or remove the prefix:

csharp
client.ClientClaimsPrefix = "";  // no prefix

By default, client claims are only sent in client credentials flow. To include them in all flows:

csharp
client.AlwaysSendClientClaims = true;

Claim Serialization

Claims are serialized based on ClaimValueType:

  • No type specified → string
  • ClaimValueTypes.Integer, Integer32, Integer64, Double, Boolean → parsed as corresponding type
  • IdentityServerConstants.ClaimValueTypes.Json → serialized as JSON

Issuing Internal Tokens

When extensibility code needs to call other APIs, use IIdentityServerTools instead of the protocol endpoints:

csharp
app.MapGet("/myAction", async (IIdentityServerTools tools) =>{    var token = await tools.IssueClientJwtAsync(        clientId: "client_id",        lifetime: 3600,        audiences: new[] { "backend.api" });
    // Use token to call backend API});

Dynamic Issuer (Multi-Issuer)

By default, a single IdentityServer derives the iss claim (and the discovery issuer) from the origin of the incoming request. The same deployment can therefore serve multiple hosts/domains and return a different iss for each — no extra configuration required.

csharp
// Requests to https://a.example.com  → iss = "https://a.example.com"// Requests to https://b.example.com  → iss = "https://b.example.com"

Setting a fixed issuer disables this behavior — every token then carries the configured value regardless of host:

csharp
builder.Services.AddIdentityServer(options =>{    // Pins iss to a single value; multi-issuer is turned off    options.IssuerUri = "https://identity.example.com";});

Multi-issuer is not multi-tenancy. Returning a per-host iss (RFC 7519 §4.1.1) does not isolate users, grants, keys, or any other data per domain. Tenant isolation remains the implementer's responsibility.

Token Lifetime Best Practices

TokenRecommended LifetimeRationale
Identity Token5 minutes (default: 300s)Only used once during authentication
JWT Access Token5-15 minutes (default: 3600s = 1 hour)Short-lived; cannot be revoked
Reference Access Token5-60 minutesCan be revoked, so slightly longer is acceptable
Refresh Token (absolute)Hours to days depending on security needsBalance UX vs risk
Refresh Token (sliding)Shorter than absolute (e.g., 1 hour)Auto-expire unused tokens

Common Anti-Patterns

  • ❌ Sending identity tokens to APIs for authorization — they are for the client only

  • ✅ Use access tokens (JWT or reference) for API authorization

  • ❌ Using very long-lived JWT access tokens (hours/days) with no revocation mechanism

  • ✅ Keep JWT lifetimes short (5-15 min) and use refresh tokens for longevity

  • ❌ Enabling OneTime refresh token rotation without considering network failure scenarios

  • ✅ Use ReUse (default) or implement AcceptConsumedTokenAsync with a grace period

  • ❌ Putting all user claims directly into access tokens, creating bloated JWTs

  • ✅ Use AddRequestedClaims to emit only claims requested by scopes; use the userinfo endpoint for additional claims

  • ❌ Parsing the returnUrl manually instead of using GetAuthorizationContextAsync

  • ✅ Always use the interaction service to extract authorization context

  • ❌ Forgetting to set AllowOfflineAccess = true on the client and then wondering why no refresh token is issued

  • ✅ Configure both the client property and request the offline_access scope

Common Pitfalls

  1. Reference tokens require introspection: APIs consuming reference tokens must call the introspection endpoint. Without a configured ApiSecret on the ApiResource, introspection will fail with 401.

  2. Refresh token cleanup: Enable EnableTokenCleanup in the operational store options. Without it, expired and consumed tokens accumulate indefinitely.

  3. Token exchange client configuration: The client performing token exchange must have AllowedGrantTypes set to urn:ietf:params:oauth:grant-type:token-exchange (use OidcConstants.GrantTypes.TokenExchange).

  4. Profile service Subject differs by caller: When called for userinfo requests, the Subject property contains claims from the access token, not the authentication session. Check context.Caller to determine the source.

  5. Client claims prefix collision: Client claims are prefixed with client_ by default. Adjust ClientClaimsPrefix if this collides with existing user claim types.

来源与署名

来源:DuendeSoftware/duende-skills位于skills/identityserver-token-lifecycle提交fb32edc

许可证: 无许可证

内容归原作者所有。SourceWeft 从公开仓库中收录这些内容。

举报或申请下架