Authorizing AWS Principals on Azure

23.09.2025

Jonathan Merlevede

How to delegate trust from Entra to AWS IAM through Cognito, authorizing Azure actions without needing long-lived credentials.

AWS IAM principals can be granted access to AWS resources through AWS IAM policies. Unfortunately, those policies do not carry weight outside of AWS, and certainly not within Azure. But what if you want to, say, query your company’s Entra directory from AWS? How then can you assign Azure privileges to AWS IAM roles and users?

Azure does not recognize your IAM Principal’s authority (image source).

On Azure, not AWS IAM but another service rules the roost. Microsoft Entra ID — the product formerly known as Azure Active Directory — manages user identities, apps, and access to Microsoft resources, including Azure, Microsoft 365, and even other applications that support Microsoft Entra ID (source). The latter notably do not include AWS applications.

This post shows how to use AWS Cognito as a bridge to generate OIDC tokens using AWS IAM-derived privileges, and how to exchange those for Microsoft Identity Platform tokens authorizing Azure (Entra) actions. We show how to set up the required infrastructure using Terraform.

For those seeking a deeper understanding of authentication, this post also includes a second part that presents the classical approach to machine-to-machine (M2M) authentication, explores what it means to trust, and touches on what it takes to reverse the scenario — setting up AWS to trust Entra principals.


Workload Identity Federation

You can configure Entra application registrations to trust a third-party OpenID Provider (OP). Entra refers to this as “Federated Credentials” and also as “Workload Identity Federation”.

After configuring Entra to trust a third-party OP, the JWT tokens issued by the third party allow obtaining Microsoft tokens through the client credentials flow with token assertions (see below, section Certificate-Based Authentication). You do not pin secrets or public keys inside Entra; instead, Microsoft validates the tokens you present to it using public keys it retrieves from the OIDC well-known endpoint you configure (see below, section OpenID Connect). Private key rotation is part of the protocols, and OPs generally rotate signing keys automatically.

Setting up AWS and Entra for trust delegation

We can configure Entra to trust a third-party OP. Unfortunately, AWS STS is not an OP, and AWS credentials are not OIDC JWT tokens. Luckily, AWS offers an OIDC-compatible identity service in the form of Cognito User Pools, and we can protect access to it using AWS IAM.

We demonstrate how to configure user pools to permit only IAM-protected login flows, allowing the exchange of AWS credentials for short-lived Cognito JWT tokens. We then show how to configure Entra to trust Cognito. Finally, we present the whole flow, culminating in Microsoft identity platform bearer tokens.

Cognito

Cognito allows users to obtain JWT tokens in several ways:

Additionally, two Cognito AWS API calls result in JWT tokens:

To the extent possible, we will disable OAuth flows and flows compatible with InitiateAuth at the Cognito client level, as these are not IAM-protected.

AWS Infrastructure

On the AWS side, create a Cognito user pool, register a Cognito application, and create a Cognito user. All these resources are free for the given settings.

User pools require only minimal configuration. You can create one through Terraform as follows:

resource "aws_cognito_user_pool" "this" {  name             = "demo"  alias_attributes = ["preferred_username"]  admin_create_user_config {    allow_admin_create_user_only = true  }}

Cognito clients require more configuration. At the time of writing, setting up a client through the Console web UI always results in at least one enabled OAuth flow, which cannot be disabled. The Cognito API, however, allows the creation of an application that allows only the AdminInitiateAuth API call as desired. You can create such an application using Terraform as follows:

resource "aws_cognito_user_pool_client" "this" {  name            = "demo"  user_pool_id    = aws_cognito_user_pool.this.id  generate_secret = false  allowed_oauth_flows_user_pool_client = false  allowed_oauth_flows                  = []  enable_token_revocation              = false  explicit_auth_flows = ["ALLOW_ADMIN_USER_PASSWORD_AUTH"]  id_token_validity      = 60  access_token_validity  = 60  refresh_token_validity = 60  token_validity_units {    id_token      = "minutes"    access_token  = "minutes"    refresh_token = "minutes"  }}

Despite not allowing the REFRESH_TOKEN_AUTH auth flowhere , at the time of writing AdminInitiateAuth always returns a refresh token, which can then be used together with the unprotected InitiateAuth and/or theGetTokensFromRefreshToken API calls depending on refresh token rotation configuration. We set the lifetime of refresh tokens to its minimum value (1 hour) to limit the impact of this weird behavior.

Note that we do not create a Cognito M2M application supporting the client credentials flow. Firstly, this results in a client secret, which, although easily rotated, is not what we want here. Secondly, calls to Cognito’s token endpoint are not IAM-protected; again, this is not what we look for in this post. Thirdly, Cognito M2M applications are not free.






Lastly, we need a Cognito user. As this user is only able to obtain tokens using the IAM-protected AdminInitiateAuth call and not through password grants or InitiateAuth, its password only serves as an unnecessary second factor and does not have to remain secret:

resource "aws_cognito_user" "this" {  user_pool_id = aws_cognito_user_pool.this.id  username     = "dummyuser"  password     = "dummyPassword1!"}

Lastly, to be able to call AdminInitiateAuth your IAM principal needs rights:

{    "Version": "2012-10-17",    "Statement": [        {            "Effect": "Allow",            "Action": "cognito-idp:AdminInitiateAuth",            "Resource": "arn:aws:cognito-idp:REGION:ACCOUNT_ID:userpool/<userpoolid>"        }    ]}

If you have multiple Azure applications and want to scope access to specific ones through AWS IAM, consider creating one user pool, client, and user for each one. The AdminInitiateAuth IAM action does not support conditions to limit it to specific client IDs or users. There is a generous limit of 1000 user pools per region (can be increased to 10000).

Obtaining AWS Bearer Tokens

If you are authenticated to AWS as a principal authorized to perform AdminInitiateAuth, obtain tokens as follows:

aws cognito-idp admin-initiate-auth \--region eu-west-1 \--user-pool-id "$user_pool_id" \--client-id "$client_id" \--auth-flow ADMIN_USER_PASSWORD_AUTH \--auth-parameters 'USERNAME=dummyuser,PASSWORD=dummyPassword1!'

This returns something like:

{  "ChallengeParameters": {},  "AuthenticationResult": {    "AccessToken": "<JWT token>",    "ExpiresIn": 3600,    "TokenType": "Bearer",    "RefreshToken": "<JWT token>",    "IdToken": "<JWT token>"  }}

The access tokens retrieved using AdminInitiateAuth always have scope aws.cognito.signin.user.admin and no audience. Its payload looks as follows:

{  "sub": "<uuid>",  "event_id": "<another-uuid>",  "token_use": "access",  "scope": "aws.cognito.signin.user.admin",  "auth_time": 1756773256,  "iss": "https://cognito-idp.<region>.amazonaws.com/<userpool-id>",  "exp": 1756773556,  "iat": 1756773256,  "jti": "<yet-another-uuid>",  "client_id": "<your application client id>",  "username": "dummyuser"}

Requesting custom scopes with Cognito is possible only when requesting tokens through OAuth flows.

As is suggested in trust delegation scenarios, we will not use the access token. Instead, we will exchange the identity token for a Microsoft/Entra JWT token. The Cognito identity token has the Cognito client ID as the audience. The decoded payload of an identity token looks as follows:

{  "sub": "<uuid of dummyuser>",  "aud": "<your application client id>",  "event_id": "<another-uuid>",  "token_use": "id",  "auth_time": 1756771643,  "iss": "https://cognito-idp.<region>.amazonaws.com/<userpool-id>",  "cognito:username": "dummyuser",  "exp": 1756775243,  "iat": 1756771643}


Configuring Trust Relationship

Now it is time to create an Entra application and configure it to trust our Cognito tokens (“federated credentials”). You can do so using the UI. The code below creates an application and configures trust using Terraform:

resource "azuread_application" "this" {  display_name = "blogpost"}
resource "azuread_application_federated_identity_credential" "this" {  application_id = azuread_application.this.id  display_name   = "cognito"  description    = "Trust Cognito"  audiences      = [aws_cognito_user_pool_client.this.id]  issuer         = "https://${aws_cognito_user_pool.this.endpoint}"  subject        = aws_cognito_user.this.sub}

This configures Entra only to accept specific tokens:

  • Tokens from your Cognito user pool, thanks to the issuer setting. Under the hood, Entra queries https://${aws_cognito_user_pool.this.endpoint}/.well-known/openid-configuration and retrieves public keys (JWKS) for validating signatures.

  • Tokens authenticating your specific Cognito user, thanks to the subject setting.

  • Identity tokens that were generated for your application/client, thanks to the audience setting.

Obtaining tokens

Defining getcreds as an alias for the aws cognito-idp admin-initiate-auth command from before, you can now obtain Microsoft identity platform bearer tokens as follows (reference):

TENANT="<your-tenant-id>"AZURE_CLIENT_ID="<your-app-registration-id>"COGNITO_TOKEN="$(getcreds | jq .AuthenticationResult.IdToken -r )"TOKEN_URL="https://login.microsoftonline.com/$TENANT/oauth2/v2.0/token"curl -X POST "$TOKEN_URL" \-H "Content-Type: application/x-www-form-urlencoded" \-d "client_id=$AZURE_CLIENT_ID" \-d "grant_type=client_credentials" \-d "scope=https://graph.microsoft.com/.default" \-d "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \--data-urlencode "client_assertion=$COGNITO_TOKEN"


If you want to understand better what happens here under the hood, or learn about alternate ways of establishing trust, read along.

Classical Approach to Trust

Establishing trust and authority with a sceptical party like Entra is always done by demonstrating to it that you have or know something that only an authorized actor should have or know.

In classical machine-to-machine interactions with Entra, this “secret something” is one of two things:

  • A client ID and client secret. These work like classical usernames and passwords. Entra knows your secrets, and you show that you do too by a straightforward “show of hands”.

  • A certificate or public/private key pair. In this case, the public part of the secret is registered with Entra. You present Entra with a secret value derived from your private key. Entra can then validate that you are in possession of the private key using the corresponding public key.

To bridge AWS IAM and Entra, you store the “secret somethings” in a location protected by AWS IAM, e.g. in AWS Secrets Manager, in the Systems Manager (SSM) Parameter Store or even as an object on S3. Then, to make authorized calls to Azure resources, you retrieve and use them.

Certificates and private keys are considered more secure than client id/secret pairs or username/password combinations because private keys themselves are not transmitted, and the secrets derived from it are short-lived.

The main problem with this basic approach is that “secret somethings” are usually long-lived, with rotation remaining a manual, infrequent, error-prone, and even forgotten-about process. Automatic rotation can alleviate this. We will get back to this below.

Certificate-Based Authentication

Before moving on, let’s dive a bit deeper into how certificates are used with Azure and Entra ID. This helps understand how similar the flow with federated credentials is.

To establish trust using certificates, you request Entra bearer tokens using the OAuth client credentials grant (RFC6749) with JWT token assertions (RFC 7523). It works as follows:

  • Generate an X.509 certificate. Entra requires you to pin specific certificates, so public key infrastructure (PKI) does not apply. You can and probably should use a self-signed certificate.

  • Register your certificate’s public key in the application registration.

  • Construct a JWT token that will serve as an assertion. Sign the JWT token using your certificate’s private key.

  • Obtain an Entra bearer token from Microsoft identity platform using the client credentials flow — that is, make a request to Microsoft’s token endpoint, exchanging the “assertion token” you signed for one that Microsoft signed.

You probably do not want to code this flow yourself; instead opt to use a library, such as Microsoft’s Authentication Library (MSAL) for Python.

Microsoft identity platform trusts you because it can validate the signature of the assertion token using the public key you gave to Entra ID before. Only someone who knows the corresponding private key can create a valid signature. As a token of this trust (😏), Microsoft gives you a “bearer token”

The bearer token is again something proving that you are indeed worthy of trust, but this time to different untrusting parties: Azure resources that are trusting of the Microsoft identity platform and nothing else. You can use the MS bearer token to authorize calls to any Entra-protected API by embedding it into the Authorization header of your HTTP calls.

Using federated credentials works in the same way, except the assertion tokens you use are not self-signed JWT tokens, signed using a certificate you pinned in Entra, but rather tokens issued by the trusted OP.

OpenID Connect

At this point, you may wonder how services receiving a Microsoft Identity bearer token or assertion from your federated OP validate its veracity.

Microsoft identity platform is an OpenID Provider (OP), meaning that it complies to the OpenID Connect (OIDC) specification. Like most OPs, it additionally implements the OpenID Connect Discovery protocol. In a nutshell, this means that:

  • The bearer tokens it issues are JWT tokens, valid only for a limited duration and easily decoded.

  • The JWT tokens are signed using a private key known only to Microsoft.

  • The public keys corresponding to the private ones that Microsoft uses for signing its tokens can be retrieved from standardized “well-known” endpoints. Multiple public keys can be acceptable at the same time, allowing for seamless key rotation.

Thanks to standardization, all you need to do to be able to trust tokens issued by the Microsoft identity platform (or any other OP) is know its well-known endpoint, hosted on an HTTPS URL. Thanks to the global PKI and its network of certificate authorities (out of scope here), you know that the information you find there is trustworthy. You retrieve public keys from coordinates you find at the well-known endpoint and can use them to verify bearer tokens. Microsoft can and does rotate its certificates, without it causing any disruptions or requiring changes on the end of its clients.

Reversed scenario

Because Entra and the Microsoft Identity Platform are OpenID providers and AWS supports what it calls “web identity credentials”, authorizing Entra identities to access AWS resources is a lot easier than the reverse scenario discussed in this post.

To do so, register your Entra tenant as an OIDC web identity provider on your AWS account. Then update your roles’ assume role policies appropriately. This enables the exchange of Entra tokens for temporary AWS credentials using AssumeRoleWithWebIdentity API calls. The exchange of JWT for and refreshing of AWS session credentials can happen automagically under the hood, e.g., by setting appropriate environment variables pointing to your Entra JWT token (AWS_WEB_IDENTITY_TOKEN_FILE) and setting the role you want to assume (AWS_ROLE_ARN) — so, unlike the solution above, this flow will work with all AWS SDKs out of the box without further customization.

If your Entra tokens are highly privileged, consider exchanging them for less priviledged ones before sending them to AWS.

Conclusion

This post leverages Cognito, AWS’s fully managed OIDC-compatible identity provider service, to exchange IAM credentials for Entra bearer tokens, without incurring costs or requiring any form of long-lived credentials. It goes on to show how certificate-based OAuth client credential flows work and how OIDC facilitates establishing trust, including seamless rotation of key pairs. Finally, it discussed that exchanging Azure credentials for AWS is possible in a more straightforward manner.

Creating a Cognito user pool, Cognito client, and especially a Cognito user in this setup is arguably awkward, and you may prefer using long-lived secrets on account of their relative simplicity. We did not outline how the solution above can be integrated with Azure SDKs to fetch and refresh tokens automatically. If you know of an easier way to exchange AWS IAM credentials for Entra ones, be sure to comment!


Source: South Park

Press enter or click to view image in full size

Latest

The ROI Challenge: Why Measuring Data’s Value is Hard, but Crucial

Too many data products, not enough ROI? Learn how to track value, cost & governance to manage data as a true business asset.

Authorizing AWS Principals on Azure

How to delegate trust from Entra to AWS IAM through Cognito, authorizing Azure actions without needing long-lived credentials.

Why Your Next Data Catalog Should Be a Marketplace

Why data catalogs fail - and how a Data Product Marketplace can rebuild trust, drive adoption, and unlock business value from your data.

Hinterlasse deine E-Mail-Adresse, um den Dataminded-Newsletter zu abonnieren.

Hinterlasse deine E-Mail-Adresse, um den Dataminded-Newsletter zu abonnieren.

Hinterlasse deine E-Mail-Adresse, um den Dataminded-Newsletter zu abonnieren.

Belgien

Vismarkt 17, 3000 Leuven - HQ
Borsbeeksebrug 34, 2600 Antwerpen


USt-IdNr. DE.0667.976.246

Deutschland

Spaces Kennedydamm,
Kaiserswerther Strasse 135, 40474 Düsseldorf, Deutschland


© 2025 Dataminded. Alle Rechte vorbehalten.


Vismarkt 17, 3000 Leuven - HQ
Borsbeeksebrug 34, 2600 Antwerpen

USt-IdNr. DE.0667.976.246

Deutschland

Spaces Kennedydamm, Kaiserswerther Strasse 135, 40474 Düsseldorf, Deutschland

© 2025 Dataminded. Alle Rechte vorbehalten.


Vismarkt 17, 3000 Leuven - HQ
Borsbeeksebrug 34, 2600 Antwerpen

USt-IdNr. DE.0667.976.246

Deutschland

Spaces Kennedydamm, Kaiserswerther Strasse 135, 40474 Düsseldorf, Deutschland

© 2025 Dataminded. Alle Rechte vorbehalten.