Part 3. Token exchange from GCP to Azure

Λ\: Clément Bosc - Feb 14 '23 - - Dev Community

In the previous article of this multi-cloud identity federation series, we saw how to securely exchange an Azure access token (on the form of a JWT token) with a GCP access token, to access private GCP resource from Azure. You are probably wondering how to do the reverse operation, you are in the right spot !

The big picture

To request Azure APIs from GCP environment, we will need the same two objects as before : a GCP service account and an Azure App Registration. The process is straightforward, because there is no Workload Identity Federation-like product on Azure, everything happens in the App registration configuration :

  • Generate a GCP ID token for the source service account, either via the Metadata Server (recommended way for production applications), or via the CLI or IAM REST API (need to have impersonate permissions on the SA)
  • Ask the Azure OAuth2 Authorization server to exchange the token for an Azure access token representing the target App registration.
  • Enjoy your APIs requests 🙂

Token exchange between Google Cloud and Azure

1. Generate a GCP ID token

First you need to generate your ID token on behalf of the source service account. Why ID token and not access token ? Because access token on GCP are opaque and are not decodable ! We need a JWT token here : Azure need to be able to check the content of the token to map the App registration on the other side and check if the issuer is Google.

For this step you can either use the Metadata Server if your workload is running in GCP Compute context (recommended way for production applications), or use gcloud CLI (or any other method available). The result will be a valid identity token. Note that you need to match the audience with the audience you will configure in the next step. The recommended value (according to Azure) is api://AzureADTokenExchange.

  1. Using the magic Metadata Server URL :
curl -H "Metadata-Flavor: Google" \
  'http://metadata/computeMetadata/v1/instance/service-accounts/default/identity?audience=api://AzureADTokenExchange'
Enter fullscreen mode Exit fullscreen mode

b. Using the gcloud CLI (for testing purpose only)

gcloud auth print-identity-token \
    --impersonate-service-account=SOURCE_SERVICE_ACCOUNT_EMAIL \
    --audiences=api://AzureADTokenExchange
Enter fullscreen mode Exit fullscreen mode

2. Create a federated credentials in your Azure App Registration

Secondly, configure your target Azure App Registration to allow impersonation from the source GCP service account. This operation happens in the Federated Credential section of your App registration in Azure Active Directory. You will need to specify the trusted issuer, the subject and the audience.

How to get these values ? By decoding the GCP identity token, of course ! Just as usual, you can go to https://jwt.io/ and inspect the content of your token’s payload :

{
  "aud": "api://AzureADTokenExchange",
  "azp": "106697322240068434726",
  "exp": 1676301170,
  "iat": 1676297570,
  "iss": "https://accounts.google.com",
  "sub": "106697322240068434726"
}
Enter fullscreen mode Exit fullscreen mode
  • iss = https://accounts.google.com is the issuer of the token (Google)
  • sub = subject is the source service account ID. You can also find this info in the GCP console, on the service account page (Unique ID)
  • aud is the default audience value, defined in the first step.

From these informations you can create your federated-credentials settings :

az ad app federated-credential create --id APPLICATION_ID --parameters credential.json
("credential.json" contains the following content)
{
    "name": "GcpFederation",
    "issuer": "https://accounts.google.com",
    "subject": "106697322240068434726",
    "description": "Test GCP federation",
    "audiences": [
        "api://AzureADTokenExchange"
    ]
}
Enter fullscreen mode Exit fullscreen mode

3. Exchange your GCP ID token for an Azure Access token

Last but not least, you need to exchange your GCP ID token for an Azure Access token to do whatever you want on Azure side : you need to make a request to the Azure Oauth2 Authorization Server by specifying the following parameters :

  • client_id to your App registration ID,
  • scope to the desired scope depending on the future usage of your token,
  • client_assertion_type fixed to urn:ietf:params:oauth:client-assertion-type:jwt-bearer for this operation
  • grant_type to client_credentials
  • And the most important: your GCP ID token under client_assertion
curl GET 'https://login.partner.microsoftonline.cn/TENANT_ID/oauth2/v2.0/token' \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data-urlencode 'client_id=APP_ID' \
--data-urlencode 'scope=https://storage.azure.com/.default' \ # or whatever other scope you might want
--data-urlencode 'client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer' \
--data-urlencode 'client_assertion=GCP_ID_TOKEN' \
--data-urlencode 'grant_type=client_credentials'

# Reponse

{
    "token_type": "Bearer",
    "expires_in": 3599,
    "ext_expires_in": 3599,
    "access_token": "eyJ0eXAiO********" # JWT token
}
Enter fullscreen mode Exit fullscreen mode

After decoding the GCP token, if the audience, issuer and subject match, your are good to go for a brand new Azure access token ! You can now access the APIs that match the scope you specified (here the Azure Storage API) :

curl GET 'https://STORAGE_ACCOUNT_NAME.blob.core.windows.net/CONTAINER' \
--header 'x-ms-version: 2020-04-08' \
--header 'Authorization: Bearer AZURE_TOKEN'

# Response 200 OK
Enter fullscreen mode Exit fullscreen mode

No need to store client_id and client_secret in GCP and risk a security breach ! Just use Azure Active Directory federated credentials !

In the next 2 articles we will see concret implementation in Python and Terraform for your production applications.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .