Digital transformation is resulting in the deployment of more software workloads. Businesses are building or rewriting their software workloads using cloud-native architectures. Multi-cloud is becoming the new normal, as developers cherry-pick the cloud resources that best suit their needs. When applications or services run outside Azure and need access to Azure resources, they need secrets to authenticate to Azure AD. These secrets pose a security risk. Securely storing these secrets and regularly rotating them is an unnecessary developer burden. Azure AD workload identity federation removes the need for these secrets in selected scenarios. Developers can configure Azure AD workload identities to trust tokens issued by another identity provider. This blog post explores how you can access Azure resources from software workloads running in Amazon Web Services (AWS).

AWS AAD federation

In an earlier blog post, we discussed accessing AWS resources from software running in Azure. This blog post discusses accessing Azure resources from software running in AWS.

Using Azure AD workload identity federation with AWS

Azure AD workload identity federation is a capability on workload identities such as Azure AD applications and managed identities. Earlier blog posts on this site provide details for using this capability with Kubernetes, SPIFFE, GitHub, and Google Cloud Platform.

Using this pattern with AWS is not straightforward. The identity model in AWS is different from other platforms such as GCP, Kubernetes, and GitHub. While AWS IAM provides an elegant model for accessing resources within the AWS cloud, it’s not easy to use it to access resources in other cloud providers. The step-by-step guide in the first part of this blog post shows how you can use the capabilities of Amazon Cognito to federate with Azure AD. For a detailed understanding of how this works end-to-end, see the advanced topics later in this blog post.

There are three parts to using Azure AD workload identity federation from your service in AWS.

  1. An identity in AWS Cloud to which Amazon Cognito will issue a token.
  2. Configure Azure AD application or managed identity to trust that AWS identity
  3. Get a token for the Amazon Cognito identity and exchange it for a token for an Azure AD workload identity.

Let’s look at each of these three parts in detail.

Part 1: Create an identity in Amazon Cognito

The Amazon Cognito Identity pool is the most suited for our needs. It is necessary, for security purposes, to create a dedicated identity pool to federate with Azure AD since Cognito only issues tokens with the identity pool as the audience. See the advanced topics in the latter part of this blog post for more details on why this matters.

Create an identity pool to federate with Azure AD

In this step, we are going to create a Cognito identity pool. And set it up so that we can use identities from that pool for our software workloads. Head to the AWS console and select the Cognito service. Go to Manage Identity Pools and pick Create new identity pool.

In the Create Identity Pool wizard, provide a name for your identity pool. Then pick Authentication providers. Select Custom and provide a name of your choice in the Developer provider name. This custom provider option tells Cognito that authorized workloads using this developer provider name can get tokens for identities in this pool.

Cognito identity pool creation

Once you create the pool, Cognito prompts you to create IAM roles. This is only necessary when you use Cognito to access AWS resources. Since this pool is dedicated to Azure, select “Cancel” to avoid creating these roles.

You will now have the id of the identity pool you just created. You can also find the Identity pool id by selecting the pool in Cognito. This selection takes you to the Cognito pool dashboard where you can see the number of Cognito identities created and their activity.

Cognito identity pool dashboard

Notice the warning You have not specified roles for this identity. Click here to fix it. Since these roles are only required when you use Cognito to access AWS resources, you can ignore this warning. Selecting Edit identity pool takes you to the details of your identity pool, including the pool id.

In my case, the identity pool id is:

us-east-1:59d4a12a-deaa-4a98-85d1-b5d9fc2d41ef

Create a Cognito identity for our workload

At this point, we have created a Cognito identity pool. You can now create identities in this pool to be used by your software workload. To create an identity, you can use AWS CLI with your credentials and do the following:

aws cognito-identity get-open-id-token-for-developer-identity \
    --identity-pool-id <the pool id you just created>  \
    --logins <developer provider name>=<a_unique_string_identifying_your_workload> 
    --region <aws region>

for example, in my case:

aws cognito-identity get-open-id-token-for-developer-identity \
    --identity-pool-id us-east-1:59d4a12a-deaa-4a98-85d1-b5d9fc2d41ef  \
    --logins azure-access=wlid_1 --region us-east-1

Note that wlid_1 is just a random string I made up for one of my workloads. Any workload authorized by me to get tokens from this pool can request tokens by providing this string. Here’s the response from the AWS CLI.

{
    "Token": <a long token string>,
    "IdentityId": "us-east-1:d2237c93-e079-4eb3-964c-9c9a87c465d7"
}

Since this is the first time the identity pool sees a request for wlid_1, it created an identity in the pool and linked it with my wlid_1 identity. Any future token request for wlid_1 will always return a token for this Cognito identity. The subject claim of the token will contain the Cognito IdentityId. In my example above, the subject claim will be us-east-1:d2237c93-e079-4eb3-964c-9c9a87c465d7

You may wonder how you will keep track of an additional set of these developer identities for your workloads. A simpler alternative is to use the client_id of the Azure AD workload identity you will use for this federation. Let’s use that approach in this walkthrough. Let’s create a Cognito identity using the client_id of our managed identity.

aws cognito-identity get-open-id-token-for-developer-identity \
    --identity-pool-id us-east-1:59d4a12a-deaa-4a98-85d1-b5d9fc2d41ef  
    --logins azure-access=cc312bc3-3859-42f1-9598-2371055dbfa4 --region us-east-1

Here’s the response from Amazon Cognito

{
    "Token": <a long token string>,
    "IdentityId": "us-east-1:fa442090-a36f-44d4-81ac-ab21418d0e90"
}

Unraveling the Cognito token shows the following details:

Header
{
  "kid": "us-east-11",
  "typ": "JWS",
  "alg": "RS512"
}
Payload
{
  "sub": "us-east-1:fa442090-a36f-44d4-81ac-ab21418d0e90",
  "aud": "us-east-1:59d4a12a-deaa-4a98-85d1-b5d9fc2d41ef",
  "amr": [
    "authenticated",
    "azure-access",
    "azure-access:us-east-1:59d4a12a-deaa-4a98-85d1-b5d9fc2d41ef:cc312bc3-3859-42f1-9598-2371055dbfa4"
  ],
  "iss": "https://cognito-identity.amazonaws.com",
  "exp": 1669672295,
  "iat": 1669671395
}

The iat claim tells us when the token was issued. The exp claim indicates when the token will expire. The difference between the two is 900, showing the token is valid for 900 seconds or 15 minutes.

At this point, we have a Cognito identity linked to the client_id of our managed identity. You can visit the identity browser in Cognito to see your identities. In the below screenshot, notice that the Cognito identity is linked to my developer-provided identity.

Cognito identity browse

Part 2: Configuring an Azure AD identity to trust the Amazon Cognito token

Azure AD has two kinds of workload identities: applications and managed identities. Both identities support federation with a token from an OIDC token issuer.

In this step-by-step, we will use a managed identity that has already been granted access to my Azure storage. We will tell Azure AD to link the managed identity to the Cognito identity by creating a Federated Identity Credential on the managed identity. You can add up to twenty of these trusts to each Azure AD workload identity.

The most important parts of the federated identity credential are the following:

  • subject: this should match the sub claim in the token issued by another identity provider, such as Amazon Cognito. This is the IdentityId we got from Cognito in the earlier section. (In this example: us-east-1:fa442090-a36f-44d4-81ac-ab21418d0e90).
  • issuer: this should match the iss claim in the token issued by the identity provider. The issuer is an URL that must comply with the OIDC Discovery Spec. Azure AD will use this issuer URL to fetch the keys necessary to validate the token. In the case of Amazon Cognito, the issuer is https://cognito-identity.amazonaws.com
  • audience: this should match the aud claim in the token. For security reasons, you should pick a value that is unique for tokens meant for Azure AD. The Microsoft recommended value is api://AzureADTokenExchange. However, there is no option to configure this in Amazon Cognito. It only issues tokens with the identity pool id as the audience. So we will configure the Cognito identity pool id as the audience in the federated identity credential. Tokens with this audience value are intended for Azure AD. (In this example: us-east-1:59d4a12a-deaa-4a98-85d1-b5d9fc2d41ef)

Let’s use these values to configure the federated identity credential on our managed identity. You can configure the federated credential on a managed identity using the Azure portal, the Azure CLI, or Azure ARM templates. The role of either owner or contributor is required to create the federated credential on a managed identity.

Using Azure CLI

az identity federated-credential create --name AccessFromAWS --identity-name workload-federate-MI1 \
   --resource-group codesamples-rg --issuer https://cognito-identity.amazonaws.com \
   --subject us-east-1:fa442090-a36f-44d4-81ac-ab21418d0e90 --audience us-east-1:59d4a12a-deaa-4a98-85d1-b5d9fc2d41ef

On success, you should expect to see something like this:

{
  "audiences": [
    "us-east-1:59d4a12a-deaa-4a98-85d1-b5d9fc2d41ef"
  ],
  "id": "/subscriptions/22a08bf1-fb31-4757-a836-cd035976a2c0/resourcegroups/codesamples-rg/providers/Microsoft.ManagedIdentity/userAssignedIdentities/workload-federate-MI1/federatedIdentityCredentials/AccessFromAWS",
  "issuer": "https://cognito-identity.amazonaws.com",
  "name": "AccessFromAWS",
  "resourceGroup": "codesamples-rg",
  "subject": "us-east-1:fa442090-a36f-44d4-81ac-ab21418d0e90",
  "type": "Microsoft.ManagedIdentity/userAssignedIdentities/federatedIdentityCredentials"
}

Verify you can access Azure using the Cognito token

At this point, you have a Cognito pool setup with a Cognito identity. The Azure managed identity has a federated credential matching the Cognito identity. You can now do a quick test with the AWS and Azure CLIs and verify that you can access Azure resources available to that managed identity using the Cognito token.

In my case, I have granted the managed identity access to my Azure blob storage. And I can do the following to verify the setup.

Get a Cognito token using AWS CLI
aws cognito-identity get-open-id-token-for-developer-identity \
    --identity-pool-id us-east-1:59d4a12a-deaa-4a98-85d1-b5d9fc2d41ef  
    --logins azure-access=cc312bc3-3859-42f1-9598-2371055dbfa4 --region us-east-1

On success, the response would look something like this:

{
    "Token": <Token string>,
    "IdentityId": "us-east-1:fa442090-a36f-44d4-81ac-ab21418d0e90"
}
Login to Azure using that token
az login --federated-token <Token string> --tenant <tenant id> \
    --service-principal -u <client_id of managed identity>

If this succeeds, you should see a response that looks like this:

[
  {
    "cloudName": "AzureCloud",
    "homeTenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
    "id": "22a08bf1-fb31-4757-a836-cd035976a2c0",
    "isDefault": true,
    "managedByTenants": [],
    "name": "Visual Studio Enterprise subscription",
    "state": "Enabled",
    "tenantId": "72f988bf-86f1-41af-91ab-2d7cd011db47",
    "user": {
      "name": "cc312bc3-3859-42f1-9598-2371055dbfa4",
      "type": "servicePrincipal"
    }
  }
]
Access Azure resource

Now access the Azure resource available to the managed identity

az storage blob exists --container-name test --name testBlob \
    --account-name demofederate --auth-mode login

A successful response indicates everything is working end-to-end!

{
  "exists": true
}

Part 3: Getting a Cognito token and exchanging it for an Azure AD token

Now that we have configured an Azure AD workload identity to trust the Cognito identity, we are ready for our software workload to get a token from Cognito and exchange it for an Azure AD access token.

We will use the Amazon Cognito SDK to get Cognito tokens. Depending on where the software workload is running, EC2 or Lambda, you first need to grant the compute environment permissions to request tokens from your Cognito pool.

Configure your EC2 instance or Lambda with permissions to your Cognito identity pool

Head to the AWS IAM console. Create a permission policy for the Cognito pool. Pick the Cognito Identity service, and add the actions of GetOpenIdTokenForDeveloperIdentity, LookupDeveloperIdentity, MergeDeveloperIdentities, and UnlinkDeveloperIdentities. Pick the specific resource that identifies the Cognito identity pool just created.

Cognito permission

Give an appropriate name for this policy and create it.

Cognito permission

Create an EC2 role, and assign this permission policy to that role.

EC2 role

Assign this role to our EC2 instance. Any software workload running on this EC2 instance now has permission to request tokens from our Cognito pool.

Assign to EC2 instance

Get the AWS token from your code in EC2

Use the client-cognito-identity AWS SDK to get Cognito tokens.

import { CognitoIdentityClient, GetOpenIdTokenForDeveloperIdentityCommand } from "@aws-sdk/client-cognito-identity"; // ES Modules import

Create a CognitoIdentityClient.

    cognitoClient = new CognitoIdentityClient({ region }); 

Use this client to get Cognito tokens.

async function getCognitoToken() {
    var Logins:any = {};
  
    // hardcoding value here for demonstration purposes only. 
    // "cc312bc3-3859-42f1-9598-2371055dbfa4" is our developer id assigned to this
    // workload, it is linked to a unique Cognito identity created within the pool 
    // when we used the aws cli earlier in this blog post. azure-access is 
    // the developer provider name that we configured when we created the Cognito pool
  
    Logins["azure-access"] = "cc312bc3-3859-42f1-9598-2371055dbfa4"; 
        
    const command = new GetOpenIdTokenForDeveloperIdentityCommand(
                            {IdentityPoolId: poolId, Logins });

    return cognitoClient.send(command)
    .then(function(data:any) {
        return data.Token;
    })
    .catch(function(error:any) {
        throw(error);
    });
}

Exchanging the identity token for an Azure AD access token

The Azure Identity SDK has added support for a new credential called ClientAssertionCredential. This credential accepts a callback function to get a federated token. I like using this credential since it simplifies the developer experience.

Here’s a sample code snippet to demonstrate this. You can see a more detailed sample in my GitHub repo

import { ClientAssertionCredential } from "@azure/identity";

// pass the getCognitoToken function we defined above to ClientAssertionCredential
// It gets called whenever ClientAssertionCredential needs a new federated token from Cognito
tokenCredential = new ClientAssertionCredential( tenantID,
                                                 clientID,
                                                 getCognitoToken ); 

Once you have the tokenCredential, you can use this in an Azure SDK such as storage-blob.

const { BlobServiceClient } = require("@azure/storage-blob");

const blobClient = new BlobServiceClient(blobUrl, tokenCredential);

When requests are made to the blobClient to access storage, the blobClient calls the getToken method on the ClientAssertionCredential. This call results in a request for a new token from Cognito, which then gets exchanged for an Azure AD access token. The ClientAssertionCredential takes care of caching tokens, so tokens are requested from Cognito or Azure AD only when necessary.

This ClientAssertionCredential is available in the latest shipping Azure Identity SDKs. It was introduced in these versions:

Now that we have completed the end-to-end walkthrough, let’s look at some additional details.

Additional details

Using MSAL instead of AzureIdentity for getting tokens.

If your preferred SDK is Microsoft Authentication Library (MSAL), that SDK is available in the following languages and supports exchanging tokens.

My GitHub repo also has a sample using MSAL to exchange a Cognito token for an Azure AD token.

Using an Azure AD application instead of managed identity

Azure AD has two kinds of workload identities: applications and managed identities. In the walkthrough so far, we considered a user-assigned managed identity. You can also choose to use an application identity instead of managed identity. You can configure a federated credential on an application using Azure portal, Azure CLI, or Microsoft Graph SDK.

Permissions needed to create federated credentials on an application identity

To create federated credentials on an application, you either need one of these roles:

Or this permission:

Configuring a federated credential on an application using Azure CLI

Create a JSON file called credential.json.

{ "name": "AccessFromAWS", 
  "issuer": "https://cognito-identity.amazonaws.com", 
  "subject": "us-east-1:fa442090-a36f-44d4-81ac-ab21418d0e90",
  "audiences": ["us-east-1:59d4a12a-deaa-4a98-85d1-b5d9fc2d41ef"], 
  "description": "This is meant for workloads in project X" 
}

Then create a federated identity credential on the Azure AD application

az ad app federated-credential create --id <appid> --parameters credential.json

In my example, it succeeds with this message:

{
  "@odata.context": "https://graph.microsoft.com/v1.0/$metadata#applications('1a4314ae-2735-4fc8-866d-c7d320ab8fd0')/federatedIdentityCredentials/$entity",
  "audiences": [
    "us-east-1:59d4a12a-deaa-4a98-85d1-b5d9fc2d41ef"
  ],
  "description": "This is meant for workloads in project X",
  "id": "430e2e5d-d06e-4bd2-a6aa-b10f3255e5e7",
  "issuer": "https://cognito-identity.amazonaws.com",
  "name": "AccessFromAWS",
  "subject": "us-east-1:fa442090-a36f-44d4-81ac-ab21418d0e90"
}

Configuring federated credentials programmatically

You can also configure the federated credentials programmatically on both applications and managed identities.

Using the Microsoft Graph SDKs for Azure AD application identity

You can use the Microsoft Graph SDKs, available in several languages, to view or modify a federated identity credential. You can choose either the delegated flow or the application-only flow: make sure the service principal you use for this purpose has consent for the Application.ReadWrite.All permissions.

The federatedIdentityCredentials API URL is (https://graph.microsoft.com/v1.0/applications/”object-id-of-app”/federatedIdentityCredentials). The Graph SDK is available in several languages.

Using ARM templates for Azure managed identity

You can use ARM templates, Bicep, or Terraform to configure federated identity credentials on a managed identity.

Understanding the token details

AWS is different from other cloud providers when dealing with identities for software workloads. There is no native concept of a workload identity or service account. And there is no direct mechanism for an EC2 instance to request a JWT token, for example via IMDS, to authenticate to another identity provider.

My colleague, Alexander Riman, suggested that Amazon Cognito GetOpenIdTokenForDeveloperIdentity could be used to support the federation flow to access Azure resources from AWS.

Amazon Cognito issues JWT tokens that can be validated via the OpenID Connect protocol. Cognito has two pools, the User pool, and the Identity pool. The User pool enables developers to authenticate users to software workloads. It does not offer any support to identify software workloads. The Identity pool has a custom authentication provider for developers to provide identities to their software workloads and get tokens for those identities. The identity pool’s purpose is to authenticate workloads to access AWS resources. The audience claim in the token is always the identity pool id and is not customizable. Despite this limitation, the Identity pool will serve our purpose of federating with Azure AD.

Cognito documents two auth flows in the Identity pool, the enhanced auth flow, and the basic auth flow.

Understanding the Cognito enhanced auth flow

This flow is also called the simplified flow. In this flow, the workload only communicates with the Cognito service and gets temporary credentials for one of the two roles in Cognito. It can access the AWS resources available to that role.

Here’s the token flow image for the enhanced flow, copied from Amazon documentation:

Cognitor enhanced auth flow

  1. The software workload uses a developer provided id and requests a token from Cognito (GetOpenIdTokenForDeveloperIdentity)
  2. Cognito finds the Cognito id matching the developer id (or creates a new Cognito id) and returns a token for that identity.
  3. The software workload presents the token to Cognito. Cognito matches this to one of the two roles in Cognito. Cognito requests temporary credentials from AWS STS for the AWS resources permitted via that role.
  4. Cognito returns the temporary creds for access to those AWS resources.

In this auth flow, you manage only two roles in Cognito. However, all your workloads get access to the same set of AWS resources available through the two roles configured for that pool.

Understanding the Cognito basic auth flow

In the basic auth flow, the software workload uses the JWT token issued by Cognito to assume an AWS IAM role.

Here’s the token flow image for the basic flow, copied from Amazon documentation: Cognito basic auth flow

  1. The software workload uses a developer provided id and requests a token from Cognito (GetOpenIdTokenForDeveloperIdentity)
  2. Cognito finds the Cognito id matching the developer id (or creates a new Cognito id) and returns a token for that identity.
  3. The software workload presents the token to AWS STS with an AWS IAM role to AssumeRoleWithWebIdentity.
  4. AWS IAM returns the temporary creds for that role. The software workload can access AWS resources assigned to that role.

This approach allows you to provide finer-grained access to your workloads by defining different roles in AWS IAM.

Understanding the Cognito to Azure AD auth flow

We will use a pattern similar to the basic auth flow: get a token from Cognito and then present it to Azure AD to get a token for an identity with access to Azure resources.

end to end flow

  1. The software workload uses a developer provided id and requests a token from Cognito (GetOpenIdTokenForDeveloperIdentity)
  2. Cognito finds the Cognito id matching the developer id (or creates a new Cognito id) and returns a token for that identity.
  3. The software workload presents the token to Azure AD and requests a token for a managed identity.
  4. Azure AD validates the token with Cognito and returns an Azure AD token.

Since we are not accessing AWS resources using our pool, we don’t need to add any roles to Cognito.

The need to keep the Cognito pool dedicated to Azure AD federation

The audience claim in JWT tokens is critical for security reasons. It contains the intended recipient of the token. Any service receiving a token must check it is the intended recipient of the token. To see why this is important, let’s consider a hypothetical example. Service-A needs to authenticate to Service-B and Service-C. If it uses the same token to authenticate to both services, Service-B can also use that token to act like Service-A when accessing Service-C. However, if the token had the audience indicating the token was for Service-B, then Service-C would reject that token since the intended audience is Service-B.

In the AWS Cognito identity pool, the audience is fixed and always the identity pool id. Tokens issued by the identity pool are intended for a single recipient. When you configure Azure AD to accept tokens with the identity pool id as the audience, you make Azure AD the intended recipient of these tokens. It’s a security best practice to avoid using tokens with the same audience when accessing AWS resources. Use a different identity pool for that purpose.

In conclusion

Azure AD workload identity federation is a capability that enables getting rid of secrets in several scenarios like services running in Kubernetes clusters, GitHub Actions workflow, and services running in Google and AWS Cloud. Stay tuned for many more use cases where this capability can help remove secrets.

If you have any comments, feedback, or suggestions on this topic, I would love to hear from you. DM me on twitter