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.

To download the source code for this article, you can visit our GitHub repository.

With that, let’s start.

Support Code Maze on Patreon to get rid of ads and get the best discounts on our products!
Become a patron at Patreon!

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:

Issued token

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:

Decoded access token

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:

Decoded access token with custom claims

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.

Liked it? Take a second to support Code Maze on Patreon and get the ad free reading experience!
Become a patron at Patreon!