.NET · .net-core · AzureFunctions · C# · Entra · Entra · Federation · Logic-App · Logic-App-PowerShell · managed-identity · OAuth 2.0 · Powershell · ServicePrincipal

Multi-Tenant Identity Federation: Accessing Multiple Entra ID Tenants with Managed Identity

Introduction

Recently, I had the opportunity to help a customer solve a complex challenge involving multi-tenant identity federation. They needed to securely access resources across different Azure Active Directory (now Entra ID) tenants without managing secrets or certificates. The solution involved using managed identity with federated credentials to perform token exchange between tenants—a powerful but often misunderstood capability.

After working through this implementation, I realized that while the documentation exists, practical end-to-end examples are scarce. This article documents the complete process, from initial tenant configuration to working implementations using both Azure Functions and Logic Apps, hoping it will help others facing similar challenges (and future me when I inevitably forget the details!).

Setting Up Multi-Tenant App Registration and Service Principal

The foundation of multi-tenant identity federation starts with properly configuring an application registration in your home tenant and creating corresponding service principals in target tenants.

Creating the Multi-Tenant App Registration

First, create a multi-tenant application registration in your home tenant. This app will serve as the bridge between tenants:

# Create multi-tenant app registration
az ad app create \
  --display-name "MOIMHA-MULTI-TENANT-APP" \
  --sign-in-audience "AzureADMultipleOrgs" \
  --required-resource-accesses '[
    {
      "resourceAppId": "00000003-0000-0000-c000-000000000000",
      "resourceAccess": [
        {
          "id": "e1fe6dd8-ba31-4d61-89e7-88639da4683d",
          "type": "Scope"
        }
      ]
    }
  ]'

This creates an app registration with the following key characteristics:

Supported account types: Multiple organizations

Display name: MOIMHA-MULTI-TENANT-APP

Application (client) ID: 7e67e1c7-6902-4007-a012-df5dae6e2a6c

Directory (tenant) ID: abc8cc32-7de9-9f3d-8d79-76375427b610

Configuring Federated Credentials

The magic happens with federated credentials. These allow your managed identity to authenticate as the multi-tenant app without secrets:

# Add federated credential for User-Assigned Managed Identity
az ad app federated-credential create \
  --id xxxxx-xxxxx-xxxxx-xxxx-xxxxxxxx\
  --parameters '{
    "name": "UAMIFederatedCredential",
    "issuer": "https://login.microsoftonline.com/xxxx-xxxx-xxxx-xxx-xxxxx/v2.0",
    "subject": "xxxxxx-xxxxx-xxxx-xxxxx-xxxxxxxx",
    "audiences": ["api://AzureADTokenExchange"]
  }'

Key parameters:

  • Issuer: Your home tenant’s OpenID Connect issuer
  • Subject: The client ID of your User-Assigned Managed Identity
  • Audiences: Must be api://AzureADTokenExchange for token exchange scenarios

Creating Service Principal in Target Tenant

Switch to your target tenant (cloudoven.onmicrosoft.com in this example) and create a service principal:

# Switch to target tenant
Connect-AzAccount -Tenant "cloudoven.onmicrosoft.com"

# Create service principal for the multi-tenant app
New-AzADServicePrincipal -ApplicationId "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"

# Assign necessary permissions (adjust scope and role as needed)
New-AzRoleAssignment \
  -ObjectId "xxxxx-xxxx-xxxxx-xxxx-xxxxxxxxxxx" \
  -RoleDefinitionName "Reader" \
  -Scope "/subscriptions/xxxxxxx-xxxxxxx-xxxxx-xxxxx-xxxxxxxxx"

The service principal created will have:

  • Name: MOIMHA-MULTI-TENANT-APP
  • Application ID: xxxxxxxxx-xxxxx-xxxxx-xxxxx-xxxxxxxxxxx
  • Object ID: xxxxxxx-xxxxx-xxxxx-xxxx-xxxxxxxxxxxxxx
IMPORTANT

You can alternatively create the service principal by visiting the following URI to the Target Entra Tenant. You need to be Administrator for the Target Entra Tenant for this to work:

https://login.microsoftonline.com/TARGET_TENANT.onmicrosoft.com/adminconsent?client_id=8e67e1c7-xxxx-xxxx-xxxx-xxxxxxxxxxxxx

Implementation with Azure Functions

Azure Functions provide an excellent platform for demonstrating multi-tenant token exchange. Here’s how the token flow works in practice:

The Token Exchange Process

The Azure Function implementation follows a clear pattern:

  1. Authenticate with Managed Identity: Use the User-Assigned Managed Identity to get an initial token
  2. Exchange for Target Tenant: Use federated credentials to exchange the token for target tenant access
  3. Access Resources: Use the exchanged token to call Azure APIs in both tenants

Here’s the core implementation from our Function1.cs:

[Function("Function1")]
public async Task<IActionResult> Run([HttpTrigger(AuthorizationLevel.Function, "get", "post")] HttpRequest req)
{
    try
    {
        // Step 1: Get token from home tenant using managed identity
        var credential = new ManagedIdentityCredential(Constants.UserAssignedManagedIdentityClientId);
        var tokenRequestContext = new TokenRequestContext(new[] { Constants.HomeTokenResource });
        var homeAccessToken = await credential.GetTokenAsync(tokenRequestContext);

        _logger.LogInformation("Successfully obtained home tenant access token");

        // Step 2: Exchange token for cloudoven tenant token
        var exchangeToken = await ExchangeTokenForResourceTenant(
            homeAccessToken.Token,
            Constants.MultiTenantAppClientId,
            Constants.ResourceTenantId,
            Constants.TargetResource);

        // Step 3: Get resource groups from both tenants
        var cloudOvenResourceGroups = await AzureResourceManager.GetResourceGroupsFromSubscription(
            _httpClient, _logger, exchangeToken.AccessToken!, Constants.SubscriptionId);

        var homeResourceGroups = await AzureResourceManager.GetHomeResourceGroupsUsingManagedIdentity(
            _httpClient, _logger, Constants.HomeSubscriptionId);

        // Return comprehensive response with data from both tenants
        var response = new
        {
            success = true,
            message = "Successfully completed multi-tenant token exchange",
            cloudOvenResourceGroupsInfo = new
            {
                subscriptionId = Constants.SubscriptionId,
                tenantName = "CloudOven",
                resourceGroupCount = cloudOvenResourceGroups.Count,
                resourceGroups = cloudOvenResourceGroups.Select(rg => new
                {
                    name = rg.Name,
                    location = rg.Location,
                    id = rg.Id
                }).ToList()
            },
            homeResourceGroupsInfo = new
            {
                subscriptionId = Constants.HomeSubscriptionId,
                tenantName = "Home",
                resourceGroupCount = homeResourceGroups.Count,
                resourceGroups = homeResourceGroups.Select(rg => new
                {
                    name = rg.Name,
                    location = rg.Location,
                    id = rg.Id
                }).ToList()
            }
        };

        return new OkObjectResult(response);
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "Error occurred during multi-tenant token exchange");
        return new BadRequestObjectResult(new { success = false, error = ex.Message });
    }
}

Token Exchange Implementation

The token exchange is where the federated credentials come into play:

private async Task<TokenExchangeResponse> ExchangeTokenForResourceTenant(
    string homeToken,
    string clientId,
    string resourceTenantId,
    string targetResource)
{
    var tokenEndpoint = $"https://login.microsoftonline.com/{resourceTenantId}/oauth2/v2.0/token";
    
    var requestBody = new FormUrlEncodedContent(
    [
        new KeyValuePair<string, string>("grant_type", "client_credentials"),
        new KeyValuePair<string, string>("client_id", clientId),
        new KeyValuePair<string, string>("client_assertion_type", "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"),
        new KeyValuePair<string, string>("client_assertion", homeToken),
        new KeyValuePair<string, string>("scope", $"{targetResource}.default")
    ]);

    var response = await _httpClient.PostAsync(tokenEndpoint, requestBody);
    var responseContent = await response.Content.ReadAsStringAsync();

    if (!response.IsSuccessStatusCode)
    {
        throw new InvalidOperationException($"Token exchange failed: {response.StatusCode} - {responseContent}");
    }

    return JsonSerializer.Deserialize<TokenExchangeResponse>(responseContent);
}

Key points in this implementation:

  • client_assertion: The token from your home tenant’s managed identity
  • client_assertion_type: Must be urn:ietf:params:oauth:client-assertion-type:jwt-bearer
  • scope: Target resource with .default suffix for client credentials flow

Implementation with Logic Apps Standard

Logic Apps Standard provides another excellent platform for this scenario, especially when you want to leverage PowerShell scripts. The Logic App workflow can execute PowerShell inline with full access to Azure PowerShell modules.

PowerShell Implementation

Here’s the complete PowerShell script that demonstrates the end-to-end process:

# 1) Sign in using User-Assigned Managed Identity
Connect-AzAccount -Identity -AccountId "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"

# Helper function to list resource group names using bearer token
function Get-ResourceGroupNames {
  param(
    [Parameter(Mandatory=$true)][string]$SubscriptionId,
    [Parameter(Mandatory=$true)][string]$BearerToken
  )

  $apiVersion = "2021-04-01"
  $uri = "https://management.azure.com/subscriptions/$SubscriptionId/resourcegroups?api-version=$apiVersion"
  $headers = @{ Authorization = "Bearer $BearerToken" }

  $names = @()
  do {
    $resp = Invoke-RestMethod -Method GET -Uri $uri -Headers $headers
    if ($resp.value) { $names += ($resp.value | ForEach-Object { $_.name }) }
    $uri = $resp.nextLink
  } while ($uri)

  return $names
}

# 2) Get home-tenant token for AzureADTokenExchange (for federation)
$firstToken = Get-AzAccessToken -ResourceUrl "api://AzureADTokenExchange"

# 3) Exchange token for resource-tenant ARM token (target tenant)
$tenant = "cloudoven.onmicrosoft.com"
$uri = "https://login.microsoftonline.com/$tenant/oauth2/v2.0/token"
$headers = @{ "Content-Type" = "application/x-www-form-urlencoded" }
$body = @{
  client_id             = "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
  client_assertion_type = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer"
  client_assertion      = $firstToken.Token
  grant_type            = "client_credentials"
  scope                 = "https://management.azure.com/.default"
}
$tokenResponse = Invoke-RestMethod -Uri $uri -Method Post -Headers $headers -Body $body
$targetArmToken = $tokenResponse.access_token

# 4) Get resource groups from TARGET tenant subscription using exchanged token
$targetSubscriptionID = "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$targetResourceGroups = Get-ResourceGroupNames -SubscriptionId $targetSubscriptionID -BearerToken $targetArmToken

# 5) Get resource groups from HOME tenant subscription using original MI ARM token
$homeArmToken = (Get-AzAccessToken -ResourceUrl "https://management.azure.com/").Token
$homeSubscriptionID = "xxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx"
$homeResourceGroups = Get-ResourceGroupNames -SubscriptionId $homeSubscriptionID -BearerToken $homeArmToken

# 6) Package and return response
$customResponse = [PSCustomObject]@{
  ExchangedForTenant      = $tenant
  targetSubscriptionID    = $targetSubscriptionID
  TargetResourceGroups    = $targetResourceGroups
  homeSubscriptionID      = $homeSubscriptionID
  HomeResourceGroups      = $homeResourceGroups
}

Push-WorkflowOutput -Output $customResponse

Key Differences in Logic Apps Approach

The Logic Apps implementation has several advantages:

  1. Built-in PowerShell Support: Logic Apps Standard can execute PowerShell scripts with full Azure PowerShell module access
  2. Simplified Token ManagementGet-AzAccessToken handles the complexity of token acquisition
  3. Workflow Orchestration: Easy to build complex workflows with multiple steps and error handling
  4. Visual Designer: Non-developers can understand and modify the workflow
Setting Up Logic Apps for PowerShell

To run PowerShell scripts in Logic Apps Standard, you need to ensure the Azure PowerShell modules are available. This is configured through a requirements.psd1 file:

@{
    'Az' = '12.*'
}

This file tells Logic Apps which PowerShell modules to load, enabling the use of commands like Connect-AzAccount and Get-AzAccessToken.

Architecture Comparison

Both implementations achieve the same goal but with different approaches:

AspectAzure FunctionsLogic Apps Standard
LanguageC# (strongly typed)PowerShell (script-based)
Token ManagementManual HTTP callsBuilt-in Azure PowerShell
Error HandlingTry-catch blocksWorkflow error handling
DebuggingApplication InsightsLogic Apps run history
DeploymentCode deploymentWorkflow files
ScalingConsumption/PremiumStandard App Service Plan

Conclusion

Multi-tenant identity federation using managed identity and federated credentials provides a powerful, secure way to access resources across different Entra ID tenants without managing secrets. The key components—proper app registration configuration, federated credentials setup, and service principal creation in target tenants—work together to enable seamless token exchange.

Both Azure Functions and Logic Apps Standard offer viable implementation paths, each with their own strengths. Azure Functions provide strongly-typed, programmatic control, while Logic Apps Standard offers a more accessible, workflow-based approach with excellent PowerShell integration.

I recently had to help a customer implement exactly this scenario, and the complexity of getting all the pieces right motivated me to write this comprehensive guide. I hope this article saves others (and future me) the time and frustration of piecing together documentation from multiple sources. The complete working examples should provide a solid foundation for implementing multi-tenant identity federation in your own environment.

Remember: always test thoroughly in non-production environments, follow the principle of least privilege when assigning permissions, and ensure your implementation meets your organization’s security and compliance requirements.

GitHub Repo

Thanks for reading!

Leave a comment