Skip to content
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions sdk/core/core-client/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,9 @@

### Features Added

- Added a new function `authorizeRequestOnClaimChallenge`, that can be used with the `@azure/core-rest-pipeline`'s `bearerTokenAuthenticationPolicy` to support [Continuous Access Evaluation (CAE) challenges](https://docs.microsoft.com/azure/active-directory/conditional-access/concept-continuous-access-evaluation).
- Call the `bearerTokenAuthenticationPolicy` with the following options: `bearerTokenAuthenticationPolicy({ authorizeRequestOnChallenge: authorizeRequestOnClaimChallenge })`. Once provided, the `bearerTokenAuthenticationPolicy` policy will internally handle Continuous Access Evaluation (CAE) challenges. When it can't complete a challenge it will return the 401 (unauthorized) response from ARM.

### Breaking Changes

### Bugs Fixed
Expand Down
4 changes: 4 additions & 0 deletions sdk/core/core-client/review/core-client.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
```ts

import { AbortSignalLike } from '@azure/abort-controller';
import { AuthorizeRequestOnChallengeOptions } from '@azure/core-rest-pipeline';
import { HttpClient } from '@azure/core-rest-pipeline';
import { HttpMethods } from '@azure/core-rest-pipeline';
import { InternalPipelineOptions } from '@azure/core-rest-pipeline';
Expand All @@ -17,6 +18,9 @@ import { PipelineResponse } from '@azure/core-rest-pipeline';
import { TokenCredential } from '@azure/core-auth';
import { TransferProgressEvent } from '@azure/core-rest-pipeline';

// @public
export function authorizeRequestOnClaimChallenge(onChallengeOptions: AuthorizeRequestOnChallengeOptions): Promise<boolean>;

// @public (undocumented)
export interface BaseMapper {
constraints?: MapperConstraints;
Expand Down
96 changes: 96 additions & 0 deletions sdk/core/core-client/src/authorizeRequestOnClaimChallenge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { GetTokenOptions } from "@azure/core-auth";
import { AuthorizeRequestOnChallengeOptions } from "@azure/core-rest-pipeline";
import { decodeString, uint8ArrayToString } from "./base64";

/**
* Converts: `Bearer a="b", c="d", Bearer d="e", f="g"`.
* Into: `[ { a: 'b', c: 'd' }, { d: 'e', f: 'g' } ]`.
*
* @internal
*/
export function parseCAEChallenge(challenges: string): any[] {
return `, ${challenges.trim()}`
.split(", Bearer ")
.filter((x) => x)
.map((challenge) =>
`${challenge.trim()}, `
.split('", ')
.filter((x) => x)
.map((keyValue) => (([key, value]) => ({ [key]: value }))(keyValue.trim().split('="')))
.reduce((a, b) => ({ ...a, ...b }), {})
);
}

/**
* CAE Challenge structure
*/
export interface CAEChallenge {
scope: string;
claims: string;
}

/**
* This function can be used as a callback for the `bearerTokenAuthenticationPolicy` of `@azure/core-rest-pipeline`, to support CAE challenges:
* [Continuous Access Evaluation](https://docs.microsoft.com/azure/active-directory/conditional-access/concept-continuous-access-evaluation).
*
* Call the `bearerTokenAuthenticationPolicy` with the following options:
*
* ```ts
* import { bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline";
* import { authorizeRequestOnClaimChallenge } from "@azure/core-client";
*
* const bearerTokenAuthenticationPolicy = bearerTokenAuthenticationPolicy({
* authorizeRequestOnChallenge: authorizeRequestOnClaimChallenge
* });
* ```
*
* Once provided, the `bearerTokenAuthenticationPolicy` policy will internally handle Continuous Access Evaluation (CAE) challenges.
* When it can't complete a challenge it will return the 401 (unauthorized) response from ARM.
*
* Example challenge with claims:
*
* ```
* Bearer authorization_uri="https://login.windows-ppe.net/", error="invalid_token",
* error_description="User session has been revoked",
* claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwgInZhbHVlIjoiMTYwMzc0MjgwMCJ9fX0="
* ```
*/
export async function authorizeRequestOnClaimChallenge(
onChallengeOptions: AuthorizeRequestOnChallengeOptions
): Promise<boolean> {
const { scopes, response } = onChallengeOptions;

const challenge = response.headers.get("WWW-Authenticate");
if (!challenge) {
console.log(
`The WWW-Authenticate header was missing. Failed to perform the Continuous Access Evaluation authentication flow.`
);
return false;
}
const challenges: CAEChallenge[] = parseCAEChallenge(challenge) || [];

const parsedChallenge = challenges.find((x) => x.claims);
if (!parsedChallenge) {
console.log(
`The WWW-Authenticate header was missing the necessary "claims" to perform the Continuous Access Evaluation authentication flow.`
);
return false;
}

const accessToken = await onChallengeOptions.getAccessToken(
parsedChallenge.scope ? [parsedChallenge.scope] : scopes,
{
claims: uint8ArrayToString(decodeString(parsedChallenge.claims))
} as GetTokenOptions
);

if (!accessToken) {
return false;
}

onChallengeOptions.request.headers.set("Authorization", `Bearer ${accessToken.token}`);
return true;
}
15 changes: 15 additions & 0 deletions sdk/core/core-client/src/base64.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,3 +40,18 @@ export function decodeString(value: string): Uint8Array {
}
return arr;
}

/**
* Converts a uint8Array to a string.
*/
export function uint8ArrayToString(ab: Uint8Array): string {
return decodeURIComponent(
escape(
Array.from(ab)
.map(function(item) {
return String.fromCharCode(item);
})
.join("")
)
);
}
8 changes: 8 additions & 0 deletions sdk/core/core-client/src/base64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,11 @@ export function encodeByteArray(value: Uint8Array): string {
export function decodeString(value: string): Uint8Array {
return Buffer.from(value, "base64");
}

/**
* Converts a uint8Array to a string.
*/
export function uint8ArrayToString(ab: Uint8Array): string {
const decoder = new TextDecoder("utf-8");
return decoder.decode(ab);
}
1 change: 1 addition & 0 deletions sdk/core/core-client/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ export {
serializationPolicyName,
SerializationPolicyOptions
} from "./serializationPolicy";
export { authorizeRequestOnClaimChallenge } from "./authorizeRequestOnClaimChallenge";
import "@azure/core-asynciterator-polyfill";
Loading