Imagine this sweat-inducing nightmare scenario. A banking customer's phone is stolen, and your mobile app is logged in, granting the thief complete access to their account. A frantic call comes into support. Every second counts. What is your speed-to-response for revoking that active session and securing their funds?
If you're relying on standard self-contained JWTs, the honest answer might be "up to an hour", depending on how long the token is valid. That's not going to cut it. Let's talk about how Reference Tokens give you an emergency stop button for exactly these situations, and how to wire it all up with Duende IdentityServer in .NET 10.
The Problem with Self-Contained JWTs
Self-contained JWTs are the workhorse of modern authorization. They carry all the claims an API needs to make access decisions right inside the token itself. No database lookup, no network call to the identity provider. The API validates the signature, checks the expiration, and you're in. It's elegant and performant.
But that self-contained nature is a double-edged sword. Once a JWT is issued, the identity provider has nothing more to say about it. The token is valid until its exp claim says otherwise, typically 5 to 60 minutes. If a device is stolen, a user account is compromised, or a threat is detected, you cannot revoke that token. You're stuck waiting for it to expire.
For many applications, that trade-off is perfectly acceptable. For high-security environments like banking, healthcare, or government systems, it's a gap you cannot afford.
Reference Tokens: Pushing The Button
Reference Tokens flip the model. Instead of embedding all claims directly in the token, IdentityServer stores the token contents server-side in its persisted grant store and hands the client an opaque identifier (a handle). When an API receives this handle, it calls the IdentityServer introspection endpoint to validate the token and retrieve the claims.
sequenceDiagram
participant Client as Client App
participant IS as IdentityServer
participant API as Protected API
Note over Client,IS: Token Issuance
Client->>IS: Request access token
IS->>IS: Store token data in<br/>persisted grant store
IS->>Client: Return opaque token handle
Note over Client,API: API Request with Reference Token
Client->>API: API request with token handle
API->>IS: Introspection request<br/>(validate token handle)
IS->>IS: Lookup token in<br/>persisted grant store
IS->>API: Return token claims<br/>("active": true)
API->>Client: API response
Note over Client,IS: Token Revocation
IS->>IS: Delete token from<br/>persisted grant store
Note over Client,API: Subsequent API Request (After Revocation)
Client->>API: API request with same token handle
API->>IS: Introspection request
IS->>IS: Token not found in store
IS->>API: "active": false
API->>Client: 401 Unauthorized
This changes everything. Because the token data lives on the server, you can delete it at any time. Revocation is immediate. The next time the API calls the introspection endpoint, it gets back "active": false, and access is denied. No waiting for expiration. No stale tokens floating around.
The trade-off? Every API call requires a round-trip to the introspection endpoint. Regarding internet-scale public APIs, that's a concern. For internal services and high-security environments, it's a reasonable price for the ability to pull the plug instantly.
Configuring Reference Tokens in IdentityServer
Switching a client to Reference Tokens is a one-line configuration change. When defining your client in Duende IdentityServer, set the AccessTokenType property:
new Client
{
ClientId = "banking_app",
ClientSecrets = { new Secret("secret".Sha256()) },
AllowedGrantTypes = GrantTypes.Code,
// This is the key line
AccessTokenType = AccessTokenType.Reference,
AllowOfflineAccess = true,
RedirectUris = { "https://banking.example.com/signin-oidc" },
AllowedScopes = { "openid", "profile", "accounts.read", "transfers.write" }
};
That's it on the IdentityServer side. Tokens issued to this client will now be opaque handles instead of self-contained JWTs.
Configuring Your API for Introspection
Your API needs to know how to validate these opaque tokens. Instead of (or in addition to) JWT validation, you configure OAuth 2.0 introspection. First, define an API Resource with a secret that the API will use to authenticate with the introspection endpoint:
new ApiResource("banking_api")
{
Scopes = { "accounts.read", "transfers.write" },
ApiSecrets = { new Secret("api_secret".Sha256()) }
};
Then in your API's Program.cs, register the introspection handler. Note, you'll want the handler to use the same authentication scheme as the one you want to introspect
builder.Services.AddAuthentication("token")
.AddOAuth2Introspection("token", options =>
{
options.Authority = "https://identity.banking.example.com";
options.ClientId = "banking_api";
options.ClientSecret = "api_secret";
});
If you need to support both JWTs and Reference Tokens (perhaps during a migration), you can register both handlers and use forwarding to route tokens to the correct one:
builder.Services.AddAuthentication("token")
.AddJwtBearer("token", options =>
{
options.Authority = "https://identity.banking.example.com";
options.Audience = "banking_api";
options.TokenValidationParameters.ValidTypes = ["at+jwt"];
options.ForwardDefaultSelector = Selector.ForwardReferenceToken("introspection");
})
.AddOAuth2Introspection("introspection", options =>
{
options.Authority = "https://identity.banking.example.com";
options.ClientId = "banking_api";
options.ClientSecret = "api_secret";
});
Revoking a Token
Now for the payoff. When that panicked customer calls in, your support system (or an automated threat-detection pipeline) can revoke their token immediately using IdentityServer's revocation endpoint, which implements RFC 7009:
using Duende.IdentityModel.Client;
var client = new HttpClient();
var result = await client.RevokeTokenAsync(new TokenRevocationRequest
{
Address = "https://identity.banking.example.com/connect/revocation",
ClientId = "banking_app",
ClientSecret = "secret",
Token = stolenAccessToken
});
if (result.IsError)
{
logger.LogError("Token revocation failed: {Error}", result.Error);
}
Once revoked, the token is removed from IdentityServer's persisted grant store. The very next introspection request from any API will confirm the token is no longer active. Access is cut off, and you can sleep at night.
Don't forget: you can (and should) also revoke the user's refresh token to prevent the client from silently obtaining a new access token:
await client.RevokeTokenAsync(new TokenRevocationRequest
{
Address = "https://identity.banking.example.com/connect/revocation",
ClientId = "banking_app",
ClientSecret = "secret",
Token = refreshToken
});
Note that both introspection and revocation emit audit events you can use to implement audit logs in regulated industries.
When to Reach for Reference Tokens
Reference Tokens are not a blanket replacement for JWTs. They shine in specific scenarios:
- Immediate revocation is a hard requirement (banking, healthcare, compliance-driven systems)
- Internal service-to-service communication where the introspection round-trip is negligible
- High-risk operations where the security benefit outweighs the performance cost
For public-facing APIs at scale where revocation latency is acceptable, self-contained JWTs with short lifetimes remain a solid choice. You can even mix and match: use Reference Tokens for sensitive clients and JWTs for lower-risk ones, all within the same IdentityServer deployment.
Conclusion
Every security architecture involves trade-offs. Self-contained JWTs trade revocability for performance. Reference Tokens trade performance for control. For environments where "wait for it to expire" is not an acceptable answer, Reference Tokens with Duende IdentityServer give you a proper emergency stop button.
The implementation is straightforward: one property on the client, an introspection handler on the API, and a revocation call when you need to pull the plug. When security incidents happen (and they will), you'll be glad you wired it up.
For more details, check out the Duende IdentityServer documentation on Reference Tokens and the revocation endpoint reference.
Thanks for stopping by!
We hope this post helped you on your identity and security journey. If you need a hand with implementation, our docs are always open. For everything else, come hang out with the team and other developers on GitHub.
If you want to get early access to new features and products while collaborating with experts in security and identity standards, join us in our Duende Product Insiders program. And if you prefer your tech content in video form, our YouTube channel is the place to be. Don't forget to like and subscribe!
Questions? Comments? Just want to say hi? Leave a comment below and let's start a conversation.