Support ARM cross-tenant authentication#19309
Support ARM cross-tenant authentication#19309chlowell merged 1 commit intoAzure:feature/multitenant-authfrom
Conversation
|
In the example, is the |
|
It's our own invention. MSAL doesn't have a similar allow list because its multitenant API is always explicit. |
|
Should our API be explicit too (it's what we did in track 1)? Are there any downsides/risks with using a wildcard for the additional tenants? |
We can't (well, not without major surgery) make the tenant explicit to the developer in every token request because an application using an SDK client doesn't request tokens directly. The client does that as needed, and the developer doesn't always know which client call will trigger a token request or which tenant a request should go to.
Yes, there's some risk for applications that accept URIs from users (some more on this in the recent blog post). We need a wildcard escape hatch anyway because some applications can't know in advance all the tenants whose resources they will want to access. |
|
I meant explicit in regard to only allowing a list of tenants, no wildcard (this is what we did in track 1). Regardless, it sounds like the wildcard is needed to support some scenarios. |
|
Retargeted this to a feature branch so we can review and merge it without interfering with other features entering |
38ee5d5 to
6565eca
Compare
3703893 to
3bf0399
Compare
|
Is there any update on this pr? Thanks in advance! |
|
@chlowell what's the trigger for getting this into |
|
We need the @jhendrixMSFT are you thinking of an |
|
I couldn't remember if the MSAL work was complete. Clearly, we need that before we can release a beta of this feature. |
### Background Add a policy for external tokens to `x-ms-authorization-auxiliary` header in core lib. This header will be used when creating a cross-tenant application we may need to handle authentication requests for resources that are in different tenants. You can learn [more here](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/authenticate-multi-tenant). Here I collect two use cases: - Create a virtual network peering between virtual networks across tenants ([see here](https://learn.microsoft.com/en-us/azure/virtual-network/create-peering-different-subscriptions?tabs=create-peering-portal#cli)) - Share images across tenants ([see here](https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/share-images-across-tenants)) ### Usecase - create a virtual network peering across tenants We have two subscriptions cross two tenants: ``` subscriptionA = "75d6dc7b-9a8d-4f94-81ce-8a9437f3ce2c" in tenantA subscriptionB = "92f95d8f-3c67-4124-91c7-8cf07cdbf241" in tenantB ``` Prepare the app register and grant permission in both subscriptions, please note we'll have one app register with two service principals in two tenants. ``` # Create app registration named `appRegisterB` which allows to be used in any orgnaizational directory located in `tenantB` # Create a service principal for `appRegisterB` in `tenantA` by login url: [https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id={client-id}](https://login.microsoftonline.com/%7Btenant-id%7D/adminconsent?client_id=%7Bclient-id%7D) # Add roles for `appRegisterB` in both `subscriptionA` and `subscriptionB` ``` Prepare the virtual network in both subscriptions: ``` # Switch to subscription A az account set -s $subscriptionA # Create resource group A az group create --name myResourceGroupA --location eastus # Create virtual network A az network vnet create --name myVnetA --resource-group myResourceGroupA --location eastus --address-prefix 10.0.0.0/16 # Switch to subscription B az account set -s $subscriptionB # Create resource group B az group create --name myResourceGroupB --location eastus # Create virtual network B az network vnet create --name myVnetB --resource-group myResourceGroupB --location eastus --address-prefix 10.1.0.0/16 ``` Create a virtual network peering between these two virtual networks. We could build this peer relationship from myVnetA to myVnetB, or from myVnetB to myVnetA. If we build a client under subscriptionB then we could create this peer from myVnetB to myVnetA with below headers: | Header name | Description | Example value | | ----------- | ----------- | ------------ | | Authorization | Primary token, token got from credentialB | Bearer <primary-token> | | x-ms-authorization-auxiliary | Auxiliary tokens, token got from credentialA | Bearer <auxiliary-token1> | ```typescript const tenantA = "c029c2bd-5f77-48fd-b9b8-6dbc7c475125"; const tenantB = "72f988bf-86f1-41af-91ab-2d7cd011db47"; const subscriptionB = "92f95d8f-3c67-4124-91c7-8cf07cdbf241"; const myResourceGroupB = "myResourceGroupB"; const myVnetB = "myVnetB"; const virtualNetworkPeeringName = "myVnetA"; const virtualNetworkPeeringParameters: VirtualNetworkPeering = { allowForwardedTraffic: false, allowGatewayTransit: false, allowVirtualNetworkAccess: true, remoteVirtualNetwork: { id: "/subscriptions/75d6dc7b-9a8d-4f94-81ce-8a9437f3ce2c/resourceGroups/myResourceGroupA/providers/Microsoft.Network/virtualNetworks/myVnetA" }, useRemoteGateways: false }; ``` ### [Preferred] Option 1: Provide an extra policy `auxiliaryAuthenticationHeaderPolicy` Provide a new policy `auxiliaryAuthenticationHeaderPolicy` in core, then customer code could leverage that policy to add auxilary header. ```typescript async function createPeeringWithPolicy() { const credentialA = new DefaultAzureCredential({tenantId: tenantA}); const credentialB = new DefaultAzureCredential({tenantId: tenantB}); const client = new NetworkManagementClient(credentialB, subscriptionB, { // Add the extra policy when building client additionalPolicies: [{ policy: auxiliaryAuthenticationHeaderPolicy({ credentials: [credentialA], scopes: "https://management.core.windows.net//.default" }), position: "perRetry", }] }); const result = await client.virtualNetworkPeerings.beginCreateOrUpdateAndWait( myResourceGroupB, myVnetB, virtualNetworkPeeringName, virtualNetworkPeeringParameters, ); console.log(result); } ``` ### Option 2: Add `auxiliaryTenants` as a client option Similar the implementation in [Go](Azure/azure-sdk-for-go#19309), we could provide an option in `CommonClientOptions`. ```typescript /** * Auxiliary tenant ids which will be used to get token from */ auxiliaryTenants?: string[]; ``` And then enhance the current bearerTokenAuthenticationPolicy logic to detect if we have the `auxiliaryTenants` provided, if yes we could automatically get tokens and add `x-ms-authorization-auxiliary` header in request. And the customer code would be like: ```typescript async function createPeeringWithParam() { const credential = new ClientSecretCredential(tenantB, env.clientB, env.secretB, { // We would also add allowed tenant list into current credential so that we could get relevant tenant tokens additionallyAllowedTenants: [tenantA] }); const client = new NetworkManagementClient(credential, subscriptionB, { // If the parameter is provided the bearer policy would append the extra header auxiliaryTenants: [tenantA] }); const result = await client.virtualNetworkPeerings.beginCreateOrUpdateAndWait( myResourceGroupB, myVnetB, virtualNetworkPeeringName, virtualNetworkPeeringParameters ); console.log(result); } ``` ### Option 3: Add `auxiliaryCredentials` option in BearerTokenAuthenticationPolicyOptions Instead of providing new policy we could add a new option in `BearerTokenAuthenticationPolicyOptions` in original bearerTokenAuthenticationPolicy. Then in that policy we could detect if the parameter `auxiliaryCredentials` is provided, if yes append the header accordingly. ```typescript /** * Provide the auxiliary credentials to get tokens in header x-ms-authorization-auxiliary */ auxiliaryCredentials: TokenCredential[]; ``` But it would be more complex from customer side, because we add bearer policy by default so we have to remove that one first and then re-add a new one. ```typescript async function createPeeringWithNewBearerPolicy() { const credentialA = new ClientSecretCredential(tenantA, clientB, secretB); const credentialB = new ClientSecretCredential(tenantB, clientB, secretB); const client = new NetworkManagementClient(credentialB, subscriptionB); // Build a new policy with auxiliaryCredentials provide const customizedBearerPolicy = bearerTokenAuthenticationPolicy({ credential: credentialB, scopes: "https://management.core.windows.net//.default", auxiliaryCredentials: [credentialA] }); // Remove the original one client.pipeline.removePolicy({ name: bearerTokenAuthenticationPolicyName }); // Add our new policy client.pipeline.addPolicy(customizedBearerPolicy); const result = await client.virtualNetworkPeerings.beginCreateOrUpdateAndWait( myResourceGroupB, myVnetB, virtualNetworkPeeringName, virtualNetworkPeeringParameters ); console.log(result); } ``` Simply speaking I prefer the option 1, you could know more [here](#25270 (comment)). ### Reference Java: Azure/azure-sdk-for-java#14336 Python: Azure/azure-sdk-for-python#24585 Go: Azure/azure-sdk-for-go#19309 .Net: Azure/azure-sdk-for-net#35097 // Only add sample, didn't implement in core --------- Co-authored-by: Jeff Fisher <xirzec@xirzec.com>
) ### Background Add a policy for external tokens to `x-ms-authorization-auxiliary` header in core lib. This header will be used when creating a cross-tenant application we may need to handle authentication requests for resources that are in different tenants. You can learn [more here](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/authenticate-multi-tenant). Here I collect two use cases: - Create a virtual network peering between virtual networks across tenants ([see here](https://learn.microsoft.com/en-us/azure/virtual-network/create-peering-different-subscriptions?tabs=create-peering-portal#cli)) - Share images across tenants ([see here](https://learn.microsoft.com/en-us/azure/virtual-machine-scale-sets/share-images-across-tenants)) ### Usecase - create a virtual network peering across tenants We have two subscriptions cross two tenants: ``` subscriptionA = "75d6dc7b-9a8d-4f94-81ce-8a9437f3ce2c" in tenantA subscriptionB = "92f95d8f-3c67-4124-91c7-8cf07cdbf241" in tenantB ``` Prepare the app register and grant permission in both subscriptions, please note we'll have one app register with two service principals in two tenants. ``` # Create app registration named `appRegisterB` which allows to be used in any orgnaizational directory located in `tenantB` # Create a service principal for `appRegisterB` in `tenantA` by login url: [https://login.microsoftonline.com/{tenant-id}/adminconsent?client_id={client-id}](https://login.microsoftonline.com/%7Btenant-id%7D/adminconsent?client_id=%7Bclient-id%7D) # Add roles for `appRegisterB` in both `subscriptionA` and `subscriptionB` ``` Prepare the virtual network in both subscriptions: ``` # Switch to subscription A az account set -s $subscriptionA # Create resource group A az group create --name myResourceGroupA --location eastus # Create virtual network A az network vnet create --name myVnetA --resource-group myResourceGroupA --location eastus --address-prefix 10.0.0.0/16 # Switch to subscription B az account set -s $subscriptionB # Create resource group B az group create --name myResourceGroupB --location eastus # Create virtual network B az network vnet create --name myVnetB --resource-group myResourceGroupB --location eastus --address-prefix 10.1.0.0/16 ``` Create a virtual network peering between these two virtual networks. We could build this peer relationship from myVnetA to myVnetB, or from myVnetB to myVnetA. If we build a client under subscriptionB then we could create this peer from myVnetB to myVnetA with below headers: | Header name | Description | Example value | | ----------- | ----------- | ------------ | | Authorization | Primary token, token got from credentialB | Bearer <primary-token> | | x-ms-authorization-auxiliary | Auxiliary tokens, token got from credentialA | Bearer <auxiliary-token1> | ```typescript const tenantA = "c029c2bd-5f77-48fd-b9b8-6dbc7c475125"; const tenantB = "72f988bf-86f1-41af-91ab-2d7cd011db47"; const subscriptionB = "92f95d8f-3c67-4124-91c7-8cf07cdbf241"; const myResourceGroupB = "myResourceGroupB"; const myVnetB = "myVnetB"; const virtualNetworkPeeringName = "myVnetA"; const virtualNetworkPeeringParameters: VirtualNetworkPeering = { allowForwardedTraffic: false, allowGatewayTransit: false, allowVirtualNetworkAccess: true, remoteVirtualNetwork: { id: "/subscriptions/75d6dc7b-9a8d-4f94-81ce-8a9437f3ce2c/resourceGroups/myResourceGroupA/providers/Microsoft.Network/virtualNetworks/myVnetA" }, useRemoteGateways: false }; ``` ### [Preferred] Option 1: Provide an extra policy `auxiliaryAuthenticationHeaderPolicy` Provide a new policy `auxiliaryAuthenticationHeaderPolicy` in core, then customer code could leverage that policy to add auxilary header. ```typescript async function createPeeringWithPolicy() { const credentialA = new DefaultAzureCredential({tenantId: tenantA}); const credentialB = new DefaultAzureCredential({tenantId: tenantB}); const client = new NetworkManagementClient(credentialB, subscriptionB, { // Add the extra policy when building client additionalPolicies: [{ policy: auxiliaryAuthenticationHeaderPolicy({ credentials: [credentialA], scopes: "https://management.core.windows.net//.default" }), position: "perRetry", }] }); const result = await client.virtualNetworkPeerings.beginCreateOrUpdateAndWait( myResourceGroupB, myVnetB, virtualNetworkPeeringName, virtualNetworkPeeringParameters, ); console.log(result); } ``` ### Option 2: Add `auxiliaryTenants` as a client option Similar the implementation in [Go](Azure/azure-sdk-for-go#19309), we could provide an option in `CommonClientOptions`. ```typescript /** * Auxiliary tenant ids which will be used to get token from */ auxiliaryTenants?: string[]; ``` And then enhance the current bearerTokenAuthenticationPolicy logic to detect if we have the `auxiliaryTenants` provided, if yes we could automatically get tokens and add `x-ms-authorization-auxiliary` header in request. And the customer code would be like: ```typescript async function createPeeringWithParam() { const credential = new ClientSecretCredential(tenantB, env.clientB, env.secretB, { // We would also add allowed tenant list into current credential so that we could get relevant tenant tokens additionallyAllowedTenants: [tenantA] }); const client = new NetworkManagementClient(credential, subscriptionB, { // If the parameter is provided the bearer policy would append the extra header auxiliaryTenants: [tenantA] }); const result = await client.virtualNetworkPeerings.beginCreateOrUpdateAndWait( myResourceGroupB, myVnetB, virtualNetworkPeeringName, virtualNetworkPeeringParameters ); console.log(result); } ``` ### Option 3: Add `auxiliaryCredentials` option in BearerTokenAuthenticationPolicyOptions Instead of providing new policy we could add a new option in `BearerTokenAuthenticationPolicyOptions` in original bearerTokenAuthenticationPolicy. Then in that policy we could detect if the parameter `auxiliaryCredentials` is provided, if yes append the header accordingly. ```typescript /** * Provide the auxiliary credentials to get tokens in header x-ms-authorization-auxiliary */ auxiliaryCredentials: TokenCredential[]; ``` But it would be more complex from customer side, because we add bearer policy by default so we have to remove that one first and then re-add a new one. ```typescript async function createPeeringWithNewBearerPolicy() { const credentialA = new ClientSecretCredential(tenantA, clientB, secretB); const credentialB = new ClientSecretCredential(tenantB, clientB, secretB); const client = new NetworkManagementClient(credentialB, subscriptionB); // Build a new policy with auxiliaryCredentials provide const customizedBearerPolicy = bearerTokenAuthenticationPolicy({ credential: credentialB, scopes: "https://management.core.windows.net//.default", auxiliaryCredentials: [credentialA] }); // Remove the original one client.pipeline.removePolicy({ name: bearerTokenAuthenticationPolicyName }); // Add our new policy client.pipeline.addPolicy(customizedBearerPolicy); const result = await client.virtualNetworkPeerings.beginCreateOrUpdateAndWait( myResourceGroupB, myVnetB, virtualNetworkPeeringName, virtualNetworkPeeringParameters ); console.log(result); } ``` Simply speaking I prefer the option 1, you could know more [here](Azure#25270 (comment)). ### Reference Java: Azure/azure-sdk-for-java#14336 Python: Azure/azure-sdk-for-python#24585 Go: Azure/azure-sdk-for-go#19309 .Net: Azure/azure-sdk-for-net#35097 // Only add sample, didn't implement in core --------- Co-authored-by: Jeff Fisher <xirzec@xirzec.com>
ARM cross-tenant auth needs support from
azcoreandazidentityto function end to end (see ARM docs for a rundown of how this feature works). This PR is theazcorepart, with new API for specifying auxiliary tenants for ARM clients and requesting access tokens from arbitrary tenants. #19529 has theazidentitypart--it depends on this PR and the next MSAL release.Usage will look like this: