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:
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 identifieraud— the client that requested authenticationauth_time— when the user authenticatedamr— authentication method (e.g.,pwd)idp— identity provider used (e.g.,local)sid— the session IDnonce— ensures the token is consumed only once at the client
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.
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.
The API consuming reference tokens must have a secret configured on the ApiResource:
Decision Matrix: JWT vs Reference Tokens
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
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:
- Have
AllowOfflineAccess = trueon its configuration - Request the
offline_accessscope in the authorize request
Using Duende.IdentityModel:
Refresh Token Lifetime Settings
Rotation (OneTime vs ReUse)
Configured via RefreshTokenUsage on the client:
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:
Register it:
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
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:
Register and configure:
Impersonation vs Delegation
Delegation adds an act claim to preserve the call chain:
To emit the act claim in tokens, your profile service must handle it:
Sensitive Parameter Filtering
Extension grant input parameters are logged by default. Filter sensitive values:
Claims Customization with IProfileService
The profile service controls which claims are emitted in identity tokens, access tokens, and userinfo responses.
Strategies
Recommended Pattern
Extend DefaultProfileService and use AddRequestedClaims:
Client Claims
Client claims are defined per-client and emitted in access tokens (prefixed with client_ by default):
Change or remove the prefix:
By default, client claims are only sent in client credentials flow. To include them in all flows:
Claim Serialization
Claims are serialized based on ClaimValueType:
- No type specified → string
ClaimValueTypes.Integer,Integer32,Integer64,Double,Boolean→ parsed as corresponding typeIdentityServerConstants.ClaimValueTypes.Json→ serialized as JSON
Issuing Internal Tokens
When extensibility code needs to call other APIs, use IIdentityServerTools instead of the protocol endpoints:
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.
Setting a fixed issuer disables this behavior — every token then carries the configured value regardless of host:
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
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
OneTimerefresh token rotation without considering network failure scenarios -
✅ Use
ReUse(default) or implementAcceptConsumedTokenAsyncwith a grace period -
❌ Putting all user claims directly into access tokens, creating bloated JWTs
-
✅ Use
AddRequestedClaimsto emit only claims requested by scopes; use the userinfo endpoint for additional claims -
❌ Parsing the
returnUrlmanually instead of usingGetAuthorizationContextAsync -
✅ Always use the interaction service to extract authorization context
-
❌ Forgetting to set
AllowOfflineAccess = trueon the client and then wondering why no refresh token is issued -
✅ Configure both the client property and request the
offline_accessscope
Common Pitfalls
-
Reference tokens require introspection: APIs consuming reference tokens must call the introspection endpoint. Without a configured
ApiSecreton theApiResource, introspection will fail with401. -
Refresh token cleanup: Enable
EnableTokenCleanupin the operational store options. Without it, expired and consumed tokens accumulate indefinitely. -
Token exchange client configuration: The client performing token exchange must have
AllowedGrantTypesset tourn:ietf:params:oauth:grant-type:token-exchange(useOidcConstants.GrantTypes.TokenExchange). -
Profile service
Subjectdiffers by caller: When called for userinfo requests, theSubjectproperty contains claims from the access token, not the authentication session. Checkcontext.Callerto determine the source. -
Client claims prefix collision: Client claims are prefixed with
client_by default. AdjustClientClaimsPrefixif this collides with existing user claim types.


