Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
1 change: 1 addition & 0 deletions sdk/core/core-client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@
"@azure/core-auth": "^1.3.0",
"@azure/core-rest-pipeline": "^1.1.0",
"@azure/core-tracing": "1.0.0-preview.13",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0"
},
"devDependencies": {
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
98 changes: 98 additions & 0 deletions sdk/core/core-client/src/authorizeRequestOnClaimChallenge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { GetTokenOptions } from "@azure/core-auth";
import { AuthorizeRequestOnChallengeOptions } from "@azure/core-rest-pipeline";
import { createClientLogger } from "@azure/logger";
import { decodeStringToString } from "./base64";

const logger = createClientLogger("authorizeRequestOnClaimChallenge");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it feels weird to create a new package logger for this - don't SDKs usually create one with the package name instead? Or is this special because it's basically a mixin function? Perhaps the logger should be passed in instead?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the idea of accepting a logger to be passed in! I’ll make a PR

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have made a PR: #18467


/**
* 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[] {
const bearerChallenges = `, ${challenges.trim()}`.split(", Bearer ").filter((x) => x);
return bearerChallenges.map((challenge) => {
const challengeParts = `${challenge.trim()}, `.split('", ').filter((x) => x);
const keyValuePairs = challengeParts.map((keyValue) =>
(([key, value]) => ({ [key]: value }))(keyValue.trim().split('="'))
);
// Key-value pairs to plain object:
return keyValuePairs.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) {
logger.info(
`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) {
logger.info(
`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: decodeStringToString(parsedChallenge.claims)
} as GetTokenOptions
);

if (!accessToken) {
return false;
}

onChallengeOptions.request.headers.set("Authorization", `Bearer ${accessToken.token}`);
return true;
}
8 changes: 8 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,11 @@ export function decodeString(value: string): Uint8Array {
}
return arr;
}

/**
* Decodes a base64 string into a string.
* @param value - the base64 string to decode
*/
export function decodeStringToString(value: string): string {
return atob(value);
}
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");
}

/**
* Decodes a base64 string into a string.
* @param value - the base64 string to decode
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Shouldnt there be an @internal tag on this just like the other functions in this file?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I will make a PR! (the good news is that this isn’t being exported by the package anyway)

*/
export function decodeStringToString(value: string): string {
return Buffer.from(value, "base64").toString();
}
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