In a microservice world, the machine (microservice A) to machine (microservice B) communications can be secured using an OAuth 2.0 compatible token service, IdentityServer in our case. i.e To successfully call microservice B, microservice A needs to get an access token first via client credentials grant type from the IdentityServer with the right scope for the operation.
- microservice A calls IdentityServer to get an
access token
. - microservice A then calls microservice B with the access token in the authorization header (bearer token).
- microservice B validates the access token, responds to microservice A with a valid response.
The above steps are fine for a single call, but in case of multiple calls from A to B, A doesn’t need to get an IdentityServer token every time since each token has its validity, so basically, A can reuse the token until it expires.
For the token reusing, you don’t need to implement the caching and refreshing token logic anywhere since some good people who are maintaining IdentityModel.AspNetCore already done that. All we need to do is set up it, and consume it.
The library will take care of the following,
- caching abstraction for access tokens
- automatic renewal of expired access tokens
- token lifetime automation for HttpClient
let’s jump into the setup. Following are our assumptions,
- microservice B has endpoints,
POST /orders
and it requires IdentityServer token withorders.write
scope to create new orders,- and to fetch the orders
GET /orders
endpoint, the scope for that one isorders.read
.
- microservice A has valid client credentials configured on the IdentityServer with the above scopes added to the allowed scopes. i.e microservice A is allowed to call the microservice B.
So let’s configure microservice A,
- Install
IdentityModel.AspNetCore
package
In the Startup.cs
> ConfigureServices
// Setup token management service
services.AddAccessTokenManagement(options =>
{
options.Client.Clients.Add(AppConsts.StsClientName, new ClientCredentialsTokenRequest
{
Address = $"{Configuration.GetValue<string>("Authentication:Sts:BaseUrl")}/connect/token", // Token service (IdentityServer) base URL
ClientId = Configuration.GetValue<string>("Authentication:Sts:ClientId"),
ClientSecret = Configuration.GetValue<string>("Authentication:Sts:ClientSecret"),
Scope = $"{AppConsts.OrdersWriteScope} {AppConsts.OrdersReadScope}" // Any number of scopes, space separated
});
});
// Adds a named HTTP client for the factory that automatically sends the client access token
services.AddClientAccessTokenClient(AppConsts.StsClientName, AppConsts.StsClientName, client => client.BaseAddress = new Uri(Configuration.GetValue<string>("ApiBaseUrl"))); // microservice B base URL
Here we setup the token management service and a named HttpClient that uses the same.
Also you can observe, we moved the static string params to a AppConsts.cs
file,
public class AppConsts
{
public const string OrdersWriteScope = "orders.write";
public static string OrdersReadScope = "orders.read";
public static string StsClientName = "microservice_a_api_client";
}
And the configurations like client credentials, token service endpoint, and rest are kept in the appsettings.json
or somewhere more private like Azure App Configuration, keyvault, and loaded to the aspnetcore configurations.
"ApiBaseUrl": "https://microservice-b.com",
"Authentication": {
"Sts": {
"BaseUrl": "https://my-identityserver.com",
"ClientId": "microservice_a_api",
"ClientSecret": ""
}
},
Configuration is done, now let’s consume the HttpClient
.
Here I’m using Flurl
to make the HTTP request, if you prefer direct HttpClient usage, you can do that way. You don’t need to do anything special to get the access token since it is already taken care of.
using Flurl.Http;
public class GetOrdersQueryHandler : IRequestHandler<SomeInput, SomeOutput>
{
private readonly IFlurlClient _flurlClient;
public GetOrdersQueryHandler(IHttpClientFactory httpClientFactory)
{
var httpClient = httpClientFactory.CreateClient(AppConsts.StsClientName);
_flurlClient = new FlurlClient(httpClient);
}
public async Task<SomeOutput> Handle(SomeInput request, CancellationToken cancellationToken)
{
return await _flurlClient
.Request($"/orders")
.GetJsonAsync<SomeOutput>(cancellationToken);
}
}
As you can see, we use the named httpClient, and that client is already configured to have the access token with the scopes. That’s it. You can use the same way to call the create order endpoint since the client has an access token with both scopes.