Add auxiliary authentication header policy in core pipeline (#25270)
### 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](https://github.com/Azure/azure-sdk-for-go/pull/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](https://github.com/Azure/azure-sdk-for-js/pull/25270#issuecomment-1510855660).
### Reference
Java: https://github.com/Azure/azure-sdk-for-java/pull/14336
Python: https://github.com/Azure/azure-sdk-for-python/pull/24585/
Go: https://github.com/Azure/azure-sdk-for-go/pull/19309
.Net: https://github.com/Azure/azure-sdk-for-net/pull/35097 // Only add
sample, didn't implement in core
---------
Co-authored-by: Jeff Fisher <xirzec@xirzec.com>