Published on

On-behalf-of auth and token store in Azure Container Apps with Easy Auth

Authors
  • avatar
    Name
    David Rosevear
    Twitter

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 😊