Forem

Cover image for No Certs, No Secrets: Microsoft Graph on Azure using Entra Workload Identity Federation and Amazon Cognito (OIDC)
samconibear
samconibear

Posted on

No Certs, No Secrets: Microsoft Graph on Azure using Entra Workload Identity Federation and Amazon Cognito (OIDC)

I recently needed to authenticate a server side only app with Microsoft Graph, without the requirements for client secrets or certificates (which require manual rotation in a enterprise security context).

The solution is to use Microsoft Entra Workload Identity Federation (WIF) with Open ID Connect (OIDC) so that an app can obtain tokens by trusting an external OIDC identity provider, in this case Microsoft Azure AD.

I could not find a good guide for this online so I decided to write my own.

There are a number of steps required to make this solution a possible, as follows:

  1. Create an Identity Pool in Amazon Cognito
  2. Create an IAM Policy to allow access to said Pool
  3. Request a JWT from Amazon Cognito to create an identity
  4. Create an Azure AD app registration
  5. Configure the Azure AD app to trust the Identity Pool as an OIDC provider
  6. Exchange a JWT from Amazon Cognito with Microsoft for an Azure JWT

1. Create an Identity Pool in Amazon Cognito

Amazon Cognito Identity Pools is a service that issues federated, temporary AWS-based identities. We will use our identity pool to generate OpenID Connect (OIDC) JWT tokens.
We will create an identity pool for authenticated access, with the identity source being an OIDC provider.

CDK:

const DEVELOPER_PROVIDER_NAME = 'my.dev.provider'
// This can be whatever you want but must be dot delimited

const identityPool = new cdk.aws_cognito.CfnIdentityPool(this, 'myIdentityPool', {
    allowUnauthenticatedIdentities: false,
    developerProviderName: DEVELOPER_PROVIDER_NAME,
    identityPoolName: 'my-identity-pool', // Up to you
});
Enter fullscreen mode Exit fullscreen mode

2. Create an IAM Policy to allow access to said Pool

We will need to create a policy to allow access to this identity pool from other services, for this demo we will use the default created role "Create a new IAM role"

We will skip the other steps creation steps in the console.

CDK:

we will manually create the policy, and can assign it to a role later.

const policy = new cdk.aws_iam.PolicyStatement({
      effect: cdk.aws_iam.Effect.ALLOW,
      actions: [
        'cognito-identity:GetOpenIdTokenForDeveloperIdentity',
        'cognito-identity:GetId',
      ],
      resources: [ identityPoolArn ],
    });
Enter fullscreen mode Exit fullscreen mode

3. Request a JWT from Amazon Cognito to create an identity

Now we've created our Cognito Identity Pool, we can request a identity federation token from it. We will need to know the ID of our identity pool - This is the "audience" for our Azure identity federation.
We can get this from the AWS console under "Identity Pool ID" or from CDK by accessing the ref property of our identityPool

to do this we must assume a role with the above policy assigned. I will do this by creating a AWS lambda function and assigning said role.

CDK:

const fn = new lambda.Function(this, 'GetToken', {
      runtime: lambda.Runtime.PYTHON_3_14,
      handler: 'index.handler',
      code: lambda.Code.fromInline(`
from boto3 import client
DEVELOPER_USER_NAME = 'my_user_id' # Note this down
DEVELOPER_PROVIDER_NAME = '${ DEVELOPER_PROVIDER_NAME }' # From earlier definition
IDENTITY_POOL_ID = '${ identityPool.ref }'
def handler(event, context):
    cognito_client = client('cognito-identity')
    return cognito_client.get_open_id_token_for_developer_identity(
        IdentityPoolId = IDENTITY_POOL_ID ,
        Logins={
            DEVELOPER_PROVIDER_NAME: DEVELOPER_USER_NAME
        },
        TokenDuration=60, # s
    )
`
});
fn.addToRolePolicy(policy)
Enter fullscreen mode Exit fullscreen mode

it is important to note the chosen DEVELOPER_PROVIDER_NAME & DEVELOPER_USER_NAME as the same values must be used to request tokens in the future.
After running the lambda we should receive a base64 encoded JWT of the shape:

{
  "iss": "https://cognito-identity.amazonaws.com",
  "sub": "REGION:GUID",                // <-- Subject
  "aud": "REGION:IDENTITY_POOL_GUID",  // <-- Audience
  "amr": ["authenticated", "my.dev.provider", "my.dev.provider-<region>:<pool>:<user>"],
  "iat": 1686577419,
  "exp": 1686581019
}
Enter fullscreen mode Exit fullscreen mode

note down the subject and audience. If you don't want to decode the token then you can also obtain the values from observing the newly created identity in the AWS console:


4. Create an Azure AD app registration


For registering an app in enterprise cloud, refer to the official instructions from your enterprise company governance to create this app


5. Configure the Azure AD app to trust the Identity Pool as an OIDC provider

In your app, navigate to Manage > Certificates & secrets
Select Federated credentials and Add credential

for Federated credential scenario select Other issuer
for Issuer, enter https://cognito-identity.amazonaws.com
for type select Explicit subject identifier
for the Value use the use the 'Subject' obtained earlier
Under Credential details, enter a Name and Description of whatever you want
for the Audience use the 'Audience' obtained earlier (the Identity Pool ID)


6. Exchange a JWT from Amazon Cognito with Microsoft for an Azure JWT

That's it! We can now use the client credentials flow with a federated credential.

Lastly, we need to note the tenant & client id's for the App, we can see these in the 'Overview' of our app in Azure portal:

Now let's call Entra's /token endpoint and pass the Cognito ID token as the client_assertion:

Python Code:

from boto3 import client
from urllib3 import PoolManager

DEVELOPER_PROVIDER_NAME = 'my.dev.provider'
DEVELOPER_USER_NAME = 'my_user_id'
IDENTITY_POOL_ID = '<REGION>:<ID>' # get from AWS or pass as env var from cdk

AZURE_APP_TENANT_ID = '<TENANT-ID-FROM-STEP-ABOVE>'
AZURE_APP_CLIENT_ID = '<CLIENT-ID-FROM-STEP-ABOVE>'

cognito_client = client('cognito-identity')
cognito_resp = cognito_client.get_open_id_token_for_developer_identity(
      IdentityPoolId = IDENTITY_POOL_ID ,
      Logins={ DEVELOPER_PROVIDER_ID: DEVELOPER_USER_NAME },
      TokenDuration=120, # s
)

client_assertion_jwt = cognito_resp['Token']

url = f'https://login.microsoftonline.com/{AZURE_APP_TENANT_ID }/oauth2/v2.0/token'
payload = {
      'client_id': AZURE_APP_CLIENT_ID ,
      'scope': 'https://graph.microsoft.com/.default',
      'client_assertion_type': "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
      'client_assertion': client_assertion_jwt ,
      'grant_type': 'client_credentials',
}
headers = {
      'Content-Type': 'application/x-www-form-urlencoded',
}
http = PoolManager()
r = http.request('POST', url, body=urlencode(payload), headers=headers)
data = json.loads(r.data.decode('utf-8'))

if 'access_token' not in data:
    raise Exception(f"access token not in graph response: {data}")

print(data['access_token'])
Enter fullscreen mode Exit fullscreen mode

Test by calling Graph API

curl -sS https://graph.microsoft.com/v1.0/applications?$top=1 \
  -H "Authorization: Bearer <ACCESS_TOKEN>"
Enter fullscreen mode Exit fullscreen mode

(Your app must have the corresponding Application permissions approved - e.g., Application.Read.All for the call above.)

Top comments (0)