This article shows how to implement the OAuth client credentials flow using the Microsoft.Identity.Client Nuget package and Azure AD to create an Azure App registration. The client application requires a secret which can be an Azure App registration or a certificate to request an access token. The token and only tokens created for this client can be used to access the API.
Code: Azure Client credentials flows
Blogs in this series
- Implementing OAuth2 APP to APP security using Azure AD from a Web APP
- APP to APP security using Azure AD from a daemon app
Azure App registration setup
The Azure App registration is setup as in this blog:
Implementing OAuth2 APP to APP security using Azure AD from a Web APP
An Azure App registration was then created to request new access tokens. The access_as_application claim is validated in the API.
API
The service API is implemented to validate the access tokens. The azp claim is used to validate that the token was requested using the known client ID and the secret. It does not validate who sent the access token, just that this client ID and secret was used to request the access token. The client credentials flow should only be used by trusted clients. The azpacr is used to validate how the token was requested. This is a confidential client. Microsoft.Identity.Web is used to implement the API security.
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
IdentityModelEventSource.ShowPII = true;
JwtSecurityTokenHandler.DefaultMapInboundClaims = false;
services.AddSingleton<IAuthorizationHandler, HasServiceApiRoleHandler>();
services.AddMicrosoftIdentityWebApiAuthentication(Configuration);
services.AddControllers();
services.AddAuthorization(options =>
{
options.AddPolicy("ValidateAccessTokenPolicy", validateAccessTokenPolicy =>
{
validateAccessTokenPolicy.Requirements.Add(new HasServiceApiRoleRequirement());
// Validate id of application for which the token was created
// In this case the CC client application
validateAccessTokenPolicy.RequireClaim("azp", "b178f3a5-7588-492a-924f-72d7887b7e48");
// only allow tokens which used "Private key JWT Client authentication"
// // https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens
// Indicates how the client was authenticated. For a public client, the value is "0".
// If client ID and client secret are used, the value is "1".
// If a client certificate was used for authentication, the value is "2".
validateAccessTokenPolicy.RequireClaim("azpacr", "1");
});
});
Microsoft.Identity.Client OAuth Client credentials client
A console application is used to implement the client credentials trusted application. A console application cannot be trusted unless it is deployed to a trusted host. It would be better to use a certificate to secure this and even better if this was not a console application but instead some server deployed Azure service which uses Key Vault to persist it’s secrets and managed identities to access the secret. The ConfidentialClientApplicationBuilder is used to create a new CC flow. The access token is used to access the API.
using System.Net.Http.Headers;
using Microsoft.Extensions.Configuration;
using Microsoft.Identity.Client;
var builder = new ConfigurationBuilder()
.SetBasePath(Directory.GetCurrentDirectory())
.AddUserSecrets("78cf2604-554c-4a6e-8846-3505f2c0697d")
.AddJsonFile("appsettings.json");
var configuration = builder.Build();
// 1. Client client credentials client
var app = ConfidentialClientApplicationBuilder.Create(configuration["AzureADServiceApi:ClientId"])
.WithClientSecret(configuration["AzureADServiceApi:ClientSecret"])
.WithAuthority(configuration["AzureADServiceApi:Authority"])
.Build();
var scopes = new[] { configuration["AzureADServiceApi:Scope"] };
// 2. Get access token
var authResult = await app.AcquireTokenForClient(scopes)
.ExecuteAsync();
if(authResult == null)
{
Console.WriteLine("no auth result... ");
}
else
{
Console.WriteLine(authResult.AccessToken);
// 3. Use access token to access token
var client = new HttpClient
{
BaseAddress = new Uri(configuration["AzureADServiceApi:ApiBaseAddress"])
};
client.DefaultRequestHeaders.Authorization
= new AuthenticationHeaderValue("Bearer", authResult.AccessToken);
client.DefaultRequestHeaders.Accept
.Add(new MediaTypeWithQualityHeaderValue("application/json"));
var response = await client.GetAsync("ApiForServiceData");
if (response.IsSuccessStatusCode)
{
Console.WriteLine(await response.Content.ReadAsStringAsync());
}
}
The console application uses app settings to load the trusted client. The Scope is defined to use the application ID from the Azure App registration and the .default scope. Then any API definitions will be added to the access token. It is important to used V2 access tokens which can be defined in the manifest.
{
"AzureADServiceApi": {
"ClientId": "b178f3a5-7588-492a-924f-72d7887b7e48",
// "ClientSecret": "--in-user-secrets--",
// Authority Guid = tenanant ID
"Authority": "https://login.microsoftonline.com/7ff95b15-dc21-4ba6-bc92-824856578fc1",
"ApiBaseAddress": "https://localhost:44324",
"Scope": "api://b178f3a5-7588-492a-924f-72d7887b7e48/.default"
}
}
The access tokens returned contains the claims with the azp, roles and azpacr claims. These claims as well as the standard claims values are used to authorize each request. Authorize attributes are used with a match scheme and authorization policy which uses the claims to validate. The Azure definitions are used in the policies and the policies are used in the application. You should not use the Azure definitions directly in the application. Avoid using Roles or RequiredScope directly in controllers or specific application parts. Map these in the IClaimsTransformation or the OnTokenValidated method.
"iss": "https://login.microsoftonline.com/7ff95b15-dc21-4ba6-bc92-824856578fc1/v2.0",
"iat": 1648363449,
"nbf": 1648363449,
"exp": 1648367349,
"aio": "E2ZgYLh1abHAkpeHvuz/fjX9QMNZDgA=",
"azp": "b178f3a5-7588-492a-924f-72d7887b7e48",
"azpacr": "1",
"oid": "3952ce95-8b14-47b4-b3e6-2a5521d35ed1",
"rh": "0.AR8AFVv5fyHcpku8koJIVlePwaXzeLGIdSpJkk9y14h7fkgfAAA.",
"roles": [
"access_as_application",
"service-api"
],
"sub": "3952ce95-8b14-47b4-b3e6-2a5521d35ed1",
"tid": "7ff95b15-dc21-4ba6-bc92-824856578fc1",
"uti": "WDee3wGpJkeJGUMN5CDOAA",
"ver": "2.0"
}
Implementing the application permissions in this way makes is possible to secure any daemon application or application flow with no user with a server for any deployment. The client can be hosted in an ASP.NET Core application which authenticates using Azure B2C or a service with no user interaction. The client and the API work alone. It is important that the client can be trusted to secure the secret used to request the access tokens.
Links:
https://github.com/AzureAD/microsoft-identity-web
https://docs.microsoft.com/en-us/azure/active-directory/develop/
https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2
https://github.com/Azure-Samples/active-directory-dotnetcore-daemon-v2/tree/master/4-Call-OwnApi-Pop