Making the IdentityModel Client Libraries HttpClientFactory friendly

IdentityModel has a number of protocol client libraries, e.g. for requesting, refreshing, revoking and introspecting OAuth 2 tokens as well as a client and cache for the OpenID Connect discovery endpoint.

While they work fine, the style around libraries that use HTTP has changed a bit recently, e.g.:

  • the lifetime of the HttpClient is currently managed internally (including IDisposable). In the light of modern APIs like HttpClientFactory, this is an anti-pattern.
  • the main extensibility point is HttpMessageHandler – again the HttpClientFactory promotes a more composable way via DelegatingHandler.

While I could just add more constructor overloads that take an HttpClient, I decided to explore another route (all credits for this idea goes to @randompunter).

I reworked all the clients to be simply extensions methods for HttpClient. This allows you to new up your own client or get one from a factory. This gives you complete control over the lifetime and configuration of the client including handlers, default headers, base address, proxy settings etc. – e.g.:

public async Task<string> NoFactory()
{
    var client = new HttpClient();
 
    var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
    {
        Address = "https://demo.identityserver.io/connect/token",
        ClientId = "client",
        ClientSecret = "secret"
    });
 
    return response.AccessToken ?? response.Error;
}

If you want to throw in the client factory – you can register the client like this:

services.AddHttpClient();

..and use it like this:

public async Task<string> Simple()
{
    var client = HttpClientFactory.CreateClient();
 
    var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
    {
        Address = "https://demo.identityserver.io/connect/token",
        ClientId = "client",
        ClientSecret = "secret"
    });
 
    return response.AccessToken ?? response.Error;
}

HttpClientFactory also supports named clients, which allows configuring certain things upfront, e.g. the base address:

services.AddHttpClient("token_client", 
    client => client.BaseAddress = new Uri("https://demo.identityserver.io/connect/token"));

Which means you don’t need to supply the address per request:

public async Task<string> WithAddress()
{
    var client = HttpClientFactory.CreateClient("token_client");
 
    var response = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
    {
        ClientId = "client",
        ClientSecret = "secret"
    });
 
    return response.AccessToken ?? response.Error;
}

You can also go one step further by creating a typed client, which exactly models the type of OAuth 2 requests you need to make in your application. You can mix that with the ASP.NET Core configuration model as well:

public class TokenClient
{
    public TokenClient(HttpClient client, IOptions<TokenClientOptions> options)
    {
        Client = client;
        Options = options.Value;
    }
 
    public HttpClient Client { get; }
    public TokenClientOptions Options { get; }
 
    public async Task<string> GetToken()
    {
        var response = await Client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
        {
            Address = Options.Address,
            ClientId = Options.ClientId,
            ClientSecret = Options.ClientSecret
        });
 
        return response.AccessToken ?? response.Error;
    }
}

..and register it like this:

services.Configure<TokenClientOptions>(options =>
{
    options.Address = "https://demo.identityserver.io/connect/token";
    options.ClientId = "client";
    options.ClientSecret = "secret";
});
 
services.AddHttpClient<TokenClient>();

…and use it e.g. like this:

public async Task<string> Typed([FromServicesTokenClient tokenClient)
{
    return await tokenClient.GetToken();
}

And one of my favourite features is the nice integration of the Polly library (and handlers in general) to give you extra features like retry logic:

services.AddHttpClient<TokenClient>()
    .AddTransientHttpErrorPolicy(builder => builder.WaitAndRetryAsync(new[]
    {
        TimeSpan.FromSeconds(1),
        TimeSpan.FromSeconds(2),
        TimeSpan.FromSeconds(3)
    }));

This is work in progress right now, but it feels like this is a better abstraction level than the current client implementations. I am planning to release that soon – if you have any feedback, please leave a comment here or open an issue on github. Thanks!

This entry was posted in ASP.NET Core, IdentityModel, Uncategorized, WebAPI. Bookmark the permalink.

6 Responses to Making the IdentityModel Client Libraries HttpClientFactory friendly

  1. Robo says:

    Love this!

    Are there any plans for a custom grant extension method? It’d be extremely useful for APIs that exchange tokens via extension grants http://docs.identityserver.io/en/release/topics/extension_grants.html

    Also, any thoughts on how automatic access token refresh might integrate with this approach? It’d be a shame to lose the functionality of RefreshTokenDelegatingHandler and the like.

  2. How can I now use the HttpClientFactory to make a call to a web api with the access token from the this TokenClient? Do I need to create and register a custom DelegatingHandler and inject the TokenClient and on every request set the Bearer header?

  3. Shimmy says:

    Hi and thanks for posting this wonderful stuff!
    I couldn’t find the TokenClientOptions, is that something provided as part of the IdentityModel package, or is a personal type?

Leave a comment