How to make authenticated requests to an ASP.NET Core web API using IdentityServer4 with Client Credentials.
This is a guide on how to make requests to a protected resource using Client Credentials with the IdentityServer4.Contrib.HttpClientService nuget package. The library is actually an HttpClient
service that makes it easy to make authenticated and resilient HTTP requests to protected by IdentityServer4 resources. Additionally, more features include automatic complex type serialization for requests / deserialization for responds (using Json.NET
), caching of the token response to reduce load and an HttpRequestMessage
factory that adds an “X-HttpClientService
” header for logging/tracking between cascading API calls.
Table of Contents
- Install the nuget package
- Add the settings to
appsettings.json
- Register the service
- Make a request
- How to setup an Access Token Request
- SetIdentityServerOptions(String)
- SetIdentityServerOptions<TOptions>(TOptions)
- SetIdentityServerOptions<TOptions>(IOptions<TOptions>)
- SetIdentityServerOptions<TOptions>(Action<TOptions>)
- More info on how to serialize requests, deserialize responses
- Attaching headers to the request
- HeadersAdd(String, String)
- HeadersAdd(String, List<String>)
- HeadersAdd(Dictionary<String, String>)
- HeadersAdd(Dictionary<String, List<String>>)
- HeadersClear()
- HeadersRemove(String)
- Conclusion
Install the nuget package
Install the IdentityServer4.Contrib.HttpClientService nuget package, using either the Package Manager or .NET CLI
, or any other method:
> dotnet add package IdentityServer4.Contrib.HttpClientService --version 2.2.1
Add the settings to appsettings.json
The IdentityServer4 settings can be passed in using one of the SetIdentityServerOptions
overloads which are explored later in section “How to setup an Access Token Request“. Although it is always a good idea to keep the code clean of any hard-coded values and use the appsettings.json
, this step is only necessary for the SetIdentityServerOptions(String)
overload.
//The values here are part of the demo offered in https://demo.identityserver.io/
{
//...
"SomeClientCredentialsOptions": {
"Address": "https://demo.identityserver.io/connect/token",
"ClientId": "m2m",
"ClientSecret": "secret",
"Scopes": "api"
}
//...
}
The configuration section should always be or end with
ClientCredentialsOptions
. In the example before, the section name was “SomeClientCredentialsOptions
“.
Register the service
Although there is a singleton instance (HttpClientServiceFactory.Instance
) available, using DI for ASP.NET Core WebAPIs is the only way to go. The library uses internally typed clients and thus the HttpClientFactory to create resilient HTTP requests and avoid socket exhaustion issues.
Steve Gordon explores HttpClient internals and potential problems in HttpClient creation and disposal internals: Should I dispose of HttpClient?
In the following example, the HttpClientService
is registered in Startup.cs
in ConfigureServices(IServiceCollection services)
without configuring any options. This is still possible, as some of the SetIdentityServerOptions
overloads allow late configuration but it excludes the use of the SetIdentityServerOptions(IOptions<>)
overload:
//...
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddHttpClientService(); //Adds the HTTP client service to the service collection
//For typed configuration use: .Configure<ClientCredentialsOptions>(Configuration.GetSection(nameof(ClientCredentialsOptions)));
//...
}
//...
As mentioned, the approach above did not include options configuration and the SetIdentityServerOptions(IOptions<>)
overload was excluded. In the example that follows, multiple custom services are registered and configured, allowing the use of the options pattern:
//...
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddHttpClientService()
.AddSingleton<IYourCustomService, YourCustomService>()
.Configure<SomeClientCredentialOptions>(Configuration.GetSection(nameof(SomeClientCredentialOptions)))
.AddSingleton<IYourCustomService2, YourCustomService2>()
.Configure<DifferentClientCredentialOptions>(Configuration.GetSection(nameof(DifferentClientCredentialOptions))); //...
}
//...
Make a request
Either if it is a controller or any custom service, the constructor of that class should request an IHttpClientServiceFactory
instance. The injected singleton instance (in the example below _requestServiceFactory
), can then be used to create an HttpClientService
instance that will hold all common information for all the requests that will be carried out by that instance. That allows adding IdentityServer4
options and Headers
only once, and then using the same HttpClientService
multiple times:
using IdentityServer4.Contrib.HttpClientService.Extensions;
[ApiController]
[Route("customers")]
public class CustomerController : ControllerBase
{
//Request the IHttpClientServiceFactory instance in your controller or service
private readonly HttpClientService _httpClientService;
public CustomerController(IHttpClientServiceFactory requestServiceFactory){
_httpClientService = requestServiceFactory
//Create a instance of the service
.CreateHttpClientService()
//Set the settings for the IdentityServer4 authentication
.SetIdentityServerOptions("ClientCredentialsOptions")
}
[HttpGet]
public async Task<IActionResult> Get(){
//Make the request
var responseObject = await _httpClientService
//GET and deserialize the response body to IEnumerable<Customers>
.GetAsync<IEnumerable<Customers>>("https://api/customers");
//Do something with the results
if (!responseObject.HasError)
{
var customers = responseObject.BodyAsType;
return Ok(customers);
}
else
{
var httpStatusCode = responseObject.StatusCode;
var errorMessage = responseObject.Error;
return StatusCode((int)httpStatusCode, errorMessage);
}
}
}
Similarly enough, all the HTTP verbs are supported. In the example that follows, a new action is added that uses the same HttpClientService
for a POST request:
using IdentityServer4.Contrib.HttpClientService.Extensions;
[ApiController]
[Route("customers")]
public class CustomerController : ControllerBase
{
//Request the IHttpClientServiceFactory instance in your controller or service
private readonly HttpClientService _httpClientService;
public CustomerController(IHttpClientServiceFactory requestServiceFactory){
_httpClientService = requestServiceFactory
//Create a instance of the service
.CreateHttpClientService()
//Set the settings for the IdentityServer4 authentication
.SetIdentityServerOptions("ClientCredentialsOptions")
}
[HttpGet]
public async Task<IActionResult> Get(){
//Make the request
var responseObject = await _httpClientService
//GET and deserialize the response body to IEnumerable<Customers>
.GetAsync<IEnumerable<Customers>>("https://api/customers");
//Do something with the results
if (!responseObject.HasError)
{
var customers = responseObject.BodyAsType;
return Ok(customers);
}
else
{
var httpStatusCode = responseObject.StatusCode;
var errorMessage = responseObject.Error;
return StatusCode((int)httpStatusCode, errorMessage);
}
}
[HttpPost]
public async Task<IActionResult> Post([FromBody]CustomerForRequest customerForRequest){
//Make the request
var responseObject = await _httpClientService
//Execute a POST request by setting the type of the request body to CustomerForRequest
// and Response to CustomerFromResponse
.PostAsync<CustomerForRequest, CustomerFromResponse>("https://api/customers", customerForRequest);
//Do something with the results
if (!responseObject.HasError)
{
var customerFromResponse = responseObject.BodyAsType;
return Ok(customerFromResponse);
}
else
{
var httpStatusCode = responseObject.StatusCode;
var errorMessage = responseObject.Error;
return StatusCode((int)httpStatusCode, errorMessage);
}
}
}
The
SetIdentityServerOptions("SomeClientCredentialsOptions")
might be the simplest way of setting up an Access Token Request, the Options Pattern though is the suggested way. Explore in the following section “How to setup an Access Token Request“, all the ways of setting up IdentityServer options.
Following are the most useful extension methods for an HTTP request:
GetAsync<TResponseBody>(String requestUri)
Sends a GET request to the specifiedrequestUri
and returns the response wrapped in aResponseObject<TResponseBody>
object in an asynchronous operation. The body of the response will be deserialized toTResponseBody
usingJson.NET
. Check the documentation for a full list of theGetAsync
extension methods.PostAsync<TRequestBody,TResponseBody>(String requestUri, TRequestBody requestBody)
Sends a POST request to the specifiedrequestUri
and returns the response wrapped in aResponseObject<TResponseBody>
object in an asynchronous operation. The body of the request will be of type TRequestBody and will be serialized using Json.NET. The body of the response will be deserialized toTResponseBody
usingJson.NET
. Check the documentation for a full list of thePostAsync
extension methods.PutAsync<TRequestBody,TResponseBody>(String requestUri, TRequestBody requestBody)
Sends a PUT request to the specifiedrequestUri
and returns the response wrapped in aResponseObject<TResponseBody>
object in an asynchronous operation. The body of the request will be of type TRequestBody and will be serialized using Json.NET. The body of the response will be deserialized toTResponseBody
usingJson.NET
. Check the documentation for a full list of thePutAsync
extension methods.DeleteAsync<TResponseBody>(String requestUri)
Sends a DELETE request to the specifiedrequestUri
and returns the response wrapped in aResponseObject<TResponseBody>
object in an asynchronous operation. The body of the response will be deserialized toTResponseBody
usingJson.NET
. Check the documentation for a full list of theDeleteAsync
extension methods.PatchAsync<TRequestBody,TResponseBody>(String requestUri, TRequestBody requestBody)
Sends a PATCH request to the specifiedrequestUri
and returns the response wrapped in aResponseObject<TResponseBody>
object in an asynchronous operation. The body of the request will be of type TRequestBody and will be serialized using Json.NET. The body of the response will be deserialized toTResponseBody
usingJson.NET
. Check the documentation for a full list of thePatchAsync
extension methods.HeadAsync<TResponseBody>(String requestUri)
Sends a HEAD request to the specifiedrequestUri
and returns the response wrapped in aResponseObject<TResponseBody>
object in an asynchronous operation. The body of the response will be deserialized toTResponseBody
usingJson.NET
. Check the documentation for a full list of theHeadAsync
extension methods.
How to setup an Access Token Request
The library supports multiple ways for setting up the necessary options to retrieve an access token, with the SetIdentityServerOptions
overloads. Upon success of retrieving one, the result is cached until the token expires; that means that a new request to a protected resource does not necessarily means a new request for an access token.
SetIdentityServerOptions(String)
Setup IdentityServer options by defining the configuration section where the settings exist. The type of the options (ClientCredentialsOptions
or PasswordOptions
) will be determined based on the name of the section (it should be or end with “ClientCredentialsOptions
” or “PasswordOptions
“):
//...
_requestService = requestServiceFactory
//Create a instance of the service
.CreateHttpClientService()
//Set the settings for the IdentityServer4 authentication
.SetIdentityServerOptions("ClientCredentialsOptions")
//...
Although this option is not very useful for PasswordTokenRequest
, the section can contain the properties of either the ClientCredentialsOptions
or PasswordOptions
objects.
For the
SetIdentityServerOptions(String)
to work, the configuration section name should either be or end withClientCredentialsOptions
.
Read the technical specifications of the
SetIdentityServerOptions(String)
in the documents.
SetIdentityServerOptions<TOptions>(TOptions)
Setup IdentityServer options by passing a ClientCredentialsOptions
or PasswordOptions
object directly to the SetIdentityServerOptions<TOptions>(TOptions)
:
//...
_httpClientService = requestServiceFactory
//Create a instance of the service
.CreateHttpClientService()
//Set the settings for the IdentityServer4 authentication
.SetIdentityServerOptions(
new PasswordOptions
{
Address = "https://demo.identityserver.io/connect/token",
ClientId = "ClientId",
ClientSecret = "ClientSecret",
Scope = "Scope",
Username = "Username",
Password = "Password"
}
)
//...
Read the technical specifications of the
SetIdentityServerOptions<TOptions>(TOptions)
in the documents.
SetIdentityServerOptions<TOptions>(IOptions<TOptions>)
Setup IdentityServer options using the options pattern (read more about the options pattern in Microsoft Docs):
Startup.cs
//...
public void ConfigureServices(IServiceCollection services)
{
//...
services.AddHttpClientService()
.AddSingleton<IProtectedResourceService, ProtectedResourceService>()
.Configure<ClientCredentialsOptions>(Configuration.GetSection(nameof(ClientCredentialsOptions)));
//...
}
//...
ProtectedResourceService.cs
//...
public class ProtectedResourceService : IProtectedResourceService
{
private readonly IHttpClientServiceFactory _requestServiceFactory;
private readonly IOptions<ClientCredentialsOptions> _identityServerOptions;
public ProtectedResourceService(IHttpClientServiceFactory requestServiceFactory, IOptions<ClientCredentialsOptions> identityServerOptions)
{
_requestServiceFactory = requestServiceFactory;
_identityServerOptions = identityServerOptions;
}
public async Task<YourObject> Get(){
_requestServiceFactory
.CreateHttpClientService()
.SetIdentityServerOptions(_identityServerOptions)
.GetAsync<YourObject>("https://url_that_returns_YourObject");
}
)
//...
Read the technical specifications of the
SetIdentityServerOptions<TOptions>(IOptions<TOptions>)
in the documents.
SetIdentityServerOptions<TOptions>(Action<TOptions>)
Setup IdentityServer options using a delegate:
//...
_httpClientService = requestServiceFactory
//Create a instance of the service
.CreateHttpClientService()
//Set the settings for the IdentityServer4 authentication
.SetIdentityServerOptions<PasswordOptions>( x => {
x.Address = "https://demo.identityserver.io/connect/token";
x.ClientId = "ClientId";
x.ClientSecret = "ClientSecret";
x.Scope = "Scope";
x.Username = "Username";
x.Password = "Password";
}
)
//...
Read the technical specifications of the
SetIdentityServerOptions<TOptions>(IOptions<TOptions>)
in the documents.
More info on how to serialize requests, deserialize responses
Responses will be deserialized to the type TResponseBody
of all the SendAsync<TRequestBody, TResponseBody>(Uri, HttpMethod, TRequestBody)
extension methods. The example that follows uses the GetAsync<TResponseBody>(String)
extension method to sent GET request to the URL passed as argument; the response then is deserialized to a ResponsePoco
instance :
//...
var responseObject = await _requestServiceFactory
.CreateHttpClientService()
.GetAsync<ResponsePoco>("https://url_that_returns_ResponsePoco_in_json");
//...
Requests that contain a body (POST, PUT and PATCH) behave similar, by deserializing the response body and serializing the request body. In the example that follows the type TRequestBody
of the PostAsync<TRequestBody,TResponseBody>()
extension method defines the type of the requestPoco
object. The request will be serialized and attached in the request body using JsonConvert.SerializeObject(requestPoco, Formatting.None)
. :
//...
var responseObject = await _requestServiceFactory
.CreateHttpClientService()
.PostAsync<RequestPoco,ResponsePoco>("https://url_that_accepts_RequestPoco_and_responds_with_ResponsePoco", requestPoco);
//...
Use the
TypeContent(TRequestBody, Encoding, string)
to set the media type and encoding of a request body. Without usingTypeContent(...)
to explitily set media-type and encoding, the defaults will beapplication/json
andUTF-8
.
ResponseObject<TResponseBody>
The object returned by all extension methods (GetAsync, PostAsync, PutAsync etc) is of type ResponseObject<TResponseBody>
and contains multiple properties. Three of them, the properties starting with “BodyAs…”, hold the body of the response either as String
, as Stream
or as TResponseBody
. Follows, a list of all properties and a quick explanation:
Stream BodyAsStream
This property is populated by setting theTResponseBody
toStream
, e.g.GetAsync<Stream>(...)
.String BodyAsString
Unless theTResponseBody
is defined asStream
, this will always contain the body of the respone as a raw string.TResponseBody BodyAsType
The property will attempt to deserialize the raw string of the response toTResponseBody
, e.g.GetAsync<CustomerModel>(...)
.String Error
The error, if any, of the request attempt.bool HasError
A boolean indicating if the something went wrong with the request.HttpResponseHeaders Headers
A collection of the response headers.HttpRequestMessge HttpRequestMessge
The entireHttpRequestMessage
for debugging purposes.HttpResponseMessage HttpResponseMessage
The entireHttpResponseMessage
.HttpStatusCode StatusCode
The HttpStatusCode returned.
TypeContent(TRequestBody, Encoding, string)
Allowing the library to serialize an object and attach it to a request in the body, will by default set the Content-Type
header to application/json; charset=utf-8
. This default behavior can change with the TypeContent(TRequestBody, Encoding, string)
class:
var responseObject = await _requestServiceFactory
//Create a instance of the service
.CreateHttpClientService()
//.PostAsync<TRequestBody,TResponseBody>(URL, customer of type Customer1)
.PostAsync<TypeContent<CustomerForRequest>,CustomerFromResponse>(
"https://api/customers",
new TypeContent(customerForRequest, Encoding.UTF8, "application/json")
);
Attaching headers to the request
Header management is done using a set of methods that adhere to the general method chaining approach. Although in total 6 methods, they actually do two jobs, adding or removing headers. In the list that follows, the first four methods add headers, and the last two remove:
HeadersAdd(String, String)
Adds a header to the request. If a second header with the same name already exists, their values will be aggregated to a List<T>, which will result in the values passes as comma separated in the request.
HeadersAdd(String, List<String>)
Adds a header to the request. If a second header with the same name already exists, their values will be aggregated to a List<T>, which will result in the values passes as comma separated in the request.
HeadersAdd(Dictionary<String, String>)
Adds a collection of headers in the request using a Dictionary<String,String>, where the Key of the Dictionary is the header name and the value is the header value. If a second header with the same name already exists, their values will be aggregated to a List<T>, which will result in the values passes as comma separated in the request.
HeadersAdd(Dictionary<String, List<String>>)
Adds a collection of headers in the request using a Dictionary<String,List<String>>, where the Key of the Dictionary is the header name and the value is a list of header values. If a second header with the same name already exists, their values will be aggregated to a List<T>, which will result in the values passes as comma separated in the request.
HeadersClear()
Clears all headers from the request. Use method chaining to reset headers for an HttpClientService
, e.g. httpClientService.HeadersClear().HeadersAdd(...)
HeadersRemove(String)
Removes header from the headers collection by name. Use method chaining to reset headers for an HttpClientService
, e.g. httpClientService.HeadersRemove("X-Header").HeadersAdd("X-Header",...)
The following example use method chaining to illustrate a rather non realistic scenario of using all methods:
var responseObject = await _requestServiceFactory
.CreateHttpClientService()
//Adds a header to the request, key-value style.
.HeadersAdd("X-Custom-Header", "value")
//Adds a header to the request, key-value style, where the value is actually a list of values.
.HeadersAdd("X-Custom-Header", new List<String>{ "value1", "value2" })
//Adds a collection of headers in the request using a Dictionary<String,String>,
// where the Key of the Dictionary is the header name and the value is the header value
.HeadersAdd(new Dictionary<String,String>{ {"X-Custom-Header", "value"} })
//Adds a collection of headers in the request using a Dictionary<String,String>,
// where the Key of the Dictionary is the header name and the value is the header value
.HeadersAdd(new Dictionary<String,List<String>>{ {"X-Custom-Header", {"value1","value2"}} })
//Continue setting identity server option, making call etc
.PostAsync<RequestPoco,ResponsePoco>("https://url_that_accepts_RequestPoco_and_responds_with_ResponsePoco", requestPoco);
Conclusion
The IdentityServer4.Contrib.HttpClientService fluent interface makes it easy to create request to protected by IdentityServer4 resources. More over, it makes your code cleaner, by enabling you to preset an HttpClientService
with the identity server options needed to login and / or any additional headers you might need. For example, in the following gist the options for the identity server and a custom header is set once, and multiple GET requests are sent in the loop using the same HttpClientService
:
public class YourService
{
//Request the IHttpClientServiceFactory instance in your controller or service
private readonly HttpClientService _httpClientService;
public CustomerController(IHttpClientServiceFactory requestServiceFactory)
{
_httpClientService = requestServiceFactory
//Create a instance of the service
.CreateHttpClientService()
//Set the settings for the IdentityServer4 authentication
.SetIdentityServerOptions("ClientCredentialsOptions")
//Add a custom header
.HeadersAdd("X-Header", "some-value")
}
public async Task SentSomeNumbers(IEnumerable<int> someNumbers)
{
foreach(var i in someNumbers) {
//Reuse the HttpClientService
await _httpClientService.GetAsync("https://api/sent_me_numbers")
}
}
}
Check the code on Github, and give it a star if you like it! The library is available as a nuget package with the name IdentityServer4.Contrib.HttpClientService.