In this article, we will show how to add custom claims to access tokens in the Duende Identity Server. Custom claims allow us to include application-specific information in ID and access tokens. This information encompasses details not covered by the standard claims that the OpenID Connect protocol defines. For example, we might include user roles, permissions, or other application-specific attributes.
As a rule, identity providers support the ability to load custom claims into both ID and access tokens.
With that, let’s start.
Create Duende Identity Server
First, we need an instance of the Duende Identity Server and a client application to reproduce the process of obtaining access tokens. A complete guide on creating the Duende Identity Server and web client is available on the official site.
Once the identity server is running, initiate the web client. The web client only allows authenticated users to access its pages, redirecting unauthenticated users to the Identity Server login page.
We will use one of the default accounts, such as alice/alice
or bob/bob
, to log in on the identity server. Upon successful login, the identity server issues ID and access tokens, providing them to the web client. Subsequently, the web client signs in the user by creating a local cookie that, among other things, stores the issued access token.
We can locate it on the rendered page:
Now, let’s delve into the content of this access token. To do that, we will decode it using an online tool like jwt.io:
By default, Duende Identity Server includes a basic set of claims in its access tokens. One of the most important for developers is the sub
claim, whose value identifies the user who requested a token. Occasionally, we might need to expand the set of standard claims to include additional values.
Add Custom Claims to Access Token
Duende Identity Server defines an IProfileService interface. By implementing this interface, developers can add custom claims to the issued access tokens in Duende.
To start, let’s create a CustomProfileService
class in the IdentityServer
project, which will be a simple implementation of the IProfileService
interface:
public sealed class CustomProfileService : IProfileService { public Task GetProfileDataAsync(ProfileDataRequestContext context) { if (context.Client.ClientId == "web") { context.IssuedClaims.Add(new Claim("tenant", "main")); } return Task.CompletedTask; } public Task IsActiveAsync(IsActiveContext context) { if (context.Subject.GetSubjectId() == "3") { context.IsActive = false; } return Task.CompletedTask; } }
The IsActiveAsync()
method can determine if an authenticated user is allowed to obtain tokens. Its current implementation doesn’t allow issuing tokens for requests from particular users. To prevent a token from being issued, we set the IsActive
property of the context
to false
:
context.IsActive = false;
The GetProfileDataAsync
method is responsible for determining which claims will be included in tokens. Its input context
argument provides various useful properties to build flexible logic for adding standard or custom claims. For example, in our code sample, we include a custom claim named tenant
in access tokens issued only when requested by the client web
. The access token will include it even though the client didn’t explicitly request this claim, because we added it directly to the IssuedClaims
list.
Additionally, we can force any client to request custom claims if needed. For that, we can provide custom claims in custom API scopes:
public static IEnumerable<ApiScope> ApiScopes => new ApiScope[] { new(name: "payments", displayName: "Allow payments", userClaims: new[] { "payments.discount" }) };
After we include the payments
scope in the list of available scopes for the client web
, we can modify the GetProfileDataAsync()
method in the CustomProfileService
class:
public Task GetProfileDataAsync(ProfileDataRequestContext context) { if (context.Client.ClientId == "web") { context.IssuedClaims.Add(new Claim("tenant", "main")); } if (context.RequestedClaimTypes.Any()) { context.AddRequestedClaims(new[] { new Claim("payments.discount", "20") }); } return Task.CompletedTask; }
When the client requests the payments
scope, the RequestedClaimTypes
collection in the context will contain the custom claim’s type payments.discount
. Notice that we use the AddRequestedClaims
method to add this claim and its value, ensuring that only requested claims will be added to the access token.
Lastly, let’s not forget to register CustomProfileService
in HostingExtensions
:
isBuilder.AddProfileService<CustomProfileService>();
Now, let’s check how the custom profile service works.
Restart the identity server and web client, sign out the user if still authenticated, and log in again. Finally, decode the new access token:
Here, we can make sure the new access token contains our custom claims, payments.discount
and tenant
.
Conclusion
Custom claims in access tokens provide a way to add additional information or attributes about the authenticated user or the context of the authentication process in the application. Including relevant information as custom claims in the access token can help reduce the need for additional queries to the back end or external services during the authorization process. This can lead to more efficient and faster authorization decisions.
Duende IdentityServer provides the IProfileService interface. It is a flexible extension point that allows developers to provide extended information to the access tokens.