Authenticating to Google Cloud from Azure App Services
Using workload identity federation, we can let Azure-hosted applications authenticate to Google Cloud using their managed identity, no service account keys required.
More specifically, we can configure the Google Cloud client libraries so that they don’t look for a service account key, but instead obtain an Azure access token and exchange it against a Google Cloud access token.
Setting up workload identity federation with Azure typically involves 4 steps:
- Creating an app registration in Entra ID and assigning it an Application URI.
- Setting up a workload identity pool and provider.
- Creating a credential configuration file that captures all necessary parameters for the client library to perform a token exchange.
- Configuring the Azure-hosted application to use the credential configuration file.
However, for Azure App Services, we have to deviate from this process a little.
Azure VMs vs App Services
Like Azure VMs, Azure App Services support managed identities, but the process how applications obtain tokens for their managed identity differs between the two environments:
On a VM, we can obtain an access token with a single
GET
request to the instance metadata service (IMDS).We do have to add an extra
Metadata: true
header when making a request to the IMDS, but both this header and the URL are static.Azure App Services does not use a static URL. Instead, we have to assemble the right URL based on information found in the
IDENTITY_ENDPOINT
andIDENTITY_HEADER
environment variables.
The Google Cloud client libraries know how to fetch an access token from a static URL, but they don’t know how to assemble URLs dynamically.
So we need to write some extra code to make workload identity federation work on Azure App Services.
The following code snippet shows how that can look like: The CreateWorkloadIdentityCredential
method
reads the IDENTITY_ENDPOINT
and IDENTITY_HEADER
environment variables and
dynamically creates the equivalent of a configuration file, which it then uses to initialize a GoogleCredential
.
static GoogleCredential CreateWorkloadIdentityCredential()
{
//
// Read endpoint and XSRF header from the App Services environment.
// Cf. https://learn.microsoft.com/en-us/azure/app-service/overview-managed-identity
//
var identityHeader = Environment.GetEnvironmentVariable("IDENTITY_HEADER");
var identityEndpoint = Environment.GetEnvironmentVariable("IDENTITY_ENDPOINT");
if (string.IsNullOrEmpty(identityHeader) || string.IsNullOrEmpty(identityEndpoint))
{
throw new InvalidOperationException(
"The environment variables IDENTITY_HEADER and IDENTITY_ENDPOINT " +
"have not been initialized. This indicates that the application is " +
"not running on Azure App Services or that you haven't assigned " +
"a managed identity to the application yet");
}
//
// Read workload identity configuration from environment.
//
var audience =
Environment.GetEnvironmentVariable("GOOGLE_WORKLOADIDENTITY_AUDIENCE");
if (string.IsNullOrEmpty(audience) || !audience.StartsWith("//"))
{
throw new InvalidOperationException(
"The environment variable GOOGLE_WORKLOADIDENTITY_AUDIENCE " +
"has not been initialized. The variable must contain a valid " +
"workload identity pool provider URL (without https: prefix)");
}
var appId = Environment.GetEnvironmentVariable("GOOGLE_WORKLOADIDENTITY_APPID");
if (string.IsNullOrEmpty(appId))
{
throw new InvalidOperationException(
"The environment variable GOOGLE_WORKLOADIDENTITY_APPID " +
"has not been initialized. The variable must contain the App ID URI " +
"of the Entra ID application registration trusted by your workload " +
"identity pool provider");
}
var serviceAccountToImpersonate =
Environment.GetEnvironmentVariable("GOOGLE_WORKLOADIDENTITY_SERVICEACCOUNT");
var identityEndpointRequestUrl = new UriBuilder(identityEndpoint)
{
Query = new QueryString()
.Add("resource", appId)
.Add("api-version", "2019-08-01")
.ToString()
}.Uri;
//
// Create the equivalent of a credential configuration file that
// incorporates the identity endpoint request URL and header.
//
var credentialConfiguration = new JsonCredentialParameters
{
Type = "external_account",
Audience = audience,
SubjectTokenType = "urn:ietf:params:oauth:token-type:jwt",
ServiceAccountImpersonationUrl = serviceAccountToImpersonate == null
? null // No impersonation
: "https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/" +
$"{serviceAccountToImpersonate}:generateAccessToken",
TokenUrl = "https://sts.googleapis.com/v1/token",
CredentialSourceConfig = new JsonCredentialParameters.CredentialSource
{
Url = identityEndpointRequestUrl.AbsoluteUri,
Headers = new Dictionary<string, string>
{
{ "X-IDENTITY-HEADER", identityHeader }
},
Format = new JsonCredentialParameters.CredentialSource.SubjectTokenFormat
{
SubjectTokenFieldName = "access_token",
Type = "json"
}
}
};
//
// Create a workload identity federation credential that automatically
// fetches and refreshes access tokens as necessary.
//
return GoogleCredential
.FromJsonParameters(credentialConfiguration)
.CreateScoped("https://www.googleapis.com/auth/cloud-platform");
}
The method essentially replaces GoogleCredential.GetApplicationDefault()
, which we’d use normally.
Revised setup process
Equipped with the method above, the process to set up workload identity federation for Azure App Services now looks like this:
- Creating an app registration in Entra ID and assigning it an Application URI.
- Setting up a workload identity pool and provider.
Creating a credential configuration file that captures all necessary parameters for the client library to authenticate using workload identity.Changing the code to use
CreateWorkloadIdentityCredential()
.Configuring the Azure-hosted application to use the credential configuration file.Configuring an extra set of environment variables for the Azure App Services app that capture the necessary parameters for using workload identity:
You can find a complete, working example of an Azure App Services app that uses workload identity federation, in the jpassing/workloadidentity-examples repository.