UNPKG

@azure/core-rest-pipeline

Version:

Isomorphic client library for making HTTP requests in node.js and browser.

243 lines 11.4 kB
"use strict"; // Copyright (c) Microsoft Corporation. // Licensed under the MIT License. Object.defineProperty(exports, "__esModule", { value: true }); exports.bearerTokenAuthenticationPolicyName = void 0; exports.bearerTokenAuthenticationPolicy = bearerTokenAuthenticationPolicy; exports.parseChallenges = parseChallenges; const tokenCycler_js_1 = require("../util/tokenCycler.js"); const log_js_1 = require("../log.js"); const restError_js_1 = require("../restError.js"); /** * The programmatic identifier of the bearerTokenAuthenticationPolicy. */ exports.bearerTokenAuthenticationPolicyName = "bearerTokenAuthenticationPolicy"; /** * Try to send the given request. * * When a response is received, returns a tuple of the response received and, if the response was received * inside a thrown RestError, the RestError that was thrown. * * Otherwise, if an error was thrown while sending the request that did not provide an underlying response, it * will be rethrown. */ async function trySendRequest(request, next) { try { return [await next(request), undefined]; } catch (e) { if ((0, restError_js_1.isRestError)(e) && e.response) { return [e.response, e]; } else { throw e; } } } /** * Default authorize request handler */ async function defaultAuthorizeRequest(options) { const { scopes, getAccessToken, request } = options; // Enable CAE true by default const getTokenOptions = { abortSignal: request.abortSignal, tracingOptions: request.tracingOptions, enableCae: true, }; const accessToken = await getAccessToken(scopes, getTokenOptions); if (accessToken) { options.request.headers.set("Authorization", `Bearer ${accessToken.token}`); } } /** * We will retrieve the challenge only if the response status code was 401, * and if the response contained the header "WWW-Authenticate" with a non-empty value. */ function isChallengeResponse(response) { return response.status === 401 && response.headers.has("WWW-Authenticate"); } /** * Re-authorize the request for CAE challenge. * The response containing the challenge is `options.response`. * If this method returns true, the underlying request will be sent once again. */ async function authorizeRequestOnCaeChallenge(onChallengeOptions, caeClaims) { var _a; const { scopes } = onChallengeOptions; const accessToken = await onChallengeOptions.getAccessToken(scopes, { enableCae: true, claims: caeClaims, }); if (!accessToken) { return false; } onChallengeOptions.request.headers.set("Authorization", `${(_a = accessToken.tokenType) !== null && _a !== void 0 ? _a : "Bearer"} ${accessToken.token}`); return true; } /** * A policy that can request a token from a TokenCredential implementation and * then apply it to the Authorization header of a request as a Bearer token. */ function bearerTokenAuthenticationPolicy(options) { var _a, _b, _c; const { credential, scopes, challengeCallbacks } = options; const logger = options.logger || log_js_1.logger; const callbacks = { authorizeRequest: (_b = (_a = challengeCallbacks === null || challengeCallbacks === void 0 ? void 0 : challengeCallbacks.authorizeRequest) === null || _a === void 0 ? void 0 : _a.bind(challengeCallbacks)) !== null && _b !== void 0 ? _b : defaultAuthorizeRequest, authorizeRequestOnChallenge: (_c = challengeCallbacks === null || challengeCallbacks === void 0 ? void 0 : challengeCallbacks.authorizeRequestOnChallenge) === null || _c === void 0 ? void 0 : _c.bind(challengeCallbacks), }; // This function encapsulates the entire process of reliably retrieving the token // The options are left out of the public API until there's demand to configure this. // Remember to extend `BearerTokenAuthenticationPolicyOptions` with `TokenCyclerOptions` // in order to pass through the `options` object. const getAccessToken = credential ? (0, tokenCycler_js_1.createTokenCycler)(credential /* , options */) : () => Promise.resolve(null); return { name: exports.bearerTokenAuthenticationPolicyName, /** * If there's no challenge parameter: * - It will try to retrieve the token using the cache, or the credential's getToken. * - Then it will try the next policy with or without the retrieved token. * * It uses the challenge parameters to: * - Skip a first attempt to get the token from the credential if there's no cached token, * since it expects the token to be retrievable only after the challenge. * - Prepare the outgoing request if the `prepareRequest` method has been provided. * - Send an initial request to receive the challenge if it fails. * - Process a challenge if the response contains it. * - Retrieve a token with the challenge information, then re-send the request. */ async sendRequest(request, next) { if (!request.url.toLowerCase().startsWith("https://")) { throw new Error("Bearer token authentication is not permitted for non-TLS protected (non-https) URLs."); } await callbacks.authorizeRequest({ scopes: Array.isArray(scopes) ? scopes : [scopes], request, getAccessToken, logger, }); let response; let error; let shouldSendRequest; [response, error] = await trySendRequest(request, next); if (isChallengeResponse(response)) { let claims = getCaeChallengeClaims(response.headers.get("WWW-Authenticate")); // Handle CAE by default when receive CAE claim if (claims) { let parsedClaim; // Return the response immediately if claims is not a valid base64 encoded string try { parsedClaim = atob(claims); } catch (e) { logger.warning(`The WWW-Authenticate header contains "claims" that cannot be parsed. Unable to perform the Continuous Access Evaluation authentication flow. Unparsable claims: ${claims}`); return response; } shouldSendRequest = await authorizeRequestOnCaeChallenge({ scopes: Array.isArray(scopes) ? scopes : [scopes], response, request, getAccessToken, logger, }, parsedClaim); // Send updated request and handle response for RestError if (shouldSendRequest) { [response, error] = await trySendRequest(request, next); } } else if (callbacks.authorizeRequestOnChallenge) { // Handle custom challenges when client provides custom callback shouldSendRequest = await callbacks.authorizeRequestOnChallenge({ scopes: Array.isArray(scopes) ? scopes : [scopes], request, response, getAccessToken, logger, }); // Send updated request and handle response for RestError if (shouldSendRequest) { [response, error] = await trySendRequest(request, next); } // If we get another CAE Claim, we will handle it by default and return whatever value we receive for this if (isChallengeResponse(response)) { claims = getCaeChallengeClaims(response.headers.get("WWW-Authenticate")); if (claims) { let parsedClaim; try { parsedClaim = atob(claims); } catch (e) { logger.warning(`The WWW-Authenticate header contains "claims" that cannot be parsed. Unable to perform the Continuous Access Evaluation authentication flow. Unparsable claims: ${claims}`); return response; } shouldSendRequest = await authorizeRequestOnCaeChallenge({ scopes: Array.isArray(scopes) ? scopes : [scopes], response, request, getAccessToken, logger, }, parsedClaim); // Send updated request and handle response for RestError if (shouldSendRequest) { [response, error] = await trySendRequest(request, next); } } } } } if (error) { throw error; } else { return response; } }, }; } /** * Converts: `Bearer a="b", c="d", Pop e="f", g="h"`. * Into: `[ { scheme: 'Bearer', params: { a: 'b', c: 'd' } }, { scheme: 'Pop', params: { e: 'f', g: 'h' } } ]`. * * @internal */ function parseChallenges(challenges) { // Challenge regex seperates the string to individual challenges with different schemes in the format `Scheme a="b", c=d` // The challenge regex captures parameteres with either quotes values or unquoted values const challengeRegex = /(\w+)\s+((?:\w+=(?:"[^"]*"|[^,]*),?\s*)+)/g; // Parameter regex captures the claims group removed from the scheme in the format `a="b"` and `c="d"` // CAE challenge always have quoted parameters. For more reference, https://learn.microsoft.com/entra/identity-platform/claims-challenge const paramRegex = /(\w+)="([^"]*)"/g; const parsedChallenges = []; let match; // Iterate over each challenge match while ((match = challengeRegex.exec(challenges)) !== null) { const scheme = match[1]; const paramsString = match[2]; const params = {}; let paramMatch; // Iterate over each parameter match while ((paramMatch = paramRegex.exec(paramsString)) !== null) { params[paramMatch[1]] = paramMatch[2]; } parsedChallenges.push({ scheme, params }); } return parsedChallenges; } /** * Parse a pipeline response and look for a CAE challenge with "Bearer" scheme * Return the value in the header without parsing the challenge * @internal */ function getCaeChallengeClaims(challenges) { var _a; if (!challenges) { return; } // Find all challenges present in the header const parsedChallenges = parseChallenges(challenges); return (_a = parsedChallenges.find((x) => x.scheme === "Bearer" && x.params.claims && x.params.error === "insufficient_claims")) === null || _a === void 0 ? void 0 : _a.params.claims; } //# sourceMappingURL=bearerTokenAuthenticationPolicy.js.map