-
Notifications
You must be signed in to change notification settings - Fork 1.3k
[core-client] authorizeRequestOnClaimChallenge #17315
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
dce7c17
90808dc
955ec78
10bcbd8
a678671
4b36f5a
c8936eb
bcce127
e43f1b3
72e8ed7
6d42185
4eab71d
346b812
4635468
c99666b
912ccd6
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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"); | ||
|
|
||
| /** | ||
| * 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[] { | ||
sadasant marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| 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; | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -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 | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Shouldnt there be an
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe 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(); | ||
| } | ||
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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