- Published on
On-behalf-of auth and token store in Azure Container Apps with Easy Auth
- Authors
- Name
- David Rosevear
TL;DR: On-behalf-of auth is great because it allows our app to make calls to services on the user's behalf. In Azure Container Apps with Easy Auth, getting the necessary X-MS-TOKEN-AAD-ACCESS-TOKEN
header requires just a few tweaks to authConfigs
. With this we can create an OnBehalfOfCredential
.
Some time ago, John Reilly wrote about getting .NET authentication to work nicely with Easy Auth. He mentioned the lack of token support at the time, "If there was a populated X-MS-TOKEN-AAD-ACCESS-TOKEN
then it would unlock all manner of possibilities. Let's say I want to make use of the Graph API on behalf of my logged in user. I cannot."
Using more recent API versions though, we can set authConfigs
in such a way to give us a token store and grant us the X-MS-TOKEN-AAD-ACCESS-TOKEN
header in each request! The token store is supported from API version 2023-05-02-preview
although if you want to use a managed identity to authenticate with the token store (which is not mentioned in the docs at the time of writing) you'll need to use version 2024-10-02-preview
or later.
The bits you need to make it work (a minimal example)
In addition to your existing config inside the containerApps/authConfigs
bicep template, we need the login
and validation
objects set inside the identity provider (azureActiveDirectory
in this example, but similar config could work with certain other identity providers):
azureActiveDirectory: {
enabled: true
registration: {
openIdIssuer: '${environment().authentication.loginEndpoint}${subscription().tenantId}/v2.0'
clientId: 'your-ad-app-client-id'
clientSecretSettingName: 'your-client-secret-name' // this secret reference must be set in `configuration/secrets` (https://learn.microsoft.com/en-us/azure/container-apps/manage-secrets?tabs=arm-template)
}
login: {
loginParameters: [ 'scope=api://the-ad-app-uri-of-a-downstream-service-you-want-to-auth-with/.default openid profile' ]
}
validation: {
allowedAudiences: [
'api://your-ad-app-id-uri'
]
defaultAuthorizationPolicy: {
allowedApplications: []
}
}
}
Note that if you ommit clientSecretSettingName
, you'll get the ID token but none of the access tokens.
Then, importantly, we need the login
object at the root – this is where we will configure the token store:
login: {
// https://learn.microsoft.com/en-us/azure/container-apps/token-store
tokenStore: {
enabled: true
azureBlobStorage: {
blobContainerUri: 'https://${your-storage-account-name}.blob.${environment().suffixes.storage}/name-of-the-your-blob-container-for-the-token-store'
managedIdentityResourceId: 'managed-identity-id-of-your-container-app'
}
}
}
You'll notice above that your token store lives inside Blob Storage. And so you will need to bring your own Storage Account with a Blob Services container that'll be used as the token store. Be sure to grant permissions to your Container App over the Blob Storage container accordingly.
Alternatively, if you'd like to use the Azure CLI (which doesn't seem to support the use of a managed identity yet):
az containerapp auth update \
--resource-group <RESOURCE_GROUP_NAME> \
--name <CONTAINER_APP_NAME> \
--sas-url-secret-name <SAS_SECRET_NAME> \
--token-store true
Now, in addition to X-MS-CLIENT-PRINCIPAL
, we get the headers:
X-MS-TOKEN-AAD-ACCESS-TOKEN
X-MS-TOKEN-AAD-EXPIRES-ON
X-MS-TOKEN-AAD-REFRESH-TOKEN
X-MS-TOKEN-AAD-ID-TOKEN
This is great because it allows our app to create on-behalf-of credentials to represent the user to other services that are called from our app. Say we want to call Graph on behalf of the user (from the docs):
var scopes = new[] { "https://graph.microsoft.com/.default" };
// Multi-tenant apps can use "common",
// single-tenant apps must use the tenant ID from the Azure portal
var tenantId = "common";
// Values from app registration
var clientId = "YOUR_CLIENT_ID";
var clientSecret = "YOUR_CLIENT_SECRET";
// using Azure.Identity;
var options = new OnBehalfOfCredentialOption
{
AuthorityHost = AzureAuthorityHosts.AzurePublicCloud,
};
// This is the incoming token to exchange using on-behalf-of flow
var oboToken = "JWT_TOKEN_TO_EXCHANGE";
var onBehalfOfCredential = new OnBehalfOfCredential(
tenantId, clientId, clientSecret, oboToken, options);
var graphClient = new GraphServiceClient(onBehalfOfCredential, scopes);
And there you have it 😊