In a Microservices architecture, we typically observe the number of services increasing as a result of an organic growth, and some services start talking with other services. Ideally the inter-services communication should be asynchronous following a publish-subscribe pattern, but in practice we have some services that need to follow a request-reply pattern.
This is typically the case when we have Backends-for-Frontends (BFF) acting as separate API gateways for each front-end client. These BFFs need to talk to other services, typically issuing queries to fetch data, or commands to perform business actions being handled by the downstream service. Most of the time these interactions are made through REST APIs, and we need to have a way to authorize these requests, even if all these services are running within the internal network perimeter with restrictions from the outside world, following a Zero Trust strategy.
Besides BFFs, another scenario where we need to authorize a service is when we have external services running on-premise, outside of the cloud, and there’s no human interaction in these services, for example having a windows service that collects some data from on-premise installations and send it to a service that lives in the cloud.
There are several solutions to implement Service-to-Service authorization, known also as Machine to Machine (M2M) authorization, such as API keys, Mutual TLS, OAuth, etc. In this post I want to explore using OAuth 2.0 with the Client Credentials Grant flow in the context of ASP.NET Core and Amazon Cognito.
The OAuth 2.0 Client Credentials Grant flow explained:
MyBFF) needs to call the Resource Server (MyService), if
it doesn’t have a valid access token yet, makes a request to the
Authorization Server (AWS Cognito) passing tthe client_id and
client_secretaccess_token with a
given expiration in expires_inMyBFF calls MyService to get some data, passing the access_token as a
Bearer token in the Authorization headerMyService validates the request and returns the data if the Bearer token
is valid.To setup Amazon Cognito for our scenario we need the following resources:
MyService. We will have as many
resource servers as the number of services.MyBFF. We will have
as many clients as the number of services that are calling other services.I’m going to use CloudFormation to create all these resources instead of AWS console because:
# Create a user pool
MyServicesUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: "MyServicesUserPool"
# Disable Self-Service Account Recovery
AccountRecoverySetting:
RecoveryMechanisms:
- Name: admin_only
Priority: 1
# Disable Self-Service Signup
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true
# Just in case a user is created by admin
MfaConfiguration: "ON"
EnabledMfas:
- SOFTWARE_TOKEN_MFAThe important thing here to notice is that I have disabled all self-service operations from end-users since we won’t have any users registered in the pool. We can check the result in AWS console.
In the Sign-in experience tab
In the Sign-up experience tab
# Creates a domain with oauth endpoints
MyServicesUserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: replace-your-domain-here
UserPoolId: !Ref MyServicesUserPoolWe are using a AWS Cognito domain:
https://[replace-your-domain-here].auth.us-east-1.amazoncognito.com
Note: We could have used a custom domain instead of a Amazon Cognito domain, which requires adding some DNS records to your DNS zone.
MyService # Resource Server for MyService
MyServiceUserPoolResourceServer:
Type: AWS::Cognito::UserPoolResourceServer
Properties:
Identifier: MyService
Name: MyService
# Organize scopes
Scopes:
- ScopeName: weather_read
ScopeDescription: Grants the ability to read weather
UserPoolId: !Ref MyServicesUserPoolWe can register whatever scopes we want to support in our service. In this
case we are just registering one scope MyService/weather_read that grants the
ability to read weather data from MyService (which will expose an endpoint to
retrieve weather data).
We can check the result in the App Integration tab
MyBFF # Client for MyBFF
MyBFFUserPoolClient:
Type: AWS::Cognito::UserPoolClient
DependsOn: MyServiceUserPoolResourceServer
Properties:
ClientName: MyBFF
GenerateSecret: true
AccessTokenValidity: 1 # Hours
RefreshTokenValidity: 30 # Days
AllowedOAuthFlows:
- client_credentials
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthScopes:
- MyService/weather_read
EnableTokenRevocation: true
ExplicitAuthFlows:
- ALLOW_REFRESH_TOKEN_AUTH
UserPoolId: !Ref MyServicesUserPoolBasically we are registering a client, with a secret, which is only allowed
to trigger the client_credentials grant flow with the scopes
MyService/weather_read. In other words, we are allowing this client
MyBFF to talk to resource server MyService.
We can check the result in AWS console, in the App Integration tab
I’m not setting all the Advanced Security features configuration available that we would use in a regular User Pool supporting other flows that deal with real users (Authorization Code, Implicit, etc,). I’m also not enabling AWS Web Application Firewall (WAF), which is something that we would do in a real scenario.
This is the template I am using for the example of this blog post.
AWSTemplateFormatVersion: "2010-09-09"
Description: AWS Cognito with OAuth Client Credentials Grant flow
Resources:
# Create a user pool
MyServicesUserPool:
Type: AWS::Cognito::UserPool
Properties:
UserPoolName: "MyServicesUserPool"
# Disable Self-Service Account Recovery
AccountRecoverySetting:
RecoveryMechanisms:
- Name: admin_only
Priority: 1
# Disable Self-Service Signup
AdminCreateUserConfig:
AllowAdminCreateUserOnly: true
# Just in case a user is created by admin, we require MFA
MfaConfiguration: "ON"
EnabledMfas:
- SOFTWARE_TOKEN_MFA
# Creates a domain with oauth endpoints
MyServicesUserPoolDomain:
Type: AWS::Cognito::UserPoolDomain
Properties:
Domain: <your-gognito-domain>
UserPoolId: !Ref MyServicesUserPool
# Resource Server for MyService
MyServiceUserPoolResourceServer:
Type: AWS::Cognito::UserPoolResourceServer
Properties:
Identifier: MyService
Name: MyService
Scopes: # Organize scopes
- ScopeName: weather_read
ScopeDescription: Grantes the ability to read weather
UserPoolId: !Ref MyServicesUserPool
# Client for MyBFF
MyBFFUserPoolClient:
Type: AWS::Cognito::UserPoolClient
DependsOn: MyServiceUserPoolResourceServer
Properties:
AccessTokenValidity: 1
AllowedOAuthFlows:
- client_credentials
AllowedOAuthFlowsUserPoolClient: true
AllowedOAuthScopes:
- MyService/weather_read
ClientName: MyBFF
EnableTokenRevocation: true
ExplicitAuthFlows:
- ALLOW_REFRESH_TOKEN_AUTH
GenerateSecret: true
RefreshTokenValidity: 30
UserPoolId: !Ref MyServicesUserPool
We can submit this template as a CloudFormation stack in AWS console and wait for its deployment completion.
At this point we can test the Token
endpoint
to get a new access_token
https://[replace-your-domain-here].auth.us-east-1.amazoncognito.com/oauth2/token
curl --location 'https://replace-your-domain-here.auth.us-east-1.amazoncognito.com/oauth2/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'grant_type=client_credentials' \
--data-urlencode 'client_id=your_client_id' \
--data-urlencode 'client_secret=your_cliend_secret'Here is the result in Postman
One thing that we can check is the content of the access_token in jwt.io
Things to notice about the access_token issued by AWS Cognito in a Client Crdentials Grant flow:
sub (subject) is the client_idaud claimtoken_use with value accessscope claim has the list of scopes, which in this
case includes the scope MyService/weather_read,MyService Setup (Resource Server)Now it’s time to build the service MyService using ASP.NET Core. I will use Minimal APIs in ASP.NET Core 8.
Let’s start by creating a webapi project for MyService.
dotnet new webapi -o MyServiceNavigate to folder MyService, and open the project with your preferred editor.
Let’s run it from the command line.
dotnet run --launch-profile https
Building...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7062
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5235
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\temp\MyServiceNow let’s open the swagger ui in the browser, and execute the operation GET /weatherforecast to get weather forecast data as a response.
We could do the same with curl
curl -X 'GET' 'https://localhost:7062/weatherforecast'
[{"date":"2024-02-10","temperatureC":50,"summary":"Balmy","temperatureF":121},{"date":"2024-02-11","temperatureC":48,"summary":"Chilly","temperatureF":118},{"date":"2024-02-12","temperatureC":23,"summary":"Mild","temperatureF":73},{"date":"2024-02-13","temperatureC":50,"summary":"Warm","temperatureF":121},{"date":"2024-02-14","temperatureC":-4,"summary":"Chilly","temperatureF":25}]Now we want to protect MyService to only allow authorized clients with access tokens issued by the AWS Cognito User pool we have created previously.
Let’ check the initial Program.cs (just removing comments from the initial
template)
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", () =>
{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}And now let’s make the changes to protect the /weatherforecast endpoint.
First, and since we want to support JWT tokens, we need to add the nuget package
Microsoft.AspNetCore.Authentication.JwtBearer
dotnet add package Microsoft.AspNetCore.Authentication.JwtBearerAnd now let’s check the differences in order to protect our endpoint.
using System.Security.Claims;using Microsoft.AspNetCore.Authorization;
var builder = WebApplication.CreateBuilder(args);
// Register Authentication with JWT Bearer tokensbuilder.Services.AddAuthentication().AddJwtBearer();// Register Authorization Policy based on Scopesbuilder.Services.AddAuthorization(options =>{ options.AddPolicy("WeatherReadPolicy", policyBuilder => { policyBuilder.RequireAuthenticatedUser(); policyBuilder.RequireClaim("token_use", "access"); policyBuilder.RequireAssertion(ctx => ctx.User.FindFirstValue("scope")?.Split(' ').Any(s => s.Equals("MyService/weather_read")) ?? false ); });});builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
var summaries = new[] {
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
app.MapGet("/weatherforecast", [Authorize(Policy = "WeatherReadPolicy")] () =>{
var forecast = Enumerable.Range(1, 5).Select(index =>
new WeatherForecast
(
DateOnly.FromDateTime(DateTime.Now.AddDays(index)),
Random.Shared.Next(-20, 55),
summaries[Random.Shared.Next(summaries.Length)]
))
.ToArray();
return forecast;
})
.WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
{
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
}These are the changes:
.well-known url configuration for our user pool, which is in the appsettings.json file (see below)WeatherReadPolicy to check for the scope MyService/weather_read[Authorize] attribute with the policy
WeatherReadPolicy in order to protect the endpoint.In the appsettings.json we need to register the metadata address
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*",
"Authentication": { "Schemes": { "Bearer": { "MetadataAddress": "https://cognito-idp.us-east-1.amazonaws.com/<your-cognito-user-pool-id>/.well-known/openid-configuration" } } }}
Let’s call again the endpoint without any authorization, and we should see an error.
curl -X 'GET' 'https://localhost:7062/weatherforecast' -iHTTP/1.1 401 Unauthorized
Content-Length: 0
Date: Mon, 12 Feb 2024 12:21:58 GMT
Server: Kestrel
WWW-Authenticate: BearerAs expected, the service returns now a 401 Unauthorized.
Let’s call the endpoint with a valid bearer token returned from the Cognito token endpoint
curl -X 'GET' 'https://localhost:7062/weatherforecast' -i -H 'Authorization: Bearer <your-access-token>'HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 12 Feb 2024 12:24:16 GMT
Server: Kestrel
Transfer-Encoding: chunked
[{"date":"2024-02-13","temperatureC":41,"summary":"Cool","temperatureF":105},{"date":"2024-02-14","temperatureC":4,"summary":"Freezing","temperatureF":39},{"date":"2024-02-15","temperatureC":50,"summary":"Cool","temperatureF":121},{"date":"2024-02-16","temperatureC":5,"summary":"Sweltering","temperatureF":40},{"date":"2024-02-17","temperatureC":8,"summary":"Hot","temperatureF":46}]We got a 200 OK. Now that we have MyService, the Resource Server, behaving as expected, let’s setup the client, i.e, MyBFF.
MyBFF Setup (Client)Let’s repeat the ASP.NET Core setup for the Backend-for-frontend MyBFF.
dotnet new webapi -o MyBFFGo to folder MyBFF, and using your preferred editor, replace the endpoint
logic of /weatherforecast to make a REST API call to MyService in order to
get weather forecast data. I am using the Refit
library to make the REST call, which means
that I need to add the nuget package
Refit.HttpClientFactory
dotnet add package Refit.HttpClientFactoryAnd here is the code.
using Microsoft.AspNetCore.Mvc;using Refit;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
builder.Services.AddRefitClient<IMyServiceClient>() .ConfigureHttpClient(client => client.BaseAddress = new Uri("https://localhost:7062"));
var app = builder.Build();
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.MapGet("/weatherforecast", ([FromServices] IMyServiceClient myServiceClient) =>{ return myServiceClient.GetWeatherForecast();}).WithName("GetWeatherForecast")
.WithOpenApi();
app.Run();
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary);interface IMyServiceClient{ [Get("/weatherforecast")] Task<WeatherForecast[]> GetWeatherForecast();}Let’s check the differences from the original template:
MyServiceMyService to get weather forecastAt this point, if we run MyBFF and call its endpoint /weatherforecast we
should see an error since we are doing an unauthorized call to MyService
because we don’t have any logic yet to pass a JWT bearer token when doing the
call.
Assuming that MyService is running and listening at https://localhost:7062,
let’s also run MyBFF. In MyBFF folder
dotnet run --launch-profile httpsBuilding...
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7248
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5006
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\temp\MyBFFAnd now let’s call MyBFF endpoint https://localhost:7248/weatherforecast
curl -X 'GET' 'https://localhost:7248/weatherforecast' -iHTTP/1.1 500 Internal Server Error
Content-Type: text/plain; charset=utf-8
Date: Mon, 12 Feb 2024 11:03:38 GMT
Server: Kestrel
Transfer-Encoding: chunked
Refit.ApiException: Response status code does not indicate success: 401 (Unauthorized).
at Refit.RequestBuilderImplementation.<>c__DisplayClass14_0`2.<<BuildCancellableTaskFuncForMethod>b__0>d.MoveNext() in /_/Refit/RequestBuilderImplementation.cs:line 288
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Http.RequestDelegateFactory.<ExecuteTaskOfTFast>g__ExecuteAwaited|132_0[T](Task`1 task, HttpContext httpContext, JsonTypeInfo`1 jsonTypeInfo)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)
HEADERS
=======
Accept: */*
Host: localhost:7248
User-Agent: curl/8.4.0Note ⚠️: detailed exception information is being returned because I’m running in a Development environment. In Production we should not return any detailed information about the exception.
Basically we are getting a 500 Internal Server Error as a result of a failed call to MyService, which is returning a 401 Unauthorized to MyBFF. We can also check the logs of MyBFF
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\temp\MyBFF
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[100]
Start processing HTTP request GET https://localhost:7062/weatherforecast
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[100]
Sending HTTP request GET https://localhost:7062/weatherforecast
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[101] Received HTTP response headers after 73.9603ms - 401info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[101] End processing HTTP request after 92.3572ms - 401fail: Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware[1] An unhandled exception has occurred while executing the request.
Refit.ApiException: Response status code does not indicate success: 401 (Unauthorized).
at Refit.RequestBuilderImplementation.<>c__DisplayClass14_0`2.<<BuildCancellableTaskFuncForMethod>b__0>d.MoveNext() in /_/Refit/RequestBuilderImplementation.cs:line 288
--- End of stack trace from previous location ---
at Microsoft.AspNetCore.Http.RequestDelegateFactory.<ExecuteTaskOfTFast>g__ExecuteAwaited|132_0[T](Task`1 task, HttpContext httpContext, JsonTypeInfo`1 jsonTypeInfo)
at Swashbuckle.AspNetCore.SwaggerUI.SwaggerUIMiddleware.Invoke(HttpContext httpContext)
at Swashbuckle.AspNetCore.Swagger.SwaggerMiddleware.Invoke(HttpContext httpContext, ISwaggerProvider swaggerProvider)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddlewareImpl.Invoke(HttpContext context)To fix it, before calling MyService, we need to be sure that we have a valid
access_token to pass it as bearer token in the authorization header. Basically
we need the following logic in the REST client:
check if we have a valid access_token:
access_token exists, we need to call the Authorization
Server passing the client_id and client_secret to get a new
access_tokenPass the access_token as a bearer token in the Authorization header.
We can use the IdentityModel library to help us implementing this logic in
MyBFF, which means that we need to install the nuget package
IdentityModel.AspnetCore
dotnet add package IdentityModel.AspnetCoreNow, we just need to register the Access Token Management Service provided by the IdentityModel library and attach it to the Refit http Client.
builder.Services.AddAccessTokenManagement(options =>
{
options.Client.Clients.Add("MyBFF", new()
{
RequestUri = new Uri(builder.Configuration["AUTH_SERVER_TOKEN_ENDPOINT_URL"]!),
ClientId = builder.Configuration["AUTH_SERVER_MYBFF_CLIENT_ID"],
ClientSecret = builder.Configuration["AUTH_SERVER_MYBFF_CLIENT_SECRET"]
});
});
builder.Services.AddRefitClient<IMyServiceClient>()
.ConfigureHttpClient(client => client.BaseAddress = new Uri("https://localhost:7062"))
.AddClientAccessTokenHandler("MyBFF");Just be sure that you add the following configuration keys to the configuration source that you are using.
AUTH_SERVER_TOKEN_ENDPOINT_URL - For a AWS Cognito domain it should be
https://[your-chosen-domain].auth.us-east-1.amazoncognito.comAUTH_SERVER_MYBFF_CLIENT_ID - the client idAUTH_SERVER_MYBFF_CLIENT_SECRET - the client secretNote ⚠️: Do not store secrets in source control
Now let’s run again MyBFF, but this time increasing the log level to debug
to see what’s happening inside the access token management service.
dotnet run --launch-profile https --Logging:LogLevel:Default=DebugBuilding...
dbug: Microsoft.Extensions.Hosting.Internal.Host[1]
Hosting starting
info: Microsoft.Hosting.Lifetime[14]
Now listening on: https://localhost:7248
info: Microsoft.Hosting.Lifetime[14]
Now listening on: http://localhost:5006
info: Microsoft.Hosting.Lifetime[0]
Application started. Press Ctrl+C to shut down.
info: Microsoft.Hosting.Lifetime[0]
Hosting environment: Development
info: Microsoft.Hosting.Lifetime[0]
Content root path: C:\temp\MyBFF
dbug: Microsoft.Extensions.Hosting.Internal.Host[2]
Hosting startedNow let’s call MyBFF /wetherforecast endpoint
curl -X 'GET' 'https://localhost:7248/weatherforecast' -iHTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Mon, 12 Feb 2024 11:59:31 GMT
Server: Kestrel
Transfer-Encoding: chunked
[{"date":"2024-02-13","temperatureC":13,"summary":"Cool"},{"date":"2024-02-14","temperatureC":-16,"summary":"Cool"},{"date":"2024-02-15","temperatureC":34,"summary":"Hot"},{"date":"2024-02-16","temperatureC":28,"summary":"Cool"},{"date":"2024-02-17","temperatureC":14,"summary":"Hot"}]We got a 200 OK and weather forecast data was returned, which means that the REST call to MyService has succeeded. Let’s check the logs
dbug: Microsoft.Extensions.Hosting.Internal.Host[2]
Hosting started
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[100]
Start processing HTTP request GET https://localhost:7062/weatherforecast
dbug: IdentityModel.AspNetCore.AccessTokenManagement.ClientAccessTokenCache[0] Cache miss for access token for client: MyBFFdbug: IdentityModel.AspNetCore.AccessTokenManagement.TokenEndpointService[0] Requesting client access token for client: MyBFFdbug: IdentityModel.AspNetCore.AccessTokenManagement.DefaultTokenClientConfigurationService[0] Returning token client configuration for client: MyBFFinfo: System.Net.Http.HttpClient.IdentityModel.AspNetCore.AccessTokenManagement.TokenEndpointService.LogicalHandler[100]
Start processing HTTP request POST https://your-cognito-domain.auth.us-east-1.amazoncognito.com/oauth2/token
info: System.Net.Http.HttpClient.IdentityModel.AspNetCore.AccessTokenManagement.TokenEndpointService.ClientHandler[100]
Sending HTTP request POST https://your-cognito-domain.auth.us-east-1.amazoncognito.com/oauth2/token
info: System.Net.Http.HttpClient.IdentityModel.AspNetCore.AccessTokenManagement.TokenEndpointService.ClientHandler[101]
Received HTTP response headers after 549.6799ms - 200
info: System.Net.Http.HttpClient.IdentityModel.AspNetCore.AccessTokenManagement.TokenEndpointService.LogicalHandler[101]
End processing HTTP request after 556.8406ms - 200
dbug: IdentityModel.AspNetCore.AccessTokenManagement.ClientAccessTokenCache[0] Caching access token for client: MyBFF. Expiration: 02/12/2024 12:08:31 +00:00info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[100]
Sending HTTP request GET https://localhost:7062/weatherforecast
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[101]
Received HTTP response headers after 15.9003ms - 200
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[101]
End processing HTTP request after 601.1684ms - 200From the logs we can see that:
MyService has succeededNow let’s call it a 2nd time, hoping this time we get a cache hit and reuse the access token. Here are the logs we got this time:
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[101]
End processing HTTP request after 658.9196ms - 200
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[100]
Start processing HTTP request GET https://localhost:7062/weatherforecast
dbug: IdentityModel.AspNetCore.AccessTokenManagement.ClientAccessTokenCache[0] Cache hit for access token for client: MyBFFinfo: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[100]
Sending HTTP request GET https://localhost:7062/weatherforecast
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.ClientHandler[101]
Received HTTP response headers after 4.1808ms - 200
info: System.Net.Http.HttpClient.Refit.Implementation.Generated+IMyServiceClient, MyBFF, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null.LogicalHandler[101]
End processing HTTP request after 4.8902ms - 200Confirmed! We got a cache hit, and the access_token is being reused. When the token expires, we will get again a cache miss, and a new token will be requested.
In this blog post I am using ASP.NET Core as an example, but the principles used here can be used whatever the programming language you are using. I am also not assuming anything about the host where the services are running - it can be On-Premise, in an EC2 instance, in a AWS Lambda + API Gateway, etc. - it works in whatever host.
In this blog post I tried to show how simple and easy it is to authorize Service-to-Service calls by using OAuth2 with Client Credentials grant flow, using AWS Cognito as the Authorization Server, and ASP.NET Core APIs as both Resource Servers or Clients.