This post shows how to implement an Azure client credential flows to access an API for a service-to-service connection. No user is involved in this flow. A client certificate (Private Key JWT authentication) is used to get the access token and the token is used to access the API which is then used and validated in the API. Azure Key Vault is used to create and provide the client certificate.
Code: https://github.com/damienbod/AzureADAuthRazorUiServiceApiCertificate
Create a client certificate in Azure Key Vault
A self signed certificate with a key size of at least 2048 and key type RSA is used to validate the client requesting the access token. In your Azure Vault create a new certificate.
Download the .cer file which contains the public key. This will be uploaded to the Azure App Registration.
Setup the Azure App Registration for the Service API
A new Azure App Registration can be created for the Service API. This API will use a client certificate to request access tokens. The public key of the certificate needs to be added to the registration. In the Certificates & Secrets, upload the .cer file which was downloaded from the Key Vault.
No user is involved in the client credentials flow. In Azure, scopes cannot be used because consent is required to use scopes (Azure specific). Two roles are added to the access token for the application access and these roles can then be validated in the API. Open the Manifest and update the “appRoles” to include the required roles. The allowedMemberTypes should be Application.
Every time an access token is requested for the API, the roles will be added to the token. The “clientId/.default” scope is used to request the access token, ie no consent and all claims are added. The required claims can be added using the API permissions.
In the API permissions/Add a permission/My APIs select application and then the API Azure App Registration and add the roles which where created in the Manifest.
The Azure App Registration and the Key Vault are now ready so that client certificates can be used to request an access token which can be used to get data from the API.
Using the Azure Key Vault certificate
Microsoft.Identity.Web is used to implement the code along with Azure SDK to access the Key Vault.
Managed identities are used to access the Key Vault from the application. The Key Vault needs to be configured for the identities in the access policies. When running from the local dev environment in Visual Studio, the logged in user needs to have certificate access to the Key Vault. The deployed Azure App Service would also need this (if deploying to Azure App Services).
// Use Key Vault to get certificate var azureServiceTokenProvider = new AzureServiceTokenProvider(); // using managed identities var kv = new KeyVaultClient(new KeyVaultClient.AuthenticationCallback(azureServiceTokenProvider.KeyVaultTokenCallback)); // Get the certificate from Key Vault var identifier = _configuration["CallApi:ClientCertificates:0:KeyVaultCertificateName"]; var cert = await GetCertificateAsync(identifier, kv);
A X509Certificate2 can then be created from the Azure SDK CertificateVersionBundle returned from the GetCertificateAsync method.
private async Task<X509Certificate2> GetCertificateAsync(string identitifier, KeyVaultClient keyVaultClient) { var vaultBaseUrl = _configuration["CallApi:ClientCertificates:0:KeyVaultUrl"]; var certificateVersionBundle = await keyVaultClient.GetCertificateAsync(vaultBaseUrl, identitifier); var certificatePrivateKeySecretBundle = await keyVaultClient.GetSecretAsync(certificateVersionBundle.SecretIdentifier.Identifier); var privateKeyBytes = Convert.FromBase64String(certificatePrivateKeySecretBundle.Value); var certificateWithPrivateKey = new X509Certificate2(privateKeyBytes, (string)null, X509KeyStorageFlags.MachineKeySet); return certificateWithPrivateKey; }
Implement the API client using IConfidentialClientApplication and certificates
The IConfidentialClientApplication interface is used to setup the Azure client credentials flow. This is part of the Microsoft.Identity.Client namespace. The certificate from Key Vault is used to create the Access token request. The …/.default scope must be used for this flow in Azure. The AcquireTokenForClient is then used to send the request for the access token.
var scope = _configuration["CallApi:ScopeForAccessToken"]; var authority = $"{_configuration["CallApi:Instance"]}{_configuration["CallApi:TenantId"]}"; // client credentials flows, get access token IConfidentialClientApplication app = ConfidentialClientApplicationBuilder .Create(_configuration["CallApi:ClientId"]) .WithAuthority(new Uri(authority)) .WithCertificate(cert) .WithLogging(MyLoggingMethod, Microsoft.Identity.Client.LogLevel.Verbose, enablePiiLogging: true, enableDefaultPlatformLogging: true) .Build(); var accessToken = await app.AcquireTokenForClient(new[] { scope }).ExecuteAsync();
The access token returned from the AcquireTokenForClient method can then be used to access the API. This is added as a HTTP header.
client.BaseAddress = new Uri(_configuration["CallApi:ApiBaseAddress"]); client.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken.AccessToken); client.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); // use access token and get payload var response = await client.GetAsync("weatherforecast"); if (response.IsSuccessStatusCode) { var responseContent = await response.Content.ReadAsStringAsync(); var data = JArray.Parse(responseContent); return data; }
The app.settings contains the configuration for the Service API and the Azure App registration specifics. The ScopeForAccessToken uses the api://–clientid–/.default as this is required for Azure client credentials flow. The ClientCertificates contains the key vault settings as defined in the Microsoft.Identity.Web docs.
"CallApi": { "ScopeForAccessToken": "api://b178f3a5-7588-492a-924f-72d7887b7e48/.default", "ApiBaseAddress": "https://localhost:44390", "Instance": "https://login.microsoftonline.com/", "Domain": "damienbodhotmail.onmicrosoft.com", "TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1", "ClientId": "b178f3a5-7588-492a-924f-72d7887b7e48", "ClientCertificates": [ { "SourceType": "KeyVault", "KeyVaultUrl": "https://damienbod.vault.azure.net", "KeyVaultCertificateName": "ServiceApiCert" } ] },
Logging the client calls
A delegate method can be used to add your own specific logging of the IConfidentialClientApplication implementation. MyLoggingMethod implements this as shown in the docs.
void MyLoggingMethod(Microsoft.Identity.Client.LogLevel level, string message, bool containsPii) { _logger.LogInformation($"MSAL {level} {containsPii} {message}"); }
This can then be used by implementing the WithLogging method. In production deployments, the demo configurations should be changed.
// client credentials flows, get access token IConfidentialClientApplication app = ConfidentialClientApplicationBuilder .Create(_configuration["CallApi:ClientId"]) .WithAuthority(new Uri(authority)) .WithCertificate(cert) .WithLogging(MyLoggingMethod, Microsoft.Identity.Client.LogLevel.Verbose, enablePiiLogging: true, enableDefaultPlatformLogging: true) .Build();
Securing the API
The API now needs to enforce the security and validate the access token. This API can only be used by services and client certificate authentication is required. The AddMicrosoftIdentityWebApiAuthentication extension method adds the Microsoft.Identity.Web code configuration. This is configured to use and check the client certificate. The azpacr claim and the azp claim are validated in the AddAuthorization method. The azpacr value must be two, meaning a client certificate was used for authentication. The required roles are also validated using an authorization policy.
public void ConfigureServices(IServiceCollection services) { JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear(); IdentityModelEventSource.ShowPII = true; services.AddSingleton<IAuthorizationHandler, HasServiceApiRoleHandler>(); services.AddMicrosoftIdentityWebApiAuthentication(Configuration); services.AddAuthorization(options => { options.AddPolicy("ValidateAccessTokenPolicy", validateAccessTokenPolicy => { validateAccessTokenPolicy.Requirements.Add(new HasServiceApiRoleRequirement()); // Validate ClientId from token validateAccessTokenPolicy.RequireClaim("azp", Configuration["AzureAd:ClientId"]); // 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", "2"); }); }); services.AddControllers(); }
The configuration for the API contains the Azure App Registration specifics as well as the certificate details to get the certificate from the Key Vault.
"AzureAd": { "Instance": "https://login.microsoftonline.com/", "Domain": "damienbodhotmail.onmicrosoft.com", "TenantId": "7ff95b15-dc21-4ba6-bc92-824856578fc1", "ClientId": "b178f3a5-7588-492a-924f-72d7887b7e48", "ClientCertificates": [ { "SourceType": "KeyVault", "KeyVaultUrl": "https://damienbod.vault.azure.net", "KeyVaultCertificateName": "ServiceApiCert" } ] },
In the Controller for the API, the ValidateAccessTokenPolicy is applied.
[Authorize(Policy = "ValidateAccessTokenPolicy")] [ApiController] [Route("[controller]")] public class WeatherForecastController : ControllerBase
The HasServiceApiRoleHandler implements the HasServiceApiRoleRequirement requirement. This checks if the required role is present.
public class HasServiceApiRoleHandler : AuthorizationHandler<HasServiceApiRoleRequirement> { protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, HasServiceApiRoleRequirement requirement) { if (context == null) throw new ArgumentNullException(nameof(context)); if (requirement == null) throw new ArgumentNullException(nameof(requirement)); var roleClaims = context.User.Claims.Where(t => t.Type == "roles"); if (roleClaims != null && HasServiceApiRole(roleClaims)) { context.Succeed(requirement); } return Task.CompletedTask; } private bool HasServiceApiRole(IEnumerable<Claim> roleClaims) { foreach(var role in roleClaims) { if("service-api" == role.Value) { return true; } } return false; } }
Using a client certificate to identify an application client calling an API can be very useful. If you do not implement both the client and the API of a confidential client, using certificates instead of secrets can be very useful, as you do not have to share a secret. The client can provide a public key, and the server can validate this. If you control both the client and the API, then both APIs could use the same secret from the same Key Vault. Private Key JWT authentication for other flow types and other API types such as access_as_user or OBO flows is also supported using Microsoft.Identity.Web.
Links
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-credential-flows
https://tools.ietf.org/html/rfc7523
https://openid.net/specs/openid-connect-core-1_0.html#ClientAuthentication
https://docs.microsoft.com/en-us/azure/active-directory/develop/access-tokens
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki/Client-Assertions
https://docs.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-client-creds-grant-flow
https://www.scottbrady91.com/OAuth/Removing-Shared-Secrets-for-OAuth-Client-Authentication
https://github.com/KevinDockx/ApiSecurityInDepth
https://github.com/AzureAD/microsoft-authentication-library-for-dotnet/wiki