Skip to content

Commit

Permalink
Add auxiliary authentication header policy in core pipeline (#25270)
Browse files Browse the repository at this point in the history
### 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 <[email protected]>
  • Loading branch information
MaryGao and xirzec authored May 10, 2023
1 parent f61d4d5 commit b69246d
Show file tree
Hide file tree
Showing 6 changed files with 418 additions and 2 deletions.
4 changes: 3 additions & 1 deletion sdk/core/core-rest-pipeline/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
# Release History

## 1.10.4 (Unreleased)
## 1.11.0 (Unreleased)

### Features Added

- Add a policy `auxiliaryAuthenticationHeaderPolicy` for external tokens to `x-ms-authorization-auxiliary` header. 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. [PR #25270](https://github.com/Azure/azure-sdk-for-js/pull/25270)

### Breaking Changes

### Bugs Fixed
Expand Down
2 changes: 1 addition & 1 deletion sdk/core/core-rest-pipeline/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@azure/core-rest-pipeline",
"version": "1.10.4",
"version": "1.11.0",
"description": "Isomorphic client library for making HTTP requests in node.js and browser.",
"sdk-type": "client",
"main": "dist/index.js",
Expand Down
13 changes: 13 additions & 0 deletions sdk/core/core-rest-pipeline/review/core-rest-pipeline.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,19 @@ export interface AuthorizeRequestOptions {
scopes: string[];
}

// @public
export function auxiliaryAuthenticationHeaderPolicy(options: AuxiliaryAuthenticationHeaderPolicyOptions): PipelinePolicy;

// @public
export const auxiliaryAuthenticationHeaderPolicyName = "auxiliaryAuthenticationHeaderPolicy";

// @public
export interface AuxiliaryAuthenticationHeaderPolicyOptions {
credentials?: TokenCredential[];
logger?: AzureLogger;
scopes: string | string[];
}

// @public
export function bearerTokenAuthenticationPolicy(options: BearerTokenAuthenticationPolicyOptions): PipelinePolicy;

Expand Down
5 changes: 5 additions & 0 deletions sdk/core/core-rest-pipeline/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -87,3 +87,8 @@ export {
AuthorizeRequestOnChallengeOptions,
} from "./policies/bearerTokenAuthenticationPolicy";
export { ndJsonPolicy, ndJsonPolicyName } from "./policies/ndJsonPolicy";
export {
auxiliaryAuthenticationHeaderPolicy,
AuxiliaryAuthenticationHeaderPolicyOptions,
auxiliaryAuthenticationHeaderPolicyName,
} from "./policies/auxiliaryAuthenticationHeaderPolicy";
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { GetTokenOptions, TokenCredential } from "@azure/core-auth";
import { AzureLogger } from "@azure/logger";
import { PipelineRequest, PipelineResponse, SendRequest } from "../interfaces";
import { PipelinePolicy } from "../pipeline";
import { AccessTokenGetter, createTokenCycler } from "../util/tokenCycler";
import { logger as coreLogger } from "../log";
import { AuthorizeRequestOptions } from "./bearerTokenAuthenticationPolicy";

/**
* The programmatic identifier of the auxiliaryAuthenticationHeaderPolicy.
*/
export const auxiliaryAuthenticationHeaderPolicyName = "auxiliaryAuthenticationHeaderPolicy";
const AUTHORIZATION_AUXILIARY_HEADER = "x-ms-authorization-auxiliary";

/**
* Options to configure the auxiliaryAuthenticationHeaderPolicy
*/
export interface AuxiliaryAuthenticationHeaderPolicyOptions {
/**
* TokenCredential list used to get token from auxiliary tenants and
* one credential for each tenant the client may need to access
*/
credentials?: TokenCredential[];
/**
* Scopes depend on the cloud your application runs in
*/
scopes: string | string[];
/**
* A logger can be sent for debugging purposes.
*/
logger?: AzureLogger;
}

async function sendAuthorizeRequest(options: AuthorizeRequestOptions): Promise<string> {
const { scopes, getAccessToken, request } = options;
const getTokenOptions: GetTokenOptions = {
abortSignal: request.abortSignal,
tracingOptions: request.tracingOptions,
};

return (await getAccessToken(scopes, getTokenOptions))?.token ?? "";
}

/**
* A policy for external tokens to `x-ms-authorization-auxiliary` header.
* 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 could see [ARM docs](https://learn.microsoft.com/en-us/azure/azure-resource-manager/management/authenticate-multi-tenant) for a rundown of how this feature works
*/
export function auxiliaryAuthenticationHeaderPolicy(
options: AuxiliaryAuthenticationHeaderPolicyOptions
): PipelinePolicy {
const { credentials, scopes } = options;
const logger = options.logger || coreLogger;
const tokenCyclerMap = new WeakMap<TokenCredential, AccessTokenGetter>();

return {
name: auxiliaryAuthenticationHeaderPolicyName,
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
if (!request.url.toLowerCase().startsWith("https://")) {
throw new Error(
"Bearer token authentication for auxiliary header is not permitted for non-TLS protected (non-https) URLs."
);
}
if (!credentials || credentials.length === 0) {
logger.info(
`${auxiliaryAuthenticationHeaderPolicyName} header will not be set due to empty credentials.`
);
return next(request);
}

const tokenPromises: Promise<string>[] = [];
for (const credential of credentials) {
let getAccessToken = tokenCyclerMap.get(credential);
if (!getAccessToken) {
getAccessToken = createTokenCycler(credential);
tokenCyclerMap.set(credential, getAccessToken);
}
tokenPromises.push(
sendAuthorizeRequest({
scopes: Array.isArray(scopes) ? scopes : [scopes],
request,
getAccessToken,
logger,
})
);
}
const auxiliaryTokens = (await Promise.all(tokenPromises)).filter((token) => Boolean(token));
if (auxiliaryTokens.length === 0) {
logger.warning(
`None of the auxiliary tokens are valid. ${AUTHORIZATION_AUXILIARY_HEADER} header will not be set.`
);
return next(request);
}
request.headers.set(
AUTHORIZATION_AUXILIARY_HEADER,
auxiliaryTokens.map((token) => `Bearer ${token}`).join(", ")
);

return next(request);
},
};
}
Loading

0 comments on commit b69246d

Please sign in to comment.