When building logic around the IdentityServer4 extensibility points, it is sometimes necessary to dynamically issue a token, with which your code can then call some external endpoints or dependencies.

Turns out that rather than round-tripping back to same IdentityServer4 instance over the network to get that token, there is a more efficient and quicker way to do it. Let’s have a look.

Some context

Let’s imagine that we are building a custom extension grant on top of IdentityServer4. Extension grants are typically used to express non-standard token flows, such as converting one token type into another or performing delegation on behalf of the user. Such grants are added to IdentityServer4 by creating a custom implementation of IExtensionGrantValidator.

Now, where things get a bit more complicated, is when your custom implementation of the grant, as part of its validation process, requires you to call an endpoint that is protected by the very same instance of IdentityServer4 you are extending. This is, in fact, not an uncommon scenario – in this case, in order to perform our custom grant, we may need to through i.e. a client_credentials flow of our validation dependency first.

Dealing with such cases was not trivial in IdentityServer3, but IdentityServer4 introduced a very simple solution – a helper service called IdentityServerTools, which can be used to dynamically issue tokens whenever needed.

Example

IdentityServerTools is automatically available via the .NET Core DI, so whenever you need it, you can simply inject it.

An example is shown below. It’s pseudo-code, but the usage should be easy to understand.

public class TranslationGrantValidator : IExtensionGrantValidator { private readonly IdentityServerTools _identityServerTools;</p> public DelegationGrantValidator(IdentityServerTools identityServerTools) { _identityServerTools = identityServerTools; } public string GrantType => "translation"; public async Task ValidateAsync(ExtensionGrantValidationContext context) { var tokenToTranslate = context.Request.Raw.Get("token"); // omitted for brevity // validate "tokenToTranslate" and extract a user ID from it // now self issue a token var userSubscriptionToken = _identityServerTools.IssueClientJwtAsync("userSubscriptionClient", 15, new[] { "userSubscription.read" }, new[] { "https://user.subscription.service/" }); // prepare it for use against external service var httpClient = new HttpClient { BaseAddress = new Uri("https://user.subscription.service/") }; var request = new HttpRequestMessage(HttpMethod.Get, "/users/{userId}"); request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", userServiceToken); // call the external service to fetch more user subscription info and potentially perform extra validation based on that // i.e. a check if a user's subscription is active and so on // eventually, finalize the grant context.Result = new GrantValidationResult(userId, "translation", extraClaims); return; } } 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 public class TranslationGrantValidator : IExtensionGrantValidator { private readonly IdentityServerTools _identityServerTools ; < / p > public DelegationGrantValidator ( IdentityServerTools identityServerTools ) { _identityServerTools = identityServerTools ; } public string GrantType = > "translation" ; public async Task ValidateAsync ( ExtensionGrantValidationContext context ) { var tokenToTranslate = context . Request . Raw . Get ( "token" ) ; // omitted for brevity // validate "tokenToTranslate" and extract a user ID from it // now self issue a token var userSubscriptionToken = _identityServerTools . IssueClientJwtAsync ( "userSubscriptionClient" , 15 , new [ ] { "userSubscription.read" } , new [ ] { "https://user.subscription.service/" } ) ; // prepare it for use against external service var httpClient = new HttpClient { BaseAddress = new Uri ( "https://user.subscription.service/" ) } ; var request = new HttpRequestMessage ( HttpMethod . Get , "/users/{userId}" ) ; request . Headers . Authorization = new AuthenticationHeaderValue ( "Bearer" , userServiceToken ) ; // call the external service to fetch more user subscription info and potentially perform extra validation based on that // i.e. a check if a user's subscription is active and so on // eventually, finalize the grant context . Result = new GrantValidationResult ( userId , "translation" , extraClaims ) ; return ; } }

In the above example, our TranslationGrantValidator takes a dependency on an external HTTP API, which allows us to fetch extra details from a hypothetical “subscription service”. In this case, a translation of a token into a new token will not only depend on the fact that the supplied token is valid, but also on the fact that user’s subscription is active. That external service is protected by our own IdentityServer.

In order to interact with that service, we can self-issue a token. This is done by calling the IssueClientJwtAsync on the IdentityServerTools. We have to pass in a couple of parameters, such as the client ID (userSubscriptionClient) and the lifetime of the token in seconds (this can be really short since we know we will consume the token immediately, and only once). Additionally, we can also add some scopes and audiences to the token. Note that both the client and the scopes have to be correctly configured in IdentityServer.

Interestingly, there is no need to provide a client secret anymore, but that makes sense, as you are already within the trusted context of IdentityServer backend code.

And that’s it – hopefully the next time you need to self-issue an IdentityServer4 token, it will all be clear!