Skip to content

Support ARM cross-tenant authentication#19309

Merged
chlowell merged 1 commit intoAzure:feature/multitenant-authfrom
chlowell:cross-tenant-core
Jan 6, 2023
Merged

Support ARM cross-tenant authentication#19309
chlowell merged 1 commit intoAzure:feature/multitenant-authfrom
chlowell:cross-tenant-core

Conversation

@chlowell
Copy link
Copy Markdown
Member

@chlowell chlowell commented Oct 7, 2022

ARM cross-tenant auth needs support from azcore and azidentity to function end to end (see ARM docs for a rundown of how this feature works). This PR is the azcore part, with new API for specifying auxiliary tenants for ARM clients and requesting access tokens from arbitrary tenants. #19529 has the azidentity part--it depends on this PR and the next MSAL release.

Usage will look like this:

// cred will request tokens from the primary tenant unless another is specified in GetToken args
cred, err := azidentity.NewClientSecretCredential("primary-tenant", "client-id", "secret", &azidentity.ClientSecretCredentialOptions{
	// cred will (attempt to) authenticate in any requested tenant; can also list specific tenants here
	AdditionallyAllowedTenants: []string{"*"},
})
client, err := armresources.NewClient("subscription", cred, &arm.ClientOptions{
	// client will add a token from each auxiliary tenant to every request's x-ms-authorization-auxiliary header
	AuxiliaryTenants: []string{"aux-tenant"},
})

@chlowell chlowell added Client This issue points to a problem in the data-plane of the library. Mgmt This issue is related to a management-plane library. Azure.Core labels Oct 7, 2022
@jhendrixMSFT
Copy link
Copy Markdown
Member

In the example, is the "*" a well-known concept for MSAL, or is this something we're inventing?

@chlowell
Copy link
Copy Markdown
Member Author

It's our own invention. MSAL doesn't have a similar allow list because its multitenant API is always explicit.

@jhendrixMSFT
Copy link
Copy Markdown
Member

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?

@chlowell
Copy link
Copy Markdown
Member Author

Should our API be explicit too (it's what we did in track 1)?

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.

Are there any downsides/risks with using a wildcard for the additional tenants?

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.

@jhendrixMSFT
Copy link
Copy Markdown
Member

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.

@chlowell chlowell changed the base branch from main to feature/multitenant-auth October 13, 2022 23:19
@chlowell
Copy link
Copy Markdown
Member Author

Retargeted this to a feature branch so we can review and merge it without interfering with other features entering main.

@chlowell chlowell marked this pull request as ready for review October 13, 2022 23:28
@chlowell chlowell requested a review from RickWinter as a code owner October 13, 2022 23:28
@chlowell chlowell marked this pull request as draft October 31, 2022 22:19
@chlowell chlowell marked this pull request as ready for review November 9, 2022 19:10
@MartinForReal
Copy link
Copy Markdown
Contributor

Is there any update on this pr? Thanks in advance!

@jhendrixMSFT
Copy link
Copy Markdown
Member

@chlowell what's the trigger for getting this into main for a beta?

@chlowell
Copy link
Copy Markdown
Member Author

chlowell commented Jan 6, 2023

We need the azidentity part of this (#19529) to complete the end-to-end feature, but that's waiting on the next version of MSAL, which has been delayed by bugs and holidays. @MartinForReal if you need a workaround in the meantime, you can try this: #19726 (comment)

@jhendrixMSFT are you thinking of an azcore beta?

@jhendrixMSFT
Copy link
Copy Markdown
Member

I couldn't remember if the MSAL work was complete. Clearly, we need that before we can release a beta of this feature.

@chlowell chlowell merged commit a3ce225 into Azure:feature/multitenant-auth Jan 6, 2023
@chlowell chlowell deleted the cross-tenant-core branch January 6, 2023 17:03
MaryGao added a commit to Azure/azure-sdk-for-js that referenced this pull request May 10, 2023
### 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>
minhanh-phan pushed a commit to minhanh-phan/azure-sdk-for-js that referenced this pull request Jun 12, 2023
)

### 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
&lt;primary-token&gt; |
| x-ms-authorization-auxiliary | Auxiliary tokens, token got from
credentialA | Bearer &lt;auxiliary-token1&gt; |

```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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Azure.Core Client This issue points to a problem in the data-plane of the library. Mgmt This issue is related to a management-plane library.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants