George Kosmidis

Microsoft MVP | Speaks of Azure, AI & .NET | Founder of Munich .NET
Building tomorrow @
slalom
slalom

How to make authenticated requests to an ASP.NET Core web API using IdentityServer4 with Client Credentials.

by George Kosmidis / Published 4 years and 1 month ago

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

  1. Install the nuget package
  2. Add the settings to appsettings.json
  3. Register the service
  4. Make a request
  5. How to setup an Access Token Request
  6. More info on how to serialize requests, deserialize responses
  7. Attaching headers to the request
  8. 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 specified requestUri and returns the response wrapped in a ResponseObject<TResponseBody> object in an asynchronous operation. The body of the response will be deserialized to TResponseBody using Json.NET. Check the documentation for a full list of the GetAsync extension methods.
  • PostAsync<TRequestBody,TResponseBody>(String requestUri, TRequestBody requestBody)
    Sends a POST request to the specified requestUri and returns the response wrapped in a ResponseObject<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 to TResponseBody using Json.NET. Check the documentation for a full list of the PostAsync extension methods.
  • PutAsync<TRequestBody,TResponseBody>(String requestUri, TRequestBody requestBody)
    Sends a PUT request to the specified requestUri and returns the response wrapped in a ResponseObject<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 to TResponseBody using Json.NET. Check the documentation for a full list of the PutAsync extension methods.
  • DeleteAsync<TResponseBody>(String requestUri)
    Sends a DELETE request to the specified requestUri and returns the response wrapped in a ResponseObject<TResponseBody> object in an asynchronous operation. The body of the response will be deserialized to TResponseBody using Json.NET. Check the documentation for a full list of the DeleteAsync extension methods.
  • PatchAsync<TRequestBody,TResponseBody>(String requestUri, TRequestBody requestBody)
    Sends a PATCH request to the specified requestUri and returns the response wrapped in a ResponseObject<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 to TResponseBody using Json.NET. Check the documentation for a full list of the PatchAsync extension methods.
  • HeadAsync<TResponseBody>(String requestUri)
    Sends a HEAD request to the specified requestUri and returns the response wrapped in a ResponseObject<TResponseBody> object in an asynchronous operation. The body of the response will be deserialized to TResponseBody using Json.NET. Check the documentation for a full list of the HeadAsync 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 with ClientCredentialsOptions.

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 using TypeContent(...) to explitily set media-type and encoding, the defaults will be application/json and UTF-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 the TResponseBody to Stream, e.g. GetAsync<Stream>(...).
  • String BodyAsString Unless the TResponseBody is defined as Stream, 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 to TResponseBody, 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 entire HttpRequestMessage for debugging purposes.
  • HttpResponseMessage HttpResponseMessage The entire HttpResponseMessage.
  • 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.

This page is open source. Noticed a typo? Or something unclear?
Edit Page Create Issue Discuss
Microsoft MVP - George Kosmidis
Azure Architecture Icons - SVGs, PNGs and draw.io libraries