Managed Identity access across tenants
Posted on: December 31, 2024When I saw post Effortlessly access cloud resources across Azure tenants without using secrets I immediately wanted to take it for a spin. For background information, please check out the article first.
Jumping directly to my demo architecture:
As you can see from my diagram, I have my two favorite companies Contoso and Litware wanting to collaborate across their environments. Contoso provides some services for Litware and needs Azure access to do that. Obviously, Litware wants to control that access and they’ve now agreed to use the above setup for access management solution.
Let’s start the setup from Contoso side first.
Contoso
Contoso starts their setup by creating managed identity and app registration. Here’s the Bicep of that setup (this is slightly modified version of the article’s example):
extension microsoftGraphV1
param location string = resourceGroup().location
resource managedIdentity 'Microsoft.ManagedIdentity/userAssignedIdentities@2023-01-31' = {
name: 'umi-multi-tenant-example'
location: location
}
resource myApp 'Microsoft.Graph/applications@v1.0' = {
displayName: 'Multi-Tenant Example App'
uniqueName: 'my-multi-tenant-application'
signInAudience: 'AzureADMultipleOrgs'
resource myMsiFic 'federatedIdentityCredentials@v1.0' = {
name: 'my-multi-tenant-application/${managedIdentity.name}'
description: 'Federated Identity Credentials for Managed Identity'
audiences: [
'api://AzureADTokenExchange'
]
issuer: '${environment().authentication.loginEndpoint}${tenant().tenantId}/v2.0'
subject: managedIdentity.properties.principalId
}
}
The above uses Bicep templates for Microsoft Graph for creating app registration to Entra ID.
Couple of important parts from the above:
AzureADMultipleOrgs
as the signInAudience
value means that it’s multi-tenant app
and can be used by other tenants as well.
api://AzureADTokenExchange
is the value for audiences
that can appear in the external token for Microsoft Entra ID.
For more information read
Overview of federated identity credentials in Microsoft Entra ID.
Here is the managed identity that was created:
Here is the app registration that got created:
It’s configured to be multitenant:
And finally, here are the Federated credentials:
Here are the configuration details of the Federated credentials:
Important parts from the above Federated credentials:
Issuer
is set to be tenant f96...d2f
(Contoso).
Subject
is set to be managed identity 02d...57d
.
It matches the managed identity Object (principal) ID:
To test this out, I’ll just create Virtual Machine and assign that managed identity to it:
Before I jump into code, I’ll give these links for you to study for more background information on how managed identities and federate credentials work:
How managed identities for Azure resources work with Azure virtual machines
Access token request with a federated credential
Configure an application to trust a managed identity
Okay, now we’re ready to execute Raw HTTP Requests and study how this setup works. Obviously, in real applications you would use higher-level libraries and SDKs for handling all of this, but I want this to be as low-level as possible so that you would understand what happens in those libraries.
For this demo setup I’m using extremely powerful combo: VS Code, Remote Development using SSH, and REST Client extension.
To connect to a remote machine, just use the Remote-SSH commands:
The bottom left corner shows that you’re connected to the remote:
Now we’re ready with VS Code setup:
Since I’m now directly connected to my Azure VM, I can start executing HTTP requests directly from the editor.
First, we’ll fetch the managed identity token for resource api://AzureADTokenExchange
:
###
# @name miTokenResponse
GET http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=api://AzureADTokenExchange
Metadata: true
Here’s the output:
{
"access_token": "eyJ0...1Q",
"client_id": "f31...d43",
"expires_in": "86400",
"expires_on": "1735625546",
"ext_expires_in": "86399",
"not_before": "1735538846",
"resource": "api://AzureADTokenExchange",
"token_type": "Bearer"
}
I’ll copy the received access token to https://jwt.ms for analysis:
Here is the abbreviated version of the content:
{
"aud": "fb60f99c-7a34-4190-8149-302f77469936",
"iss": "https://login.microsoftonline.com/f96...0d2f/v2.0",
"azp": "f31...d43",
"azpacr": "2",
"idtyp": "app",
"oid": "02d...57d",
"sub": "02d...57d",
"tid": "f96...d2f",
}
From the above token everything else is as expected
except aud
(audience) with value fb60f99c-7a34-4190-8149-302f77469936
.
That value happens to be AAD Token Exchange Endpoint application so it’s synonymous to api://AzureADTokenExchange
(that application, as many other applications, is from Microsoft Service’s Microsoft Entra tenant ID):
Since we happen to have this setup ready in our Contoso tenant,
we can try to use that directly against our home tenant.
Let’s try to get a token for https://storage.azure.com/.default
scope:
### Get Storage token to Contoso tenant
# @name entraTokenResponse
POST https://login.microsoftonline.com/{{home_tenant_id}}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
scope=https://storage.azure.com/.default
&client_id={{home_client_id}}
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={{miTokenResponse.response.body.access_token}}
&grant_type=client_credentials
Here’s the output:
{
"token_type": "Bearer",
"expires_in": 3599,
"ext_expires_in": 3599,
"access_token": "eyJ...c1g"
}
This is the token received from the above:
Here is the abbreviated version of the content:
{
"aud": "https://storage.azure.com",
"iss": "https://login.microsoftonline.com/f96...0d2f/v2.0",
"appid": "d70...c6f",
"appidacr": "2",
"idp": "https://sts.windows.net/f96...d2f/",
"idtyp": "app",
"tid": "f96...d2f"
}
So, we have now tested that we can use the federated credentials for acquiring access tokens in our Contoso tenant.
If you paid attention in the above app registration view, then you noticed that we didn’t create service principal into our home tenant since we don’t plan use it for e.g., granting access. Here is an another Multi-Tenant Example App 2 application which has a service principal created and that can be used in role assignments:
The above setup was pretty clear and now we’re able to continue the collaboration with Litware and we will share details about our app with them.
Litware
You might at first think that the setup starts by creating a new App Registration at the Litware tenant with same values as in the above Contoso setup for Issuer and Subject:
Issuer
is set to be tenant of Contoso: f96...d2f
.
Subject
is set to be identifier of the managed identity in the Contoso tenant: 02d...57d
.
If you would do the above, and share your newly created
Application (client) ID target_client_id
and
Directory (tenant) ID target_tenant_id
and
ask them to test at Contoso, then you would run into error:
### Get Storage token to Litware tenant
# @name entraStorageTokenResponse
POST https://login.microsoftonline.com/{{target_tenant_id}}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
scope=https://storage.azure.com/.default
&client_id={{target_client_id}}
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={{miTokenResponse.response.body.access_token}}
&grant_type=client_credentials
The request would fail with the following error message:
AADSTS700236: Entra ID tokens issued by issuer ‘https://login.microsoftonline.com/f96…d2f/v2.0’ may not be used for federated identity credential flows for applications or managed identities registered in this tenant.
Connecting this federation via a new app registration is not the way to go. Let’s review documentation about How and why applications are added to Microsoft Entra ID:
A reference back to an application object through the application ID property
…
An application has one application object in its home directory that is referenced by one or more service principals in each of the directories where it operates (including the application’s home directory).
So, we need to provision the app into the Litware tenant. Read more about Grant tenant-wide admin consent to an application.
In this scenario, Litware admins use New-AzADServicePrincipal to create a new service principal with existing application identifier:
New-AzADServicePrincipal -ApplicationId "d70...c6f"
They can use Azure Cloud Shell for that:
The application identifier used in the above ("d70...c6f"
) matches Application (client) ID
of the multi-tenant app in the Contoso tenant:
Remember that the above command works only if Contoso has remembered to create their application as multitenant app.
After the command has successfully finished, Litware admins can find this application in their Enterprise apps view:
Let’s now repeat the previous test again with updated application identifier target_client_id
in Contoso environment:
### Get Storage token to Litware tenant
# @name entraStorageTokenResponse
POST https://login.microsoftonline.com/{{target_tenant_id}}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
scope=https://storage.azure.com/.default
&client_id={{target_client_id}}
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={{miTokenResponse.response.body.access_token}}
&grant_type=client_credentials
Here’s the output:
{
"token_type": "Bearer",
"expires_in": 3598,
"ext_expires_in": 3598,
"access_token": "eyJ...WfQ"
}
Here is the abbreviated version of the content:
{
"aud": "https://storage.azure.com",
"iss": "https://sts.windows.net/eac...691/",
"appid": "d70...c6f",
"appidacr": "2",
"idp": "https://sts.windows.net/eac...691/",
"idtyp": "app",
"oid": "9b5...477",
"sub": "9b5...477",
"tid": "eac...691"
}
From the above token, we can see that identifiers have been changed to match Litware environment specific identifiers, and everything is as expected.
Litware admins can now proceed to grant required access to that application to their Azure subscriptions so that Contoso can do their required actions on those resources.
E.g., Reader
access to NetworkWatcherRG
resource group:
Now Contoso can change their code to request token for managing Azure (scope https://management.azure.com/.default
):
### Get ARM token to Litware tenant
# @name entraManagementTokenResponse
POST https://login.microsoftonline.com/{{target_tenant_id}}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
scope=https://management.azure.com/.default
&client_id={{target_client_id}}
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={{miTokenResponse.response.body.access_token}}
&grant_type=client_credentials
Here’s the output:
{
"token_type": "Bearer",
"expires_in": 3598,
"ext_expires_in": 3598,
"access_token": "eyJ...WfQ"
}
Here is the abbreviated version of the content:
{
"aud": "https://management.azure.com",
"iss": "https://sts.windows.net/eac...691/",
"appid": "d70...c6f",
"appidacr": "2",
"idp": "https://sts.windows.net/eac...691/",
"idtyp": "app",
"oid": "9b5...477",
"sub": "9b5...477",
"tid": "eac...691"
}
That token can be used to call Azure Rest APIs to e.g., list resource groups:
### Query resource groups from Litware subscription
GET https://management.azure.com/subscriptions/{{target_subscription_id}}/resourceGroups?api-version=2024-08-01
Content-Type: application/json
Authorization: Bearer {{entraManagementTokenResponse.response.body.access_token}}
Here’s the output:
{
"value": [
{
"id": "/subscriptions/04d...824/resourceGroups/NetworkWatcherRG",
"name": "NetworkWatcherRG",
"type": "Microsoft.Resources/resourceGroups",
"location": "northeurope",
"properties": {
"provisioningState": "Succeeded"
}
}
]
}
Hurray! We have now verified that Contoso can access Litware resources without sharing any secrets.
Here’s our updated architecture diagram to illustrate the above:
Code
Here is the full code snippet used in the above examples:
@home_tenant_id = {{$dotenv home_tenant_id}}
@home_client_id = {{$dotenv home_client_id}}
@target_tenant_id = {{$dotenv target_tenant_id}}
@target_client_id = {{$dotenv target_client_id}}
@target_subscription_id = {{$dotenv target_subscription_id}}
###
# @name miTokenResponse
GET http://169.254.169.254/metadata/identity/oauth2/token?api-version=2018-02-01&resource=api://AzureADTokenExchange
Metadata: true
### Get Storage token to Contoso tenant
# @name entraTokenResponse
POST https://login.microsoftonline.com/{{home_tenant_id}}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
scope=https://storage.azure.com/.default
&client_id={{home_client_id}}
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={{miTokenResponse.response.body.access_token}}
&grant_type=client_credentials
### Get Storage token to Litware tenant
# @name entraStorageTokenResponse
POST https://login.microsoftonline.com/{{target_tenant_id}}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
scope=https://storage.azure.com/.default
&client_id={{target_client_id}}
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={{miTokenResponse.response.body.access_token}}
&grant_type=client_credentials
### Get ARM token to Litware tenant
# @name entraManagementTokenResponse
POST https://login.microsoftonline.com/{{target_tenant_id}}/oauth2/v2.0/token
Content-Type: application/x-www-form-urlencoded
scope=https://management.azure.com/.default
&client_id={{target_client_id}}
&client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer
&client_assertion={{miTokenResponse.response.body.access_token}}
&grant_type=client_credentials
### Query resource groups from Litware subscription
GET https://management.azure.com/subscriptions/{{target_subscription_id}}/resourceGroups?api-version=2024-08-01
Content-Type: application/json
Authorization: Bearer {{entraManagementTokenResponse.response.body.access_token}}
You can find all the above code examples in my GitHub repo:
Conclusion
This new capability announced at the post Effortlessly access cloud resources across Azure tenants without using secrets is powerful and will be most likely used in various automations and deployments across tenants.
Of course, monitoring and managing these federated identity credentials is natural next step. Maybe that’s a topic for future blog posts.
I hope you find this useful!