Subsections of Consume an API
Machine-to-Machine (M2M) authentication
There are many scenarios where applications, such as CLIs, or Backend services, need to call other APIs and when these APIs requires an access token the application must be able to authenticate itself (if the call isn’t done on behalf of a user). The Client Credentials Flow (defined in OAuth 2.0) allows an application to exchange its credentials, commonly a Client ID and Client Secret for an access token.
Exchange Client Credentials for an Access Token
To exchange the client credentials for an access token the Token endpoint is used.
POST /connect/token HTTP/1.1
Host: YOURINSTANCE.irmciam.se
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=THE_CLIENT_ID
&client_secret=THE_CLIENT_SECRET
&scope=THE_SCOPES_REQUIRED_TO_CALL_THE_API
C# Example
In this example code IdentityModel is used to simplify the code.
var client = new HttpClient();
var tokenRequest = new ClientCredentialsTokenRequest
{
Address = "https://YOURINSTANCE.irmciam.se/connect/token",
ClientId = "THE_CLIENT_ID",
ClientSecret = "THE_CLIENT_SECRET",
Scope = "THE_SCOPES_REQUIRED_TO_CALL_THE_API"
};
var response = await client.RequestClientCredentialsTokenAsync(tokenRequest);
if (response.IsError) throw new Exception(response.Error);
var token = response.AccessToken;
Use the Access Token
After retrieving the access token it should be added in the Authrozation HTTP header on all calls to the API:
Authorization: Bearer THE_ACCESS_TOKEN
The token is typically valid for a while (in many scenarios 1 hour), so re-use the token in multiple calls instead of retrieving a new for each call. This is extra important when many calls are done, since this will otherwise cause unnecessary load on Authway.
Call an API
It is common for a system/API to need to call another API within the same organization. These APIs are often protected in the same way, requiring a Bearer token for the request to be allowed. A Bearer token is sent in the HTTP header Authorization: Authorization: Bearer [ACCESS TOKEN]
.
The first decision to make is what the Access Token you’re sending should represent: a user or a system. When sending a token on behalf of a user, it’s referred to as Delegation, while when the system makes the call, it follows the Trusted Subsystem pattern.
Delegation: Making a call on behalf of a user
Delegation can be handled in two different ways. One common approach is what is sometimes called “poor man’s delegation,” which involves simply sending the user’s token directly to the underlying API. The other option is for the system making the call to perform a “token exchange” (OAuth Token Exchange specification, RFC 8693).
Regardless of the chosen model, it is necessary for your web application/API to access the user’s token. To enable this, .NET needs to store the token, which is done with the following configuration code in Startup.cs:
.AddOpenIdConnect(options =>
{
...
options.SaveTokens = true;
...
});
Assuming this configuration is in place, it is possible to retrieve the user’s access token as follows:
httpContext.GetTokenAsync("access_token");
Poor mans delegation
The biggest advantage of this variant is that it is very easy to use. The only requirement is that all the API scopes that need to be used are included when the user’s access token is created. The downside of this solution is that if this access token falls into the wrong hands, it provides access to a larger attack surface than if it only gives access to a single API scope.
Typically, for this solution, you want to add the user’s access token to the requests made via HttpClient. A good way to handle this is to create a DelegatingHandler whose task is to do this, and configure HttpClient to use it. An implementation could look like this (available in IRM.Extensions.Http):
/// <summary>
/// A <see cref="HttpClient"/> handler that that adds a bearer token authentication header to the http request.
/// </summary>
public class BearerTokenAuthenticationHandler : DelegatingHandler
{
private readonly IResolveBearerToken _resolveBearerToken;
/// <summary>
/// Creates a new instance of <see cref="BearerTokenAuthenticationHandler" />.
/// </summary>
/// <param name="resolveBearerToken">The <see cref="IResolveBearerToken"/> used to get a bearer token.</param>
public BearerTokenAuthenticationHandler(IResolveBearerToken resolveBearerToken)
{
_resolveBearerToken = resolveBearerToken ?? throw new ArgumentNullException(nameof(resolveBearerToken));
}
/// <inheritdoc />
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", await _resolveBearerToken.GetTokenAsync(request, cancellationToken).ConfigureAwait(false));
return await base.SendAsync(request, cancellationToken).ConfigureAwait(false);
}
}
The code above provides general support for adding a Bearer token and delegates the logic to retrieve the token itself to a class that implements IResolveBearerToken (also available in IRM.Extension.Http). For the “poor man’s delegation,” the implementation could look like this:
/// <summary>
/// Gets an access token from the current <see cref="HttpContext"/>.
/// </summary>
public class HttpContextBearerTokenResolver : IResolveBearerToken
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly ILogger<HttpContextBearerTokenResolver> _logger;
/// <summary>
/// Creates a new instance of <see cref="HttpContextBearerTokenResolver" />.
/// </summary>
/// <param name="httpContextAccessor">The <see cref="IHttpContextAccessor"/> used to get the <see cref="HttpContext"/> where the access token is retrieved from.</param>
/// <param name="logger"></param>
public HttpContextBearerTokenResolver(IHttpContextAccessor httpContextAccessor, ILogger<HttpContextBearerTokenResolver> logger)
{
_httpContextAccessor = httpContextAccessor ?? throw new ArgumentNullException(nameof(httpContextAccessor));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
}
/// <inheritdoc />
public Task<string> GetTokenAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var httpContext = _httpContextAccessor.HttpContext ?? throw new InvalidOperationException("There is no HttpContext available which is required for HttpContextBearerTokenResolver.");
_logger.LogInformation("Resolves bearer token by getting the access token from Httpcontext.");
return httpContext.GetTokenAsync("access_token");
}
}
Token Exchange
The current version of Authway does not have complete support for Token Exchange, but it can be easily added. In conjunction with that, certain implementation details can be discussed based on the desired objectives.
Trusted Subssytem: System-to-system
To use Client Credentials, you need to configure either a regular client (Configure an Application)(../configure-authway/configure-an-application) or an external system (Configure an External System)(../configure-authway/configure-an-external-system).
OpenID Connect provides the Client Credentials flow, designed for situations where one system needs to call another system. Typically, you require a client ID and a client secret to obtain an access token using these credentials. This access token typically contains protocol-specific claims. Since it doesn’t represent a user, common claims like sub, email, name, or, in Authway’s case, perm or role, are not present. When implementing an API, significant work is required to handle this, as it differs significantly from when a user calls the API. For example, regular authorization controls don’t work, and if you want to log who made a change and save the name/ID of the person performing the change, that information is not available either. Authway supports something called External Systems to address this challenge.
External Systems are clients with support for Client Credentials. In the background, an associated user is created with the same ID (sub) as the client ID. This enables treating the calling system similarly to a user.
Example to retrieve an access token according to Client Credentials
In the same way as for a user above, we use the BearerTokenAuthenticationHandler, but with a different implementation to extract the access token. This code has dependencies on the IdentityModel NuGet package.
/// <summary>
/// Gets a client credentials access token from an identity server.
/// </summary>
public class ClientCredentialBearerTokenResolver : IResolveBearerToken
{
private readonly ClientCredentialBearerTokenResolverOptions _options;
private readonly HttpClient _httpClient;
private IMemoryCache _cache;
private readonly ILogger<ClientCredentialBearerTokenResolver> _logger;
private const string _cacheKey = "IRM.Extensions.Http.ClientCredentialBearerTokenResolver.Token";
/// <summary>
/// Creates a new instance of <see cref="ClientCredentialBearerTokenResolver" />.
/// </summary>
/// <param name="httpClient"></param>
/// <param name="optionsAccessor"></param>
/// <param name="logger"></param>
/// <param name="cache"></param>
public ClientCredentialBearerTokenResolver(HttpClient httpClient, IOptions<ClientCredentialBearerTokenResolverOptions> optionsAccessor, ILogger<ClientCredentialBearerTokenResolver> logger, IMemoryCache cache = null)
{
_options = optionsAccessor?.Value ?? throw new ArgumentNullException(nameof(optionsAccessor));
_httpClient = httpClient ?? throw new ArgumentNullException(nameof(httpClient));
_logger = logger ?? throw new ArgumentNullException(nameof(logger));
if (_options.CacheAccessToken && cache == null)
throw new ArgumentNullException(nameof(cache));
_cache = cache;
}
/// <inheritdoc />
public async Task<string> GetTokenAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
if (_options.CacheAccessToken && _cache != null)
{
var cacheKey = $"{_cacheKey}:{_options.ClientId}";
var token = await _cache.GetOrCreateAsync(cacheKey, async entry =>
{
var response = await GetTokenFromServerAsync(request, cancellationToken).ConfigureAwait(false);
entry.AbsoluteExpiration = DateTimeOffset.Now.AddSeconds(response.ExpiresIn).AddSeconds(-20);
return response.AccessToken;
}).ConfigureAwait(false);
return token;
}
else
{
var response = await GetTokenFromServerAsync(request, cancellationToken).ConfigureAwait(false);
return response.AccessToken;
}
}
private async Task<TokenResponse> GetTokenFromServerAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var tokenUrl = await GetTokenUrl(_options.Authority).ConfigureAwait(false);
var tokenRequest = new ClientCredentialsTokenRequest
{
Address = tokenUrl,
ClientId = _options.ClientId,
ClientSecret = _options.ClientSecret,
Scope = _options.Scope
};
_logger.LogInformation("Tries to resolve bearer token by getting the access token from {tokenUrl} for client {clientId}.", tokenUrl, _options.ClientId);
var response = await _httpClient.RequestClientCredentialsTokenAsync(tokenRequest, cancellationToken).ConfigureAwait(false);
if (!response.IsError)
return response;
if (response.ErrorType == ResponseErrorType.Exception)
ExceptionDispatchInfo.Capture(response.Exception).Throw();
string error;
if (response.ErrorType == ResponseErrorType.Http)
error = response.HttpErrorReason;
else
error = $"Error: {response.Error}\nDescription: {response.ErrorDescription}";
_logger.LogError(error);
throw new Exception(error);
}
private async Task<string> GetTokenUrl(string authority)
{
var discovery = new DiscoveryCache(authority, () => _httpClient);
var disco = await discovery.GetAsync().ConfigureAwait(false);
if (disco.IsError)
{
if (disco.Exception != null)
ExceptionDispatchInfo.Capture(disco.Exception).Throw();
throw new InvalidOperationException($"Failed to get token url. Details: {disco.Error ?? disco.HttpErrorReason}.");
}
return disco.TokenEndpoint;
}
}
External system
In a basic configuration of Authway, it is still the case that an access token in the client credentials flow does not contain any user claims such as sub, name, and perm because that’s how the standard is supposed to work. There is an option to extend Authway with support that ensures these claims are included even in client credentials, but this must be done by IRM.
Instead, we recommend that the API retrieves this additional information from the Auth module when calls to the API arrive, for example, in a ClaimsTransformation class. The advantage of this approach is that the API itself can control how fresh (updated) claims it wants by managing caching, compared to when all claims are packed in the access token with the same lifespan as the token. Additionally, the token becomes smaller, and there are other advantages that you can read about in Privacy and GDPR. The same documentation also includes code examples of how to retrieve more information."