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([FromServices] TokenClient 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!