@azure/keyvault-common 2.0.2-alpha.20260305.1 → 2.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/commonjs/extendedClientOptions.js +1 -0
- package/dist/commonjs/extendedClientOptions.js.map +1 -1
- package/dist/commonjs/index.js +1 -0
- package/dist/commonjs/index.js.map +1 -1
- package/dist/commonjs/keyVaultAuthenticationPolicy.js +1 -0
- package/dist/commonjs/keyVaultAuthenticationPolicy.js.map +1 -1
- package/dist/commonjs/logger.js +1 -0
- package/dist/commonjs/logger.js.map +1 -1
- package/dist/commonjs/parseKeyVaultIdentifier.js +1 -0
- package/dist/commonjs/parseKeyVaultIdentifier.js.map +1 -1
- package/dist/commonjs/parseWWWAuthenticate.js +1 -0
- package/dist/commonjs/parseWWWAuthenticate.js.map +1 -1
- package/dist/commonjs/tokenCycler.js +1 -0
- package/dist/commonjs/tokenCycler.js.map +1 -1
- package/dist/commonjs/tsdoc-metadata.json +1 -1
- package/dist/esm/extendedClientOptions.js +4 -0
- package/dist/esm/extendedClientOptions.js.map +1 -7
- package/dist/esm/index.js +3 -0
- package/dist/esm/index.js.map +1 -7
- package/dist/esm/keyVaultAuthenticationPolicy.js +151 -125
- package/dist/esm/keyVaultAuthenticationPolicy.js.map +1 -7
- package/dist/esm/logger.js +4 -4
- package/dist/esm/logger.js.map +1 -7
- package/dist/esm/parseKeyVaultIdentifier.js +39 -35
- package/dist/esm/parseKeyVaultIdentifier.js.map +1 -7
- package/dist/esm/parseWWWAuthenticate.js +43 -31
- package/dist/esm/parseWWWAuthenticate.js.map +1 -7
- package/dist/esm/tokenCycler.js +151 -98
- package/dist/esm/tokenCycler.js.map +1 -7
- package/package.json +15 -15
|
@@ -13,3 +13,4 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
13
13
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
14
14
|
var extendedClientOptions_exports = {};
|
|
15
15
|
module.exports = __toCommonJS(extendedClientOptions_exports);
|
|
16
|
+
//# sourceMappingURL=extendedClientOptions.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../src/extendedClientOptions.ts"],
|
|
4
4
|
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nimport type { HttpClient, PipelineOptions } from \"@azure/core-rest-pipeline\";\nimport type { AdditionalPolicyConfig } from \"@azure-rest/core-client\";\n\n/**\n * The common set of options that high level clients are expected to expose.\n */\nexport interface CommonClientOptions extends PipelineOptions {\n /**\n * The HttpClient that will be used to send HTTP requests.\n */\n httpClient?: HttpClient;\n /**\n * Set to true if the request is sent over HTTP instead of HTTPS\n */\n allowInsecureConnection?: boolean;\n /**\n * Additional policies to include in the HTTP pipeline.\n */\n additionalPolicies?: AdditionalPolicyConfig[];\n}\n\n/**\n * Options for keep alive behavior of HTTP connections.\n */\nexport interface KeepAliveOptions {\n /**\n * When true, connections will be kept alive for multiple requests.\n * Defaults to true.\n */\n enable?: boolean;\n}\n\n/**\n * Options for how redirect responses are handled.\n */\nexport interface RedirectOptions {\n /**\n * When true, redirect responses are followed. Defaults to true.\n */\n handleRedirects?: boolean;\n\n /**\n * The maximum number of times the redirect URL will be tried before\n * failing. Defaults to 20.\n */\n maxRetries?: number;\n}\n\n/**\n * Extended common client options that include keep alive and redirect options.\n * This type combines the standard CommonClientOptions with additional options\n * for controlling HTTP connection keep-alive and redirect behavior.\n */\nexport type ExtendedCommonClientOptions = CommonClientOptions & {\n /**\n * Options to configure keep alive behavior for HTTP connections.\n */\n keepAliveOptions?: KeepAliveOptions;\n /**\n * Options to configure how redirect responses are handled.\n */\n redirectOptions?: RedirectOptions;\n};\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;AAAA;AAAA;",
|
|
6
6
|
"names": []
|
package/dist/commonjs/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../src/index.ts"],
|
|
4
4
|
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nexport * from \"./keyVaultAuthenticationPolicy.js\";\nexport * from \"./parseKeyVaultIdentifier.js\";\nexport type * from \"./extendedClientOptions.js\";\nexport type { AdditionalPolicyConfig } from \"@azure-rest/core-client\";\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;AAAA;AAAA;AAGA,wBAAc,8CAHd;AAIA,wBAAc,yCAJd;",
|
|
6
6
|
"names": []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../src/keyVaultAuthenticationPolicy.ts"],
|
|
4
4
|
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nimport type {\n PipelinePolicy,\n PipelineRequest,\n PipelineResponse,\n RequestBodyType,\n SendRequest,\n} from \"@azure/core-rest-pipeline\";\nimport type { WWWAuthenticate } from \"./parseWWWAuthenticate.js\";\nimport { parseWWWAuthenticateHeader } from \"./parseWWWAuthenticate.js\";\n\nimport type { GetTokenOptions, TokenCredential } from \"@azure/core-auth\";\nimport { createTokenCycler } from \"./tokenCycler.js\";\nimport { logger } from \"./logger.js\";\n\n/**\n * @internal\n * Holds the state of Challenge Auth.\n * When making the first request we force Key Vault to begin a challenge\n * by clearing out the request body and storing it locally.\n *\n * Later on, the authorizeRequestOnChallenge callback will process the\n * challenge and, if ready to resend the original request, reset the body\n * so that it may be sent again.\n *\n * Once a client has succeeded once, we can start skipping CAE.\n */\ntype ChallengeState =\n | {\n status: \"none\";\n }\n | {\n status: \"started\";\n originalBody?: RequestBodyType;\n }\n | {\n status: \"complete\";\n scopes: string[];\n tenantId?: string;\n };\n\n/**\n * Additional options for the challenge based authentication policy.\n */\nexport interface KeyVaultAuthenticationPolicyOptions {\n /**\n * Whether to disable verification that the challenge resource matches the Key Vault or Managed HSM domain.\n *\n * Defaults to false.\n */\n disableChallengeResourceVerification?: boolean;\n}\n\nfunction verifyChallengeResource(scope: string, request: PipelineRequest): void {\n let scopeAsUrl: URL;\n try {\n scopeAsUrl = new URL(scope);\n } catch (e) {\n throw new Error(`The challenge contains invalid scope '${scope}'`);\n }\n\n const requestUrl = new URL(request.url);\n\n if (!requestUrl.hostname.endsWith(`.${scopeAsUrl.hostname}`)) {\n throw new Error(\n `The challenge resource '${scopeAsUrl.hostname}' does not match the requested domain. Set disableChallengeResourceVerification to true in your client options to disable. See https://aka.ms/azsdk/blog/vault-uri for more information.`,\n );\n }\n}\n\n/**\n * Name of the Key Vault authentication policy.\n */\nexport const keyVaultAuthenticationPolicyName = \"keyVaultAuthenticationPolicy\";\n\n/**\n * A custom implementation of the bearer-token authentication policy that handles Key Vault and CAE challenges.\n *\n * Key Vault supports other authentication schemes, but we ensure challenge authentication\n * is used by first sending a copy of the request, without authorization or content.\n *\n * when the challenge is received, it will be authenticated and used to send the original\n * request with authorization.\n *\n * Following the first request of a client, follow-up requests will get the cached token\n * if possible.\n *\n */\nexport function keyVaultAuthenticationPolicy(\n credential: TokenCredential,\n options: KeyVaultAuthenticationPolicyOptions = {},\n): PipelinePolicy {\n const { disableChallengeResourceVerification } = options;\n let challengeState: ChallengeState = { status: \"none\" };\n const getAccessToken = createTokenCycler(credential);\n\n function requestToOptions(request: PipelineRequest): GetTokenOptions {\n return {\n abortSignal: request.abortSignal,\n requestOptions: {\n timeout: request.timeout > 0 ? request.timeout : undefined,\n },\n tracingOptions: request.tracingOptions,\n };\n }\n\n async function authorizeRequest(request: PipelineRequest): Promise<void> {\n const requestOptions: GetTokenOptions = requestToOptions(request);\n\n switch (challengeState.status) {\n case \"none\":\n challengeState = {\n status: \"started\",\n originalBody: request.body,\n };\n request.body = null;\n break;\n case \"started\":\n break; // Retry, we should not overwrite the original body\n case \"complete\": {\n const token = await getAccessToken(challengeState.scopes, {\n ...requestOptions,\n enableCae: true,\n tenantId: challengeState.tenantId,\n });\n if (token) {\n request.headers.set(\"authorization\", `Bearer ${token.token}`);\n }\n break;\n }\n }\n }\n\n async function handleChallenge(\n request: PipelineRequest,\n response: PipelineResponse,\n next: SendRequest,\n ): Promise<PipelineResponse> {\n // If status is not 401, this is a no-op\n if (response.status !== 401) {\n return response;\n }\n\n if (request.body === null && challengeState.status === \"started\") {\n // Reset the original body before doing anything else.\n // Note: If successful status will be \"complete\", otherwise \"none\" will\n // restart the process.\n request.body = challengeState.originalBody;\n }\n\n const getTokenOptions = requestToOptions(request);\n\n const challenge = response.headers.get(\"WWW-Authenticate\");\n if (!challenge) {\n logger.warning(\n \"keyVaultAuthentication policy encountered a 401 response without a corresponding WWW-Authenticate header. This is unexpected. Not handling the 401 response.\",\n );\n return response;\n }\n const parsedChallenge: WWWAuthenticate = parseWWWAuthenticateHeader(challenge);\n\n const scope = parsedChallenge.resource\n ? parsedChallenge.resource + \"/.default\"\n : parsedChallenge.scope;\n\n if (!scope) {\n // Cannot handle this kind of challenge here (if scope is not present, may be a CAE challenge)\n return response;\n }\n\n if (!disableChallengeResourceVerification) {\n verifyChallengeResource(scope, request);\n }\n\n const accessToken = await getAccessToken([scope], {\n ...getTokenOptions,\n enableCae: true,\n tenantId: parsedChallenge.tenantId,\n });\n\n if (!accessToken) {\n // No access token provided, treat as no-op\n return response;\n }\n\n request.headers.set(\"Authorization\", `Bearer ${accessToken.token}`);\n\n challengeState = {\n status: \"complete\",\n scopes: [scope],\n tenantId: parsedChallenge.tenantId,\n };\n\n // We have a token now, so try send the request again\n return next(request);\n }\n\n async function handleCaeChallenge(\n request: PipelineRequest,\n response: PipelineResponse,\n next: SendRequest,\n ): Promise<PipelineResponse> {\n // Cannot handle CAE challenge if a regular challenge has not been completed first\n if (challengeState.status !== \"complete\") {\n return response;\n }\n\n // If status is not 401, this is a no-op\n if (response.status !== 401) {\n return response;\n }\n\n const getTokenOptions = requestToOptions(request);\n\n const challenge = response.headers.get(\"WWW-Authenticate\");\n if (!challenge) {\n return response;\n }\n const { claims: base64EncodedClaims, error }: WWWAuthenticate =\n parseWWWAuthenticateHeader(challenge);\n\n if (error !== \"insufficient_claims\" || base64EncodedClaims === undefined) {\n return response;\n }\n\n const claims = atob(base64EncodedClaims);\n\n const accessToken = await getAccessToken(challengeState.scopes, {\n ...getTokenOptions,\n enableCae: true,\n tenantId: challengeState.tenantId,\n claims,\n });\n\n request.headers.set(\"Authorization\", `Bearer ${accessToken.token}`);\n\n return next(request);\n }\n\n async function sendRequest(\n request: PipelineRequest,\n next: SendRequest,\n ): Promise<PipelineResponse> {\n // Add token if possible\n await authorizeRequest(request);\n\n // Try send request (first attempt)\n let response = await next(request);\n\n // Handle standard challenge if present\n response = await handleChallenge(request, response, next);\n\n // Handle CAE challenge if present\n response = await handleCaeChallenge(request, response, next);\n\n return response;\n }\n\n return {\n name: keyVaultAuthenticationPolicyName,\n sendRequest,\n };\n}\n"],
|
|
5
5
|
"mappings": "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",
|
|
6
6
|
"names": []
|
package/dist/commonjs/logger.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../src/logger.ts"],
|
|
4
4
|
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nimport { createClientLogger } from \"@azure/logger\";\n\nexport const logger = createClientLogger(\"keyvault-common\");\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAGA,oBAAmC;AAE5B,MAAM,aAAS,kCAAmB,iBAAiB;",
|
|
6
6
|
"names": []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../src/parseKeyVaultIdentifier.ts"],
|
|
4
4
|
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n/**\n * The parsed components of a Key Vault entity identifier.\n */\nexport interface KeyVaultEntityIdentifier {\n /**\n * The vault URI.\n */\n vaultUrl: string;\n /**\n * The version of key/secret/certificate. May be undefined.\n */\n version?: string;\n /**\n * The name of key/secret/certificate.\n */\n name: string;\n}\n\n/**\n * Parses a Key Vault identifier into its components.\n *\n * @param collection - The collection of the Key Vault identifier.\n * @param identifier - The Key Vault identifier to be parsed.\n */\nexport function parseKeyVaultIdentifier(\n collection: string,\n identifier: string | undefined,\n): KeyVaultEntityIdentifier {\n if (typeof collection !== \"string\" || !(collection = collection.trim())) {\n throw new Error(\"Invalid collection argument\");\n }\n\n if (typeof identifier !== \"string\" || !(identifier = identifier.trim())) {\n throw new Error(\"Invalid identifier argument\");\n }\n\n let baseUri;\n try {\n baseUri = new URL(identifier);\n } catch (e: any) {\n throw new Error(`Invalid ${collection} identifier: ${identifier}. Not a valid URI`);\n }\n\n // Path is of the form '/collection/name[/version]'\n const segments = (baseUri.pathname || \"\").split(\"/\");\n if (segments.length !== 3 && segments.length !== 4) {\n throw new Error(\n `Invalid ${collection} identifier: ${identifier}. Bad number of segments: ${segments.length}`,\n );\n }\n\n if (collection !== segments[1]) {\n throw new Error(\n `Invalid ${collection} identifier: ${identifier}. segment [1] should be \"${collection}\", found \"${segments[1]}\"`,\n );\n }\n\n const vaultUrl = `${baseUri.protocol}//${baseUri.host}`;\n const name = segments[2];\n const version = segments.length === 4 ? segments[3] : undefined;\n return {\n vaultUrl,\n name,\n version,\n };\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA2BO,SAAS,wBACd,YACA,YAC0B;AAC1B,MAAI,OAAO,eAAe,YAAY,EAAE,aAAa,WAAW,KAAK,IAAI;AACvE,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC/C;AAEA,MAAI,OAAO,eAAe,YAAY,EAAE,aAAa,WAAW,KAAK,IAAI;AACvE,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC/C;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,IAAI,IAAI,UAAU;AAAA,EAC9B,SAAS,GAAQ;AACf,UAAM,IAAI,MAAM,WAAW,UAAU,gBAAgB,UAAU,mBAAmB;AAAA,EACpF;AAGA,QAAM,YAAY,QAAQ,YAAY,IAAI,MAAM,GAAG;AACnD,MAAI,SAAS,WAAW,KAAK,SAAS,WAAW,GAAG;AAClD,UAAM,IAAI;AAAA,MACR,WAAW,UAAU,gBAAgB,UAAU,6BAA6B,SAAS,MAAM;AAAA,IAC7F;AAAA,EACF;AAEA,MAAI,eAAe,SAAS,CAAC,GAAG;AAC9B,UAAM,IAAI;AAAA,MACR,WAAW,UAAU,gBAAgB,UAAU,4BAA4B,UAAU,aAAa,SAAS,CAAC,CAAC;AAAA,IAC/G;AAAA,EACF;AAEA,QAAM,WAAW,GAAG,QAAQ,QAAQ,KAAK,QAAQ,IAAI;AACrD,QAAM,OAAO,SAAS,CAAC;AACvB,QAAM,UAAU,SAAS,WAAW,IAAI,SAAS,CAAC,IAAI;AACtD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
6
|
"names": []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../src/parseWWWAuthenticate.ts"],
|
|
4
4
|
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n/**\n * Parameters parsed out of the WWW-Authenticate header value by the parseWWWAuthenticate function.\n */\nexport interface WWWAuthenticate {\n /**\n * The authorization parameter, if present.\n */\n authorization?: string;\n\n /**\n * The authorization_url parameter, if present.\n */\n authorization_url?: string;\n\n /**\n * The resource parameter, if present.\n */\n resource?: string;\n\n /**\n * The scope parameter, if present.\n */\n scope?: string;\n\n /**\n * The tenantId parameter, if present.\n */\n tenantId?: string;\n\n /**\n * The claims parameter, if present.\n */\n claims?: string;\n\n /**\n * The error parameter, if present.\n */\n error?: string;\n}\n\nconst validWWWAuthenticateProperties: readonly (keyof WWWAuthenticate)[] = [\n \"authorization\",\n \"authorization_url\",\n \"resource\",\n \"scope\",\n \"tenantId\",\n \"claims\",\n \"error\",\n] as const;\n\n/**\n * Parses an WWW-Authenticate response header.\n * This transforms a string value like:\n * `Bearer authorization=\"https://some.url/tenantId\", resource=\"https://some.url\"`\n * into an object like:\n * `{ authorization: \"https://some.url/tenantId\", resource: \"https://some.url\" }`\n * @param headerValue - String value in the WWW-Authenticate header\n */\nexport function parseWWWAuthenticateHeader(headerValue: string): WWWAuthenticate {\n const pairDelimiter = /,? +/;\n const parsed = headerValue.split(pairDelimiter).reduce<WWWAuthenticate>((kvPairs, p) => {\n if (p.match(/\\w=\"/)) {\n // 'sampleKey=\"sample_value\"' -> [sampleKey, \"sample_value\"] -> { sampleKey: sample_value }\n const [key, ...value] = p.split(\"=\");\n if (validWWWAuthenticateProperties.includes(key as keyof WWWAuthenticate)) {\n // The values will be wrapped in quotes, which need to be stripped out.\n return { ...kvPairs, [key]: value.join(\"=\").slice(1, -1) };\n }\n }\n return kvPairs;\n }, {});\n\n // Finally, we pull the tenantId from the authorization header to support multi-tenant authentication.\n if (parsed.authorization) {\n try {\n const tenantId = new URL(parsed.authorization).pathname.substring(1);\n if (tenantId) {\n parsed.tenantId = tenantId;\n }\n } catch (_) {\n throw new Error(`The challenge authorization URI '${parsed.authorization}' is invalid.`);\n }\n }\n\n return parsed;\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AA2CA,MAAM,iCAAqE;AAAA,EACzE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAUO,SAAS,2BAA2B,aAAsC;AAC/E,QAAM,gBAAgB;AACtB,QAAM,SAAS,YAAY,MAAM,aAAa,EAAE,OAAwB,CAAC,SAAS,MAAM;AACtF,QAAI,EAAE,MAAM,MAAM,GAAG;AAEnB,YAAM,CAAC,KAAK,GAAG,KAAK,IAAI,EAAE,MAAM,GAAG;AACnC,UAAI,+BAA+B,SAAS,GAA4B,GAAG;AAEzE,eAAO,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,MAAM,KAAK,GAAG,EAAE,MAAM,GAAG,EAAE,EAAE;AAAA,MAC3D;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAGL,MAAI,OAAO,eAAe;AACxB,QAAI;AACF,YAAM,WAAW,IAAI,IAAI,OAAO,aAAa,EAAE,SAAS,UAAU,CAAC;AACnE,UAAI,UAAU;AACZ,eAAO,WAAW;AAAA,MACpB;AAAA,IACF,SAAS,GAAG;AACV,YAAM,IAAI,MAAM,oCAAoC,OAAO,aAAa,eAAe;AAAA,IACzF;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
6
|
"names": []
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
|
-
"sources": ["
|
|
3
|
+
"sources": ["../../src/tokenCycler.ts"],
|
|
4
4
|
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// This file is a direct copy of the tokenCycler implementation in core-rest-pipeline.\n\nimport type { AccessToken, GetTokenOptions, TokenCredential } from \"@azure/core-auth\";\nimport { delay } from \"@azure/core-util\";\n\n/**\n * A function that gets a promise of an access token and allows providing\n * options.\n *\n * @param options - the options to pass to the underlying token provider\n */\nexport type AccessTokenGetter = (\n scopes: string | string[],\n options: GetTokenOptions,\n) => Promise<AccessToken>;\n\nexport interface TokenCyclerOptions {\n /**\n * The window of time before token expiration during which the token will be\n * considered unusable due to risk of the token expiring before sending the\n * request.\n *\n * This will only become meaningful if the refresh fails for over\n * (refreshWindow - forcedRefreshWindow) milliseconds.\n */\n forcedRefreshWindowInMs: number;\n /**\n * Interval in milliseconds to retry failed token refreshes.\n */\n retryIntervalInMs: number;\n /**\n * The window of time before token expiration during which\n * we will attempt to refresh the token.\n */\n refreshWindowInMs: number;\n}\n\n// Default options for the cycler if none are provided\nexport const DEFAULT_CYCLER_OPTIONS: TokenCyclerOptions = {\n forcedRefreshWindowInMs: 1000, // Force waiting for a refresh 1s before the token expires\n retryIntervalInMs: 3000, // Allow refresh attempts every 3s\n refreshWindowInMs: 1000 * 60 * 2, // Start refreshing 2m before expiry\n};\n\n/**\n * Converts an an unreliable access token getter (which may resolve with null)\n * into an AccessTokenGetter by retrying the unreliable getter in a regular\n * interval.\n *\n * @param getAccessToken - A function that produces a promise of an access token that may fail by returning null.\n * @param retryIntervalInMs - The time (in milliseconds) to wait between retry attempts.\n * @param refreshTimeout - The timestamp after which the refresh attempt will fail, throwing an exception.\n * @returns - A promise that, if it resolves, will resolve with an access token.\n */\nasync function beginRefresh(\n getAccessToken: () => Promise<AccessToken | null>,\n retryIntervalInMs: number,\n refreshTimeout: number,\n): Promise<AccessToken> {\n // This wrapper handles exceptions gracefully as long as we haven't exceeded\n // the timeout.\n async function tryGetAccessToken(): Promise<AccessToken | null> {\n if (Date.now() < refreshTimeout) {\n try {\n return await getAccessToken();\n } catch {\n return null;\n }\n } else {\n const finalToken = await getAccessToken();\n\n // Timeout is up, so throw if it's still null\n if (finalToken === null) {\n throw new Error(\"Failed to refresh access token.\");\n }\n\n return finalToken;\n }\n }\n\n let token: AccessToken | null = await tryGetAccessToken();\n\n while (token === null) {\n await delay(retryIntervalInMs);\n\n token = await tryGetAccessToken();\n }\n\n return token;\n}\n\n/**\n * Creates a token cycler from a credential, scopes, and optional settings.\n *\n * A token cycler represents a way to reliably retrieve a valid access token\n * from a TokenCredential. It will handle initializing the token, refreshing it\n * when it nears expiration, and synchronizes refresh attempts to avoid\n * concurrency hazards.\n *\n * @param credential - the underlying TokenCredential that provides the access\n * token\n * @param tokenCyclerOptions - optionally override default settings for the cycler\n *\n * @returns - a function that reliably produces a valid access token\n */\nexport function createTokenCycler(\n credential: TokenCredential,\n tokenCyclerOptions?: Partial<TokenCyclerOptions>,\n): AccessTokenGetter {\n let refreshWorker: Promise<AccessToken> | null = null;\n let token: AccessToken | null = null;\n let tenantId: string | undefined;\n\n const options = {\n ...DEFAULT_CYCLER_OPTIONS,\n ...tokenCyclerOptions,\n };\n\n /**\n * This little holder defines several predicates that we use to construct\n * the rules of refreshing the token.\n */\n const cycler = {\n /**\n * Produces true if a refresh job is currently in progress.\n */\n get isRefreshing(): boolean {\n return refreshWorker !== null;\n },\n /**\n * Produces true if the cycler SHOULD refresh (we are within the refresh\n * window and not already refreshing)\n */\n get shouldRefresh(): boolean {\n if (cycler.isRefreshing) {\n return false;\n }\n if (token?.refreshAfterTimestamp && token.refreshAfterTimestamp < Date.now()) {\n return true;\n }\n\n return (token?.expiresOnTimestamp ?? 0) - options.refreshWindowInMs < Date.now();\n },\n /**\n * Produces true if the cycler MUST refresh (null or nearly-expired\n * token).\n */\n get mustRefresh(): boolean {\n return (\n token === null || token.expiresOnTimestamp - options.forcedRefreshWindowInMs < Date.now()\n );\n },\n };\n\n /**\n * Starts a refresh job or returns the existing job if one is already\n * running.\n */\n function refresh(\n scopes: string | string[],\n getTokenOptions: GetTokenOptions,\n ): Promise<AccessToken> {\n if (!cycler.isRefreshing) {\n // We bind `scopes` here to avoid passing it around a lot\n const tryGetAccessToken = (): Promise<AccessToken | null> =>\n credential.getToken(scopes, getTokenOptions);\n\n // Take advantage of promise chaining to insert an assignment to `token`\n // before the refresh can be considered done.\n refreshWorker = beginRefresh(\n tryGetAccessToken,\n options.retryIntervalInMs,\n // If we don't have a token, then we should timeout immediately\n token?.expiresOnTimestamp ?? Date.now(),\n )\n .then((_token) => {\n refreshWorker = null;\n token = _token;\n tenantId = getTokenOptions.tenantId;\n return token;\n })\n .catch((reason) => {\n // We also should reset the refresher if we enter a failed state. All\n // existing awaiters will throw, but subsequent requests will start a\n // new retry chain.\n refreshWorker = null;\n token = null;\n tenantId = undefined;\n throw reason;\n });\n }\n\n return refreshWorker as Promise<AccessToken>;\n }\n\n return async (scopes: string | string[], tokenOptions: GetTokenOptions): Promise<AccessToken> => {\n //\n // Simple rules:\n // - If we MUST refresh, then return the refresh task, blocking\n // the pipeline until a token is available.\n // - If we SHOULD refresh, then run refresh but don't return it\n // (we can still use the cached token).\n // - Return the token, since it's fine if we didn't return in\n // step 1.\n //\n\n const hasClaimChallenge = Boolean(tokenOptions.claims);\n const tenantIdChanged = tenantId !== tokenOptions.tenantId;\n\n if (hasClaimChallenge) {\n // If we've received a claim, we know the existing token isn't valid\n // We want to clear it so that that refresh worker won't use the old expiration time as a timeout\n token = null;\n }\n\n // If the tenantId passed in token options is different to the one we have\n // Or if we are in claim challenge and the token was rejected and a new access token need to be issued, we need to\n // refresh the token with the new tenantId or token.\n const mustRefresh = tenantIdChanged || hasClaimChallenge || cycler.mustRefresh;\n\n if (mustRefresh) {\n return refresh(scopes, tokenOptions);\n }\n\n if (cycler.shouldRefresh) {\n refresh(scopes, tokenOptions);\n }\n\n return token as AccessToken;\n };\n}\n"],
|
|
5
5
|
"mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAMA,uBAAsB;AAmCf,MAAM,yBAA6C;AAAA,EACxD,yBAAyB;AAAA;AAAA,EACzB,mBAAmB;AAAA;AAAA,EACnB,mBAAmB,MAAO,KAAK;AAAA;AACjC;AAYA,eAAe,aACb,gBACA,mBACA,gBACsB;AAGtB,iBAAe,oBAAiD;AAC9D,QAAI,KAAK,IAAI,IAAI,gBAAgB;AAC/B,UAAI;AACF,eAAO,MAAM,eAAe;AAAA,MAC9B,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,OAAO;AACL,YAAM,aAAa,MAAM,eAAe;AAGxC,UAAI,eAAe,MAAM;AACvB,cAAM,IAAI,MAAM,iCAAiC;AAAA,MACnD;AAEA,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,QAA4B,MAAM,kBAAkB;AAExD,SAAO,UAAU,MAAM;AACrB,cAAM,wBAAM,iBAAiB;AAE7B,YAAQ,MAAM,kBAAkB;AAAA,EAClC;AAEA,SAAO;AACT;AAgBO,SAAS,kBACd,YACA,oBACmB;AACnB,MAAI,gBAA6C;AACjD,MAAI,QAA4B;AAChC,MAAI;AAEJ,QAAM,UAAU;AAAA,IACd,GAAG;AAAA,IACH,GAAG;AAAA,EACL;AAMA,QAAM,SAAS;AAAA;AAAA;AAAA;AAAA,IAIb,IAAI,eAAwB;AAC1B,aAAO,kBAAkB;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,IAAI,gBAAyB;AAC3B,UAAI,OAAO,cAAc;AACvB,eAAO;AAAA,MACT;AACA,UAAI,OAAO,yBAAyB,MAAM,wBAAwB,KAAK,IAAI,GAAG;AAC5E,eAAO;AAAA,MACT;AAEA,cAAQ,OAAO,sBAAsB,KAAK,QAAQ,oBAAoB,KAAK,IAAI;AAAA,IACjF;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,IAAI,cAAuB;AACzB,aACE,UAAU,QAAQ,MAAM,qBAAqB,QAAQ,0BAA0B,KAAK,IAAI;AAAA,IAE5F;AAAA,EACF;AAMA,WAAS,QACP,QACA,iBACsB;AACtB,QAAI,CAAC,OAAO,cAAc;AAExB,YAAM,oBAAoB,MACxB,WAAW,SAAS,QAAQ,eAAe;AAI7C,sBAAgB;AAAA,QACd;AAAA,QACA,QAAQ;AAAA;AAAA,QAER,OAAO,sBAAsB,KAAK,IAAI;AAAA,MACxC,EACG,KAAK,CAAC,WAAW;AAChB,wBAAgB;AAChB,gBAAQ;AACR,mBAAW,gBAAgB;AAC3B,eAAO;AAAA,MACT,CAAC,EACA,MAAM,CAAC,WAAW;AAIjB,wBAAgB;AAChB,gBAAQ;AACR,mBAAW;AACX,cAAM;AAAA,MACR,CAAC;AAAA,IACL;AAEA,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,QAA2B,iBAAwD;AAW/F,UAAM,oBAAoB,QAAQ,aAAa,MAAM;AACrD,UAAM,kBAAkB,aAAa,aAAa;AAElD,QAAI,mBAAmB;AAGrB,cAAQ;AAAA,IACV;AAKA,UAAM,cAAc,mBAAmB,qBAAqB,OAAO;AAEnE,QAAI,aAAa;AACf,aAAO,QAAQ,QAAQ,YAAY;AAAA,IACrC;AAEA,QAAI,OAAO,eAAe;AACxB,cAAQ,QAAQ,YAAY;AAAA,IAC9B;AAEA,WAAO;AAAA,EACT;AACF;",
|
|
6
6
|
"names": []
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": [],
|
|
4
|
-
"sourcesContent": [],
|
|
5
|
-
"mappings": "",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
1
|
+
{"version":3,"file":"extendedClientOptions.js","sourceRoot":"","sources":["../../src/extendedClientOptions.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC","sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nimport type { HttpClient, PipelineOptions } from \"@azure/core-rest-pipeline\";\nimport type { AdditionalPolicyConfig } from \"@azure-rest/core-client\";\n\n/**\n * The common set of options that high level clients are expected to expose.\n */\nexport interface CommonClientOptions extends PipelineOptions {\n /**\n * The HttpClient that will be used to send HTTP requests.\n */\n httpClient?: HttpClient;\n /**\n * Set to true if the request is sent over HTTP instead of HTTPS\n */\n allowInsecureConnection?: boolean;\n /**\n * Additional policies to include in the HTTP pipeline.\n */\n additionalPolicies?: AdditionalPolicyConfig[];\n}\n\n/**\n * Options for keep alive behavior of HTTP connections.\n */\nexport interface KeepAliveOptions {\n /**\n * When true, connections will be kept alive for multiple requests.\n * Defaults to true.\n */\n enable?: boolean;\n}\n\n/**\n * Options for how redirect responses are handled.\n */\nexport interface RedirectOptions {\n /**\n * When true, redirect responses are followed. Defaults to true.\n */\n handleRedirects?: boolean;\n\n /**\n * The maximum number of times the redirect URL will be tried before\n * failing. Defaults to 20.\n */\n maxRetries?: number;\n}\n\n/**\n * Extended common client options that include keep alive and redirect options.\n * This type combines the standard CommonClientOptions with additional options\n * for controlling HTTP connection keep-alive and redirect behavior.\n */\nexport type ExtendedCommonClientOptions = CommonClientOptions & {\n /**\n * Options to configure keep alive behavior for HTTP connections.\n */\n keepAliveOptions?: KeepAliveOptions;\n /**\n * Options to configure how redirect responses are handled.\n */\n redirectOptions?: RedirectOptions;\n};\n"]}
|
package/dist/esm/index.js
CHANGED
package/dist/esm/index.js.map
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["/mnt/vss/_work/1/s/sdk/keyvault/keyvault-common/src/index.ts"],
|
|
4
|
-
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nexport * from \"./keyVaultAuthenticationPolicy.js\";\nexport * from \"./parseKeyVaultIdentifier.js\";\nexport type * from \"./extendedClientOptions.js\";\nexport type { AdditionalPolicyConfig } from \"@azure-rest/core-client\";\n"],
|
|
5
|
-
"mappings": "AAGA,cAAc;AACd,cAAc;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC;AAElC,cAAc,mCAAmC,CAAC;AAClD,cAAc,8BAA8B,CAAC","sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nexport * from \"./keyVaultAuthenticationPolicy.js\";\nexport * from \"./parseKeyVaultIdentifier.js\";\nexport type * from \"./extendedClientOptions.js\";\nexport type { AdditionalPolicyConfig } from \"@azure-rest/core-client\";\n"]}
|
|
@@ -1,138 +1,164 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
1
3
|
import { parseWWWAuthenticateHeader } from "./parseWWWAuthenticate.js";
|
|
2
4
|
import { createTokenCycler } from "./tokenCycler.js";
|
|
3
5
|
import { logger } from "./logger.js";
|
|
4
6
|
function verifyChallengeResource(scope, request) {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
} catch (e) {
|
|
9
|
-
throw new Error(`The challenge contains invalid scope '${scope}'`);
|
|
10
|
-
}
|
|
11
|
-
const requestUrl = new URL(request.url);
|
|
12
|
-
if (!requestUrl.hostname.endsWith(`.${scopeAsUrl.hostname}`)) {
|
|
13
|
-
throw new Error(
|
|
14
|
-
`The challenge resource '${scopeAsUrl.hostname}' does not match the requested domain. Set disableChallengeResourceVerification to true in your client options to disable. See https://aka.ms/azsdk/blog/vault-uri for more information.`
|
|
15
|
-
);
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
const keyVaultAuthenticationPolicyName = "keyVaultAuthenticationPolicy";
|
|
19
|
-
function keyVaultAuthenticationPolicy(credential, options = {}) {
|
|
20
|
-
const { disableChallengeResourceVerification } = options;
|
|
21
|
-
let challengeState = { status: "none" };
|
|
22
|
-
const getAccessToken = createTokenCycler(credential);
|
|
23
|
-
function requestToOptions(request) {
|
|
24
|
-
return {
|
|
25
|
-
abortSignal: request.abortSignal,
|
|
26
|
-
requestOptions: {
|
|
27
|
-
timeout: request.timeout > 0 ? request.timeout : void 0
|
|
28
|
-
},
|
|
29
|
-
tracingOptions: request.tracingOptions
|
|
30
|
-
};
|
|
31
|
-
}
|
|
32
|
-
async function authorizeRequest(request) {
|
|
33
|
-
const requestOptions = requestToOptions(request);
|
|
34
|
-
switch (challengeState.status) {
|
|
35
|
-
case "none":
|
|
36
|
-
challengeState = {
|
|
37
|
-
status: "started",
|
|
38
|
-
originalBody: request.body
|
|
39
|
-
};
|
|
40
|
-
request.body = null;
|
|
41
|
-
break;
|
|
42
|
-
case "started":
|
|
43
|
-
break;
|
|
44
|
-
// Retry, we should not overwrite the original body
|
|
45
|
-
case "complete": {
|
|
46
|
-
const token = await getAccessToken(challengeState.scopes, {
|
|
47
|
-
...requestOptions,
|
|
48
|
-
enableCae: true,
|
|
49
|
-
tenantId: challengeState.tenantId
|
|
50
|
-
});
|
|
51
|
-
if (token) {
|
|
52
|
-
request.headers.set("authorization", `Bearer ${token.token}`);
|
|
53
|
-
}
|
|
54
|
-
break;
|
|
55
|
-
}
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
async function handleChallenge(request, response, next) {
|
|
59
|
-
if (response.status !== 401) {
|
|
60
|
-
return response;
|
|
7
|
+
let scopeAsUrl;
|
|
8
|
+
try {
|
|
9
|
+
scopeAsUrl = new URL(scope);
|
|
61
10
|
}
|
|
62
|
-
|
|
63
|
-
|
|
11
|
+
catch (e) {
|
|
12
|
+
throw new Error(`The challenge contains invalid scope '${scope}'`);
|
|
64
13
|
}
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
logger.warning(
|
|
69
|
-
"keyVaultAuthentication policy encountered a 401 response without a corresponding WWW-Authenticate header. This is unexpected. Not handling the 401 response."
|
|
70
|
-
);
|
|
71
|
-
return response;
|
|
14
|
+
const requestUrl = new URL(request.url);
|
|
15
|
+
if (!requestUrl.hostname.endsWith(`.${scopeAsUrl.hostname}`)) {
|
|
16
|
+
throw new Error(`The challenge resource '${scopeAsUrl.hostname}' does not match the requested domain. Set disableChallengeResourceVerification to true in your client options to disable. See https://aka.ms/azsdk/blog/vault-uri for more information.`);
|
|
72
17
|
}
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Name of the Key Vault authentication policy.
|
|
21
|
+
*/
|
|
22
|
+
export const keyVaultAuthenticationPolicyName = "keyVaultAuthenticationPolicy";
|
|
23
|
+
/**
|
|
24
|
+
* A custom implementation of the bearer-token authentication policy that handles Key Vault and CAE challenges.
|
|
25
|
+
*
|
|
26
|
+
* Key Vault supports other authentication schemes, but we ensure challenge authentication
|
|
27
|
+
* is used by first sending a copy of the request, without authorization or content.
|
|
28
|
+
*
|
|
29
|
+
* when the challenge is received, it will be authenticated and used to send the original
|
|
30
|
+
* request with authorization.
|
|
31
|
+
*
|
|
32
|
+
* Following the first request of a client, follow-up requests will get the cached token
|
|
33
|
+
* if possible.
|
|
34
|
+
*
|
|
35
|
+
*/
|
|
36
|
+
export function keyVaultAuthenticationPolicy(credential, options = {}) {
|
|
37
|
+
const { disableChallengeResourceVerification } = options;
|
|
38
|
+
let challengeState = { status: "none" };
|
|
39
|
+
const getAccessToken = createTokenCycler(credential);
|
|
40
|
+
function requestToOptions(request) {
|
|
41
|
+
return {
|
|
42
|
+
abortSignal: request.abortSignal,
|
|
43
|
+
requestOptions: {
|
|
44
|
+
timeout: request.timeout > 0 ? request.timeout : undefined,
|
|
45
|
+
},
|
|
46
|
+
tracingOptions: request.tracingOptions,
|
|
47
|
+
};
|
|
88
48
|
}
|
|
89
|
-
request
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
49
|
+
async function authorizeRequest(request) {
|
|
50
|
+
const requestOptions = requestToOptions(request);
|
|
51
|
+
switch (challengeState.status) {
|
|
52
|
+
case "none":
|
|
53
|
+
challengeState = {
|
|
54
|
+
status: "started",
|
|
55
|
+
originalBody: request.body,
|
|
56
|
+
};
|
|
57
|
+
request.body = null;
|
|
58
|
+
break;
|
|
59
|
+
case "started":
|
|
60
|
+
break; // Retry, we should not overwrite the original body
|
|
61
|
+
case "complete": {
|
|
62
|
+
const token = await getAccessToken(challengeState.scopes, {
|
|
63
|
+
...requestOptions,
|
|
64
|
+
enableCae: true,
|
|
65
|
+
tenantId: challengeState.tenantId,
|
|
66
|
+
});
|
|
67
|
+
if (token) {
|
|
68
|
+
request.headers.set("authorization", `Bearer ${token.token}`);
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
100
73
|
}
|
|
101
|
-
|
|
102
|
-
|
|
74
|
+
async function handleChallenge(request, response, next) {
|
|
75
|
+
// If status is not 401, this is a no-op
|
|
76
|
+
if (response.status !== 401) {
|
|
77
|
+
return response;
|
|
78
|
+
}
|
|
79
|
+
if (request.body === null && challengeState.status === "started") {
|
|
80
|
+
// Reset the original body before doing anything else.
|
|
81
|
+
// Note: If successful status will be "complete", otherwise "none" will
|
|
82
|
+
// restart the process.
|
|
83
|
+
request.body = challengeState.originalBody;
|
|
84
|
+
}
|
|
85
|
+
const getTokenOptions = requestToOptions(request);
|
|
86
|
+
const challenge = response.headers.get("WWW-Authenticate");
|
|
87
|
+
if (!challenge) {
|
|
88
|
+
logger.warning("keyVaultAuthentication policy encountered a 401 response without a corresponding WWW-Authenticate header. This is unexpected. Not handling the 401 response.");
|
|
89
|
+
return response;
|
|
90
|
+
}
|
|
91
|
+
const parsedChallenge = parseWWWAuthenticateHeader(challenge);
|
|
92
|
+
const scope = parsedChallenge.resource
|
|
93
|
+
? parsedChallenge.resource + "/.default"
|
|
94
|
+
: parsedChallenge.scope;
|
|
95
|
+
if (!scope) {
|
|
96
|
+
// Cannot handle this kind of challenge here (if scope is not present, may be a CAE challenge)
|
|
97
|
+
return response;
|
|
98
|
+
}
|
|
99
|
+
if (!disableChallengeResourceVerification) {
|
|
100
|
+
verifyChallengeResource(scope, request);
|
|
101
|
+
}
|
|
102
|
+
const accessToken = await getAccessToken([scope], {
|
|
103
|
+
...getTokenOptions,
|
|
104
|
+
enableCae: true,
|
|
105
|
+
tenantId: parsedChallenge.tenantId,
|
|
106
|
+
});
|
|
107
|
+
if (!accessToken) {
|
|
108
|
+
// No access token provided, treat as no-op
|
|
109
|
+
return response;
|
|
110
|
+
}
|
|
111
|
+
request.headers.set("Authorization", `Bearer ${accessToken.token}`);
|
|
112
|
+
challengeState = {
|
|
113
|
+
status: "complete",
|
|
114
|
+
scopes: [scope],
|
|
115
|
+
tenantId: parsedChallenge.tenantId,
|
|
116
|
+
};
|
|
117
|
+
// We have a token now, so try send the request again
|
|
118
|
+
return next(request);
|
|
103
119
|
}
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
120
|
+
async function handleCaeChallenge(request, response, next) {
|
|
121
|
+
// Cannot handle CAE challenge if a regular challenge has not been completed first
|
|
122
|
+
if (challengeState.status !== "complete") {
|
|
123
|
+
return response;
|
|
124
|
+
}
|
|
125
|
+
// If status is not 401, this is a no-op
|
|
126
|
+
if (response.status !== 401) {
|
|
127
|
+
return response;
|
|
128
|
+
}
|
|
129
|
+
const getTokenOptions = requestToOptions(request);
|
|
130
|
+
const challenge = response.headers.get("WWW-Authenticate");
|
|
131
|
+
if (!challenge) {
|
|
132
|
+
return response;
|
|
133
|
+
}
|
|
134
|
+
const { claims: base64EncodedClaims, error } = parseWWWAuthenticateHeader(challenge);
|
|
135
|
+
if (error !== "insufficient_claims" || base64EncodedClaims === undefined) {
|
|
136
|
+
return response;
|
|
137
|
+
}
|
|
138
|
+
const claims = atob(base64EncodedClaims);
|
|
139
|
+
const accessToken = await getAccessToken(challengeState.scopes, {
|
|
140
|
+
...getTokenOptions,
|
|
141
|
+
enableCae: true,
|
|
142
|
+
tenantId: challengeState.tenantId,
|
|
143
|
+
claims,
|
|
144
|
+
});
|
|
145
|
+
request.headers.set("Authorization", `Bearer ${accessToken.token}`);
|
|
146
|
+
return next(request);
|
|
108
147
|
}
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
148
|
+
async function sendRequest(request, next) {
|
|
149
|
+
// Add token if possible
|
|
150
|
+
await authorizeRequest(request);
|
|
151
|
+
// Try send request (first attempt)
|
|
152
|
+
let response = await next(request);
|
|
153
|
+
// Handle standard challenge if present
|
|
154
|
+
response = await handleChallenge(request, response, next);
|
|
155
|
+
// Handle CAE challenge if present
|
|
156
|
+
response = await handleCaeChallenge(request, response, next);
|
|
157
|
+
return response;
|
|
112
158
|
}
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
tenantId: challengeState.tenantId,
|
|
118
|
-
claims
|
|
119
|
-
});
|
|
120
|
-
request.headers.set("Authorization", `Bearer ${accessToken.token}`);
|
|
121
|
-
return next(request);
|
|
122
|
-
}
|
|
123
|
-
async function sendRequest(request, next) {
|
|
124
|
-
await authorizeRequest(request);
|
|
125
|
-
let response = await next(request);
|
|
126
|
-
response = await handleChallenge(request, response, next);
|
|
127
|
-
response = await handleCaeChallenge(request, response, next);
|
|
128
|
-
return response;
|
|
129
|
-
}
|
|
130
|
-
return {
|
|
131
|
-
name: keyVaultAuthenticationPolicyName,
|
|
132
|
-
sendRequest
|
|
133
|
-
};
|
|
159
|
+
return {
|
|
160
|
+
name: keyVaultAuthenticationPolicyName,
|
|
161
|
+
sendRequest,
|
|
162
|
+
};
|
|
134
163
|
}
|
|
135
|
-
|
|
136
|
-
keyVaultAuthenticationPolicy,
|
|
137
|
-
keyVaultAuthenticationPolicyName
|
|
138
|
-
};
|
|
164
|
+
//# sourceMappingURL=keyVaultAuthenticationPolicy.js.map
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["/mnt/vss/_work/1/s/sdk/keyvault/keyvault-common/src/keyVaultAuthenticationPolicy.ts"],
|
|
4
|
-
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nimport type {\n PipelinePolicy,\n PipelineRequest,\n PipelineResponse,\n RequestBodyType,\n SendRequest,\n} from \"@azure/core-rest-pipeline\";\nimport type { WWWAuthenticate } from \"./parseWWWAuthenticate.js\";\nimport { parseWWWAuthenticateHeader } from \"./parseWWWAuthenticate.js\";\n\nimport type { GetTokenOptions, TokenCredential } from \"@azure/core-auth\";\nimport { createTokenCycler } from \"./tokenCycler.js\";\nimport { logger } from \"./logger.js\";\n\n/**\n * @internal\n * Holds the state of Challenge Auth.\n * When making the first request we force Key Vault to begin a challenge\n * by clearing out the request body and storing it locally.\n *\n * Later on, the authorizeRequestOnChallenge callback will process the\n * challenge and, if ready to resend the original request, reset the body\n * so that it may be sent again.\n *\n * Once a client has succeeded once, we can start skipping CAE.\n */\ntype ChallengeState =\n | {\n status: \"none\";\n }\n | {\n status: \"started\";\n originalBody?: RequestBodyType;\n }\n | {\n status: \"complete\";\n scopes: string[];\n tenantId?: string;\n };\n\n/**\n * Additional options for the challenge based authentication policy.\n */\nexport interface KeyVaultAuthenticationPolicyOptions {\n /**\n * Whether to disable verification that the challenge resource matches the Key Vault or Managed HSM domain.\n *\n * Defaults to false.\n */\n disableChallengeResourceVerification?: boolean;\n}\n\nfunction verifyChallengeResource(scope: string, request: PipelineRequest): void {\n let scopeAsUrl: URL;\n try {\n scopeAsUrl = new URL(scope);\n } catch (e) {\n throw new Error(`The challenge contains invalid scope '${scope}'`);\n }\n\n const requestUrl = new URL(request.url);\n\n if (!requestUrl.hostname.endsWith(`.${scopeAsUrl.hostname}`)) {\n throw new Error(\n `The challenge resource '${scopeAsUrl.hostname}' does not match the requested domain. Set disableChallengeResourceVerification to true in your client options to disable. See https://aka.ms/azsdk/blog/vault-uri for more information.`,\n );\n }\n}\n\n/**\n * Name of the Key Vault authentication policy.\n */\nexport const keyVaultAuthenticationPolicyName = \"keyVaultAuthenticationPolicy\";\n\n/**\n * A custom implementation of the bearer-token authentication policy that handles Key Vault and CAE challenges.\n *\n * Key Vault supports other authentication schemes, but we ensure challenge authentication\n * is used by first sending a copy of the request, without authorization or content.\n *\n * when the challenge is received, it will be authenticated and used to send the original\n * request with authorization.\n *\n * Following the first request of a client, follow-up requests will get the cached token\n * if possible.\n *\n */\nexport function keyVaultAuthenticationPolicy(\n credential: TokenCredential,\n options: KeyVaultAuthenticationPolicyOptions = {},\n): PipelinePolicy {\n const { disableChallengeResourceVerification } = options;\n let challengeState: ChallengeState = { status: \"none\" };\n const getAccessToken = createTokenCycler(credential);\n\n function requestToOptions(request: PipelineRequest): GetTokenOptions {\n return {\n abortSignal: request.abortSignal,\n requestOptions: {\n timeout: request.timeout > 0 ? request.timeout : undefined,\n },\n tracingOptions: request.tracingOptions,\n };\n }\n\n async function authorizeRequest(request: PipelineRequest): Promise<void> {\n const requestOptions: GetTokenOptions = requestToOptions(request);\n\n switch (challengeState.status) {\n case \"none\":\n challengeState = {\n status: \"started\",\n originalBody: request.body,\n };\n request.body = null;\n break;\n case \"started\":\n break; // Retry, we should not overwrite the original body\n case \"complete\": {\n const token = await getAccessToken(challengeState.scopes, {\n ...requestOptions,\n enableCae: true,\n tenantId: challengeState.tenantId,\n });\n if (token) {\n request.headers.set(\"authorization\", `Bearer ${token.token}`);\n }\n break;\n }\n }\n }\n\n async function handleChallenge(\n request: PipelineRequest,\n response: PipelineResponse,\n next: SendRequest,\n ): Promise<PipelineResponse> {\n // If status is not 401, this is a no-op\n if (response.status !== 401) {\n return response;\n }\n\n if (request.body === null && challengeState.status === \"started\") {\n // Reset the original body before doing anything else.\n // Note: If successful status will be \"complete\", otherwise \"none\" will\n // restart the process.\n request.body = challengeState.originalBody;\n }\n\n const getTokenOptions = requestToOptions(request);\n\n const challenge = response.headers.get(\"WWW-Authenticate\");\n if (!challenge) {\n logger.warning(\n \"keyVaultAuthentication policy encountered a 401 response without a corresponding WWW-Authenticate header. This is unexpected. Not handling the 401 response.\",\n );\n return response;\n }\n const parsedChallenge: WWWAuthenticate = parseWWWAuthenticateHeader(challenge);\n\n const scope = parsedChallenge.resource\n ? parsedChallenge.resource + \"/.default\"\n : parsedChallenge.scope;\n\n if (!scope) {\n // Cannot handle this kind of challenge here (if scope is not present, may be a CAE challenge)\n return response;\n }\n\n if (!disableChallengeResourceVerification) {\n verifyChallengeResource(scope, request);\n }\n\n const accessToken = await getAccessToken([scope], {\n ...getTokenOptions,\n enableCae: true,\n tenantId: parsedChallenge.tenantId,\n });\n\n if (!accessToken) {\n // No access token provided, treat as no-op\n return response;\n }\n\n request.headers.set(\"Authorization\", `Bearer ${accessToken.token}`);\n\n challengeState = {\n status: \"complete\",\n scopes: [scope],\n tenantId: parsedChallenge.tenantId,\n };\n\n // We have a token now, so try send the request again\n return next(request);\n }\n\n async function handleCaeChallenge(\n request: PipelineRequest,\n response: PipelineResponse,\n next: SendRequest,\n ): Promise<PipelineResponse> {\n // Cannot handle CAE challenge if a regular challenge has not been completed first\n if (challengeState.status !== \"complete\") {\n return response;\n }\n\n // If status is not 401, this is a no-op\n if (response.status !== 401) {\n return response;\n }\n\n const getTokenOptions = requestToOptions(request);\n\n const challenge = response.headers.get(\"WWW-Authenticate\");\n if (!challenge) {\n return response;\n }\n const { claims: base64EncodedClaims, error }: WWWAuthenticate =\n parseWWWAuthenticateHeader(challenge);\n\n if (error !== \"insufficient_claims\" || base64EncodedClaims === undefined) {\n return response;\n }\n\n const claims = atob(base64EncodedClaims);\n\n const accessToken = await getAccessToken(challengeState.scopes, {\n ...getTokenOptions,\n enableCae: true,\n tenantId: challengeState.tenantId,\n claims,\n });\n\n request.headers.set(\"Authorization\", `Bearer ${accessToken.token}`);\n\n return next(request);\n }\n\n async function sendRequest(\n request: PipelineRequest,\n next: SendRequest,\n ): Promise<PipelineResponse> {\n // Add token if possible\n await authorizeRequest(request);\n\n // Try send request (first attempt)\n let response = await next(request);\n\n // Handle standard challenge if present\n response = await handleChallenge(request, response, next);\n\n // Handle CAE challenge if present\n response = await handleCaeChallenge(request, response, next);\n\n return response;\n }\n\n return {\n name: keyVaultAuthenticationPolicyName,\n sendRequest,\n };\n}\n"],
|
|
5
|
-
"mappings": "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",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
1
|
+
{"version":3,"file":"keyVaultAuthenticationPolicy.js","sourceRoot":"","sources":["../../src/keyVaultAuthenticationPolicy.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC;AAUlC,OAAO,EAAE,0BAA0B,EAAE,MAAM,2BAA2B,CAAC;AAGvE,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AACrD,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAwCrC,SAAS,uBAAuB,CAAC,KAAa,EAAE,OAAwB;IACtE,IAAI,UAAe,CAAC;IACpB,IAAI,CAAC;QACH,UAAU,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,CAAC;IAC9B,CAAC;IAAC,OAAO,CAAC,EAAE,CAAC;QACX,MAAM,IAAI,KAAK,CAAC,yCAAyC,KAAK,GAAG,CAAC,CAAC;IACrE,CAAC;IAED,MAAM,UAAU,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC;IAExC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,QAAQ,CAAC,IAAI,UAAU,CAAC,QAAQ,EAAE,CAAC,EAAE,CAAC;QAC7D,MAAM,IAAI,KAAK,CACb,2BAA2B,UAAU,CAAC,QAAQ,0LAA0L,CACzO,CAAC;IACJ,CAAC;AACH,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,MAAM,gCAAgC,GAAG,8BAA8B,CAAC;AAE/E;;;;;;;;;;;;GAYG;AACH,MAAM,UAAU,4BAA4B,CAC1C,UAA2B,EAC3B,UAA+C,EAAE;IAEjD,MAAM,EAAE,oCAAoC,EAAE,GAAG,OAAO,CAAC;IACzD,IAAI,cAAc,GAAmB,EAAE,MAAM,EAAE,MAAM,EAAE,CAAC;IACxD,MAAM,cAAc,GAAG,iBAAiB,CAAC,UAAU,CAAC,CAAC;IAErD,SAAS,gBAAgB,CAAC,OAAwB;QAChD,OAAO;YACL,WAAW,EAAE,OAAO,CAAC,WAAW;YAChC,cAAc,EAAE;gBACd,OAAO,EAAE,OAAO,CAAC,OAAO,GAAG,CAAC,CAAC,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,SAAS;aAC3D;YACD,cAAc,EAAE,OAAO,CAAC,cAAc;SACvC,CAAC;IACJ,CAAC;IAED,KAAK,UAAU,gBAAgB,CAAC,OAAwB;QACtD,MAAM,cAAc,GAAoB,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAElE,QAAQ,cAAc,CAAC,MAAM,EAAE,CAAC;YAC9B,KAAK,MAAM;gBACT,cAAc,GAAG;oBACf,MAAM,EAAE,SAAS;oBACjB,YAAY,EAAE,OAAO,CAAC,IAAI;iBAC3B,CAAC;gBACF,OAAO,CAAC,IAAI,GAAG,IAAI,CAAC;gBACpB,MAAM;YACR,KAAK,SAAS;gBACZ,MAAM,CAAC,mDAAmD;YAC5D,KAAK,UAAU,CAAC,CAAC,CAAC;gBAChB,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,cAAc,CAAC,MAAM,EAAE;oBACxD,GAAG,cAAc;oBACjB,SAAS,EAAE,IAAI;oBACf,QAAQ,EAAE,cAAc,CAAC,QAAQ;iBAClC,CAAC,CAAC;gBACH,IAAI,KAAK,EAAE,CAAC;oBACV,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,KAAK,CAAC,KAAK,EAAE,CAAC,CAAC;gBAChE,CAAC;gBACD,MAAM;YACR,CAAC;QACH,CAAC;IACH,CAAC;IAED,KAAK,UAAU,eAAe,CAC5B,OAAwB,EACxB,QAA0B,EAC1B,IAAiB;QAEjB,wCAAwC;QACxC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,IAAI,OAAO,CAAC,IAAI,KAAK,IAAI,IAAI,cAAc,CAAC,MAAM,KAAK,SAAS,EAAE,CAAC;YACjE,sDAAsD;YACtD,uEAAuE;YACvE,uBAAuB;YACvB,OAAO,CAAC,IAAI,GAAG,cAAc,CAAC,YAAY,CAAC;QAC7C,CAAC;QAED,MAAM,eAAe,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAElD,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAC3D,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,MAAM,CAAC,OAAO,CACZ,8JAA8J,CAC/J,CAAC;YACF,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,MAAM,eAAe,GAAoB,0BAA0B,CAAC,SAAS,CAAC,CAAC;QAE/E,MAAM,KAAK,GAAG,eAAe,CAAC,QAAQ;YACpC,CAAC,CAAC,eAAe,CAAC,QAAQ,GAAG,WAAW;YACxC,CAAC,CAAC,eAAe,CAAC,KAAK,CAAC;QAE1B,IAAI,CAAC,KAAK,EAAE,CAAC;YACX,8FAA8F;YAC9F,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,IAAI,CAAC,oCAAoC,EAAE,CAAC;YAC1C,uBAAuB,CAAC,KAAK,EAAE,OAAO,CAAC,CAAC;QAC1C,CAAC;QAED,MAAM,WAAW,GAAG,MAAM,cAAc,CAAC,CAAC,KAAK,CAAC,EAAE;YAChD,GAAG,eAAe;YAClB,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,eAAe,CAAC,QAAQ;SACnC,CAAC,CAAC;QAEH,IAAI,CAAC,WAAW,EAAE,CAAC;YACjB,2CAA2C;YAC3C,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC;QAEpE,cAAc,GAAG;YACf,MAAM,EAAE,UAAU;YAClB,MAAM,EAAE,CAAC,KAAK,CAAC;YACf,QAAQ,EAAE,eAAe,CAAC,QAAQ;SACnC,CAAC;QAEF,qDAAqD;QACrD,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAED,KAAK,UAAU,kBAAkB,CAC/B,OAAwB,EACxB,QAA0B,EAC1B,IAAiB;QAEjB,kFAAkF;QAClF,IAAI,cAAc,CAAC,MAAM,KAAK,UAAU,EAAE,CAAC;YACzC,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,wCAAwC;QACxC,IAAI,QAAQ,CAAC,MAAM,KAAK,GAAG,EAAE,CAAC;YAC5B,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,MAAM,eAAe,GAAG,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAElD,MAAM,SAAS,GAAG,QAAQ,CAAC,OAAO,CAAC,GAAG,CAAC,kBAAkB,CAAC,CAAC;QAC3D,IAAI,CAAC,SAAS,EAAE,CAAC;YACf,OAAO,QAAQ,CAAC;QAClB,CAAC;QACD,MAAM,EAAE,MAAM,EAAE,mBAAmB,EAAE,KAAK,EAAE,GAC1C,0BAA0B,CAAC,SAAS,CAAC,CAAC;QAExC,IAAI,KAAK,KAAK,qBAAqB,IAAI,mBAAmB,KAAK,SAAS,EAAE,CAAC;YACzE,OAAO,QAAQ,CAAC;QAClB,CAAC;QAED,MAAM,MAAM,GAAG,IAAI,CAAC,mBAAmB,CAAC,CAAC;QAEzC,MAAM,WAAW,GAAG,MAAM,cAAc,CAAC,cAAc,CAAC,MAAM,EAAE;YAC9D,GAAG,eAAe;YAClB,SAAS,EAAE,IAAI;YACf,QAAQ,EAAE,cAAc,CAAC,QAAQ;YACjC,MAAM;SACP,CAAC,CAAC;QAEH,OAAO,CAAC,OAAO,CAAC,GAAG,CAAC,eAAe,EAAE,UAAU,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC;QAEpE,OAAO,IAAI,CAAC,OAAO,CAAC,CAAC;IACvB,CAAC;IAED,KAAK,UAAU,WAAW,CACxB,OAAwB,EACxB,IAAiB;QAEjB,wBAAwB;QACxB,MAAM,gBAAgB,CAAC,OAAO,CAAC,CAAC;QAEhC,mCAAmC;QACnC,IAAI,QAAQ,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC;QAEnC,uCAAuC;QACvC,QAAQ,GAAG,MAAM,eAAe,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;QAE1D,kCAAkC;QAClC,QAAQ,GAAG,MAAM,kBAAkB,CAAC,OAAO,EAAE,QAAQ,EAAE,IAAI,CAAC,CAAC;QAE7D,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,OAAO;QACL,IAAI,EAAE,gCAAgC;QACtC,WAAW;KACZ,CAAC;AACJ,CAAC","sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nimport type {\n PipelinePolicy,\n PipelineRequest,\n PipelineResponse,\n RequestBodyType,\n SendRequest,\n} from \"@azure/core-rest-pipeline\";\nimport type { WWWAuthenticate } from \"./parseWWWAuthenticate.js\";\nimport { parseWWWAuthenticateHeader } from \"./parseWWWAuthenticate.js\";\n\nimport type { GetTokenOptions, TokenCredential } from \"@azure/core-auth\";\nimport { createTokenCycler } from \"./tokenCycler.js\";\nimport { logger } from \"./logger.js\";\n\n/**\n * @internal\n * Holds the state of Challenge Auth.\n * When making the first request we force Key Vault to begin a challenge\n * by clearing out the request body and storing it locally.\n *\n * Later on, the authorizeRequestOnChallenge callback will process the\n * challenge and, if ready to resend the original request, reset the body\n * so that it may be sent again.\n *\n * Once a client has succeeded once, we can start skipping CAE.\n */\ntype ChallengeState =\n | {\n status: \"none\";\n }\n | {\n status: \"started\";\n originalBody?: RequestBodyType;\n }\n | {\n status: \"complete\";\n scopes: string[];\n tenantId?: string;\n };\n\n/**\n * Additional options for the challenge based authentication policy.\n */\nexport interface KeyVaultAuthenticationPolicyOptions {\n /**\n * Whether to disable verification that the challenge resource matches the Key Vault or Managed HSM domain.\n *\n * Defaults to false.\n */\n disableChallengeResourceVerification?: boolean;\n}\n\nfunction verifyChallengeResource(scope: string, request: PipelineRequest): void {\n let scopeAsUrl: URL;\n try {\n scopeAsUrl = new URL(scope);\n } catch (e) {\n throw new Error(`The challenge contains invalid scope '${scope}'`);\n }\n\n const requestUrl = new URL(request.url);\n\n if (!requestUrl.hostname.endsWith(`.${scopeAsUrl.hostname}`)) {\n throw new Error(\n `The challenge resource '${scopeAsUrl.hostname}' does not match the requested domain. Set disableChallengeResourceVerification to true in your client options to disable. See https://aka.ms/azsdk/blog/vault-uri for more information.`,\n );\n }\n}\n\n/**\n * Name of the Key Vault authentication policy.\n */\nexport const keyVaultAuthenticationPolicyName = \"keyVaultAuthenticationPolicy\";\n\n/**\n * A custom implementation of the bearer-token authentication policy that handles Key Vault and CAE challenges.\n *\n * Key Vault supports other authentication schemes, but we ensure challenge authentication\n * is used by first sending a copy of the request, without authorization or content.\n *\n * when the challenge is received, it will be authenticated and used to send the original\n * request with authorization.\n *\n * Following the first request of a client, follow-up requests will get the cached token\n * if possible.\n *\n */\nexport function keyVaultAuthenticationPolicy(\n credential: TokenCredential,\n options: KeyVaultAuthenticationPolicyOptions = {},\n): PipelinePolicy {\n const { disableChallengeResourceVerification } = options;\n let challengeState: ChallengeState = { status: \"none\" };\n const getAccessToken = createTokenCycler(credential);\n\n function requestToOptions(request: PipelineRequest): GetTokenOptions {\n return {\n abortSignal: request.abortSignal,\n requestOptions: {\n timeout: request.timeout > 0 ? request.timeout : undefined,\n },\n tracingOptions: request.tracingOptions,\n };\n }\n\n async function authorizeRequest(request: PipelineRequest): Promise<void> {\n const requestOptions: GetTokenOptions = requestToOptions(request);\n\n switch (challengeState.status) {\n case \"none\":\n challengeState = {\n status: \"started\",\n originalBody: request.body,\n };\n request.body = null;\n break;\n case \"started\":\n break; // Retry, we should not overwrite the original body\n case \"complete\": {\n const token = await getAccessToken(challengeState.scopes, {\n ...requestOptions,\n enableCae: true,\n tenantId: challengeState.tenantId,\n });\n if (token) {\n request.headers.set(\"authorization\", `Bearer ${token.token}`);\n }\n break;\n }\n }\n }\n\n async function handleChallenge(\n request: PipelineRequest,\n response: PipelineResponse,\n next: SendRequest,\n ): Promise<PipelineResponse> {\n // If status is not 401, this is a no-op\n if (response.status !== 401) {\n return response;\n }\n\n if (request.body === null && challengeState.status === \"started\") {\n // Reset the original body before doing anything else.\n // Note: If successful status will be \"complete\", otherwise \"none\" will\n // restart the process.\n request.body = challengeState.originalBody;\n }\n\n const getTokenOptions = requestToOptions(request);\n\n const challenge = response.headers.get(\"WWW-Authenticate\");\n if (!challenge) {\n logger.warning(\n \"keyVaultAuthentication policy encountered a 401 response without a corresponding WWW-Authenticate header. This is unexpected. Not handling the 401 response.\",\n );\n return response;\n }\n const parsedChallenge: WWWAuthenticate = parseWWWAuthenticateHeader(challenge);\n\n const scope = parsedChallenge.resource\n ? parsedChallenge.resource + \"/.default\"\n : parsedChallenge.scope;\n\n if (!scope) {\n // Cannot handle this kind of challenge here (if scope is not present, may be a CAE challenge)\n return response;\n }\n\n if (!disableChallengeResourceVerification) {\n verifyChallengeResource(scope, request);\n }\n\n const accessToken = await getAccessToken([scope], {\n ...getTokenOptions,\n enableCae: true,\n tenantId: parsedChallenge.tenantId,\n });\n\n if (!accessToken) {\n // No access token provided, treat as no-op\n return response;\n }\n\n request.headers.set(\"Authorization\", `Bearer ${accessToken.token}`);\n\n challengeState = {\n status: \"complete\",\n scopes: [scope],\n tenantId: parsedChallenge.tenantId,\n };\n\n // We have a token now, so try send the request again\n return next(request);\n }\n\n async function handleCaeChallenge(\n request: PipelineRequest,\n response: PipelineResponse,\n next: SendRequest,\n ): Promise<PipelineResponse> {\n // Cannot handle CAE challenge if a regular challenge has not been completed first\n if (challengeState.status !== \"complete\") {\n return response;\n }\n\n // If status is not 401, this is a no-op\n if (response.status !== 401) {\n return response;\n }\n\n const getTokenOptions = requestToOptions(request);\n\n const challenge = response.headers.get(\"WWW-Authenticate\");\n if (!challenge) {\n return response;\n }\n const { claims: base64EncodedClaims, error }: WWWAuthenticate =\n parseWWWAuthenticateHeader(challenge);\n\n if (error !== \"insufficient_claims\" || base64EncodedClaims === undefined) {\n return response;\n }\n\n const claims = atob(base64EncodedClaims);\n\n const accessToken = await getAccessToken(challengeState.scopes, {\n ...getTokenOptions,\n enableCae: true,\n tenantId: challengeState.tenantId,\n claims,\n });\n\n request.headers.set(\"Authorization\", `Bearer ${accessToken.token}`);\n\n return next(request);\n }\n\n async function sendRequest(\n request: PipelineRequest,\n next: SendRequest,\n ): Promise<PipelineResponse> {\n // Add token if possible\n await authorizeRequest(request);\n\n // Try send request (first attempt)\n let response = await next(request);\n\n // Handle standard challenge if present\n response = await handleChallenge(request, response, next);\n\n // Handle CAE challenge if present\n response = await handleCaeChallenge(request, response, next);\n\n return response;\n }\n\n return {\n name: keyVaultAuthenticationPolicyName,\n sendRequest,\n };\n}\n"]}
|
package/dist/esm/logger.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
1
3
|
import { createClientLogger } from "@azure/logger";
|
|
2
|
-
const logger = createClientLogger("keyvault-common");
|
|
3
|
-
|
|
4
|
-
logger
|
|
5
|
-
};
|
|
4
|
+
export const logger = createClientLogger("keyvault-common");
|
|
5
|
+
//# sourceMappingURL=logger.js.map
|
package/dist/esm/logger.js.map
CHANGED
|
@@ -1,7 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["/mnt/vss/_work/1/s/sdk/keyvault/keyvault-common/src/logger.ts"],
|
|
4
|
-
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nimport { createClientLogger } from \"@azure/logger\";\n\nexport const logger = createClientLogger(\"keyvault-common\");\n"],
|
|
5
|
-
"mappings": "AAGA,SAAS,0BAA0B;AAE5B,MAAM,SAAS,mBAAmB,iBAAiB;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/logger.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC;AAElC,OAAO,EAAE,kBAAkB,EAAE,MAAM,eAAe,CAAC;AAEnD,MAAM,CAAC,MAAM,MAAM,GAAG,kBAAkB,CAAC,iBAAiB,CAAC,CAAC","sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\nimport { createClientLogger } from \"@azure/logger\";\n\nexport const logger = createClientLogger(\"keyvault-common\");\n"]}
|
|
@@ -1,36 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
3
|
+
/**
|
|
4
|
+
* Parses a Key Vault identifier into its components.
|
|
5
|
+
*
|
|
6
|
+
* @param collection - The collection of the Key Vault identifier.
|
|
7
|
+
* @param identifier - The Key Vault identifier to be parsed.
|
|
8
|
+
*/
|
|
9
|
+
export function parseKeyVaultIdentifier(collection, identifier) {
|
|
10
|
+
if (typeof collection !== "string" || !(collection = collection.trim())) {
|
|
11
|
+
throw new Error("Invalid collection argument");
|
|
12
|
+
}
|
|
13
|
+
if (typeof identifier !== "string" || !(identifier = identifier.trim())) {
|
|
14
|
+
throw new Error("Invalid identifier argument");
|
|
15
|
+
}
|
|
16
|
+
let baseUri;
|
|
17
|
+
try {
|
|
18
|
+
baseUri = new URL(identifier);
|
|
19
|
+
}
|
|
20
|
+
catch (e) {
|
|
21
|
+
throw new Error(`Invalid ${collection} identifier: ${identifier}. Not a valid URI`);
|
|
22
|
+
}
|
|
23
|
+
// Path is of the form '/collection/name[/version]'
|
|
24
|
+
const segments = (baseUri.pathname || "").split("/");
|
|
25
|
+
if (segments.length !== 3 && segments.length !== 4) {
|
|
26
|
+
throw new Error(`Invalid ${collection} identifier: ${identifier}. Bad number of segments: ${segments.length}`);
|
|
27
|
+
}
|
|
28
|
+
if (collection !== segments[1]) {
|
|
29
|
+
throw new Error(`Invalid ${collection} identifier: ${identifier}. segment [1] should be "${collection}", found "${segments[1]}"`);
|
|
30
|
+
}
|
|
31
|
+
const vaultUrl = `${baseUri.protocol}//${baseUri.host}`;
|
|
32
|
+
const name = segments[2];
|
|
33
|
+
const version = segments.length === 4 ? segments[3] : undefined;
|
|
34
|
+
return {
|
|
35
|
+
vaultUrl,
|
|
36
|
+
name,
|
|
37
|
+
version,
|
|
38
|
+
};
|
|
33
39
|
}
|
|
34
|
-
|
|
35
|
-
parseKeyVaultIdentifier
|
|
36
|
-
};
|
|
40
|
+
//# sourceMappingURL=parseKeyVaultIdentifier.js.map
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["/mnt/vss/_work/1/s/sdk/keyvault/keyvault-common/src/parseKeyVaultIdentifier.ts"],
|
|
4
|
-
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n/**\n * The parsed components of a Key Vault entity identifier.\n */\nexport interface KeyVaultEntityIdentifier {\n /**\n * The vault URI.\n */\n vaultUrl: string;\n /**\n * The version of key/secret/certificate. May be undefined.\n */\n version?: string;\n /**\n * The name of key/secret/certificate.\n */\n name: string;\n}\n\n/**\n * Parses a Key Vault identifier into its components.\n *\n * @param collection - The collection of the Key Vault identifier.\n * @param identifier - The Key Vault identifier to be parsed.\n */\nexport function parseKeyVaultIdentifier(\n collection: string,\n identifier: string | undefined,\n): KeyVaultEntityIdentifier {\n if (typeof collection !== \"string\" || !(collection = collection.trim())) {\n throw new Error(\"Invalid collection argument\");\n }\n\n if (typeof identifier !== \"string\" || !(identifier = identifier.trim())) {\n throw new Error(\"Invalid identifier argument\");\n }\n\n let baseUri;\n try {\n baseUri = new URL(identifier);\n } catch (e: any) {\n throw new Error(`Invalid ${collection} identifier: ${identifier}. Not a valid URI`);\n }\n\n // Path is of the form '/collection/name[/version]'\n const segments = (baseUri.pathname || \"\").split(\"/\");\n if (segments.length !== 3 && segments.length !== 4) {\n throw new Error(\n `Invalid ${collection} identifier: ${identifier}. Bad number of segments: ${segments.length}`,\n );\n }\n\n if (collection !== segments[1]) {\n throw new Error(\n `Invalid ${collection} identifier: ${identifier}. segment [1] should be \"${collection}\", found \"${segments[1]}\"`,\n );\n }\n\n const vaultUrl = `${baseUri.protocol}//${baseUri.host}`;\n const name = segments[2];\n const version = segments.length === 4 ? segments[3] : undefined;\n return {\n vaultUrl,\n name,\n version,\n };\n}\n"],
|
|
5
|
-
"mappings": "AA2BO,SAAS,wBACd,YACA,YAC0B;AAC1B,MAAI,OAAO,eAAe,YAAY,EAAE,aAAa,WAAW,KAAK,IAAI;AACvE,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC/C;AAEA,MAAI,OAAO,eAAe,YAAY,EAAE,aAAa,WAAW,KAAK,IAAI;AACvE,UAAM,IAAI,MAAM,6BAA6B;AAAA,EAC/C;AAEA,MAAI;AACJ,MAAI;AACF,cAAU,IAAI,IAAI,UAAU;AAAA,EAC9B,SAAS,GAAQ;AACf,UAAM,IAAI,MAAM,WAAW,UAAU,gBAAgB,UAAU,mBAAmB;AAAA,EACpF;AAGA,QAAM,YAAY,QAAQ,YAAY,IAAI,MAAM,GAAG;AACnD,MAAI,SAAS,WAAW,KAAK,SAAS,WAAW,GAAG;AAClD,UAAM,IAAI;AAAA,MACR,WAAW,UAAU,gBAAgB,UAAU,6BAA6B,SAAS,MAAM;AAAA,IAC7F;AAAA,EACF;AAEA,MAAI,eAAe,SAAS,CAAC,GAAG;AAC9B,UAAM,IAAI;AAAA,MACR,WAAW,UAAU,gBAAgB,UAAU,4BAA4B,UAAU,aAAa,SAAS,CAAC,CAAC;AAAA,IAC/G;AAAA,EACF;AAEA,QAAM,WAAW,GAAG,QAAQ,QAAQ,KAAK,QAAQ,IAAI;AACrD,QAAM,OAAO,SAAS,CAAC;AACvB,QAAM,UAAU,SAAS,WAAW,IAAI,SAAS,CAAC,IAAI;AACtD,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA;AAAA,EACF;AACF;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
1
|
+
{"version":3,"file":"parseKeyVaultIdentifier.js","sourceRoot":"","sources":["../../src/parseKeyVaultIdentifier.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC;AAoBlC;;;;;GAKG;AACH,MAAM,UAAU,uBAAuB,CACrC,UAAkB,EAClB,UAA8B;IAE9B,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,CAAC,UAAU,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,OAAO,UAAU,KAAK,QAAQ,IAAI,CAAC,CAAC,UAAU,GAAG,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QACxE,MAAM,IAAI,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACjD,CAAC;IAED,IAAI,OAAO,CAAC;IACZ,IAAI,CAAC;QACH,OAAO,GAAG,IAAI,GAAG,CAAC,UAAU,CAAC,CAAC;IAChC,CAAC;IAAC,OAAO,CAAM,EAAE,CAAC;QAChB,MAAM,IAAI,KAAK,CAAC,WAAW,UAAU,gBAAgB,UAAU,mBAAmB,CAAC,CAAC;IACtF,CAAC;IAED,mDAAmD;IACnD,MAAM,QAAQ,GAAG,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;IACrD,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,IAAI,QAAQ,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QACnD,MAAM,IAAI,KAAK,CACb,WAAW,UAAU,gBAAgB,UAAU,6BAA6B,QAAQ,CAAC,MAAM,EAAE,CAC9F,CAAC;IACJ,CAAC;IAED,IAAI,UAAU,KAAK,QAAQ,CAAC,CAAC,CAAC,EAAE,CAAC;QAC/B,MAAM,IAAI,KAAK,CACb,WAAW,UAAU,gBAAgB,UAAU,4BAA4B,UAAU,aAAa,QAAQ,CAAC,CAAC,CAAC,GAAG,CACjH,CAAC;IACJ,CAAC;IAED,MAAM,QAAQ,GAAG,GAAG,OAAO,CAAC,QAAQ,KAAK,OAAO,CAAC,IAAI,EAAE,CAAC;IACxD,MAAM,IAAI,GAAG,QAAQ,CAAC,CAAC,CAAC,CAAC;IACzB,MAAM,OAAO,GAAG,QAAQ,CAAC,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,SAAS,CAAC;IAChE,OAAO;QACL,QAAQ;QACR,IAAI;QACJ,OAAO;KACR,CAAC;AACJ,CAAC","sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n/**\n * The parsed components of a Key Vault entity identifier.\n */\nexport interface KeyVaultEntityIdentifier {\n /**\n * The vault URI.\n */\n vaultUrl: string;\n /**\n * The version of key/secret/certificate. May be undefined.\n */\n version?: string;\n /**\n * The name of key/secret/certificate.\n */\n name: string;\n}\n\n/**\n * Parses a Key Vault identifier into its components.\n *\n * @param collection - The collection of the Key Vault identifier.\n * @param identifier - The Key Vault identifier to be parsed.\n */\nexport function parseKeyVaultIdentifier(\n collection: string,\n identifier: string | undefined,\n): KeyVaultEntityIdentifier {\n if (typeof collection !== \"string\" || !(collection = collection.trim())) {\n throw new Error(\"Invalid collection argument\");\n }\n\n if (typeof identifier !== \"string\" || !(identifier = identifier.trim())) {\n throw new Error(\"Invalid identifier argument\");\n }\n\n let baseUri;\n try {\n baseUri = new URL(identifier);\n } catch (e: any) {\n throw new Error(`Invalid ${collection} identifier: ${identifier}. Not a valid URI`);\n }\n\n // Path is of the form '/collection/name[/version]'\n const segments = (baseUri.pathname || \"\").split(\"/\");\n if (segments.length !== 3 && segments.length !== 4) {\n throw new Error(\n `Invalid ${collection} identifier: ${identifier}. Bad number of segments: ${segments.length}`,\n );\n }\n\n if (collection !== segments[1]) {\n throw new Error(\n `Invalid ${collection} identifier: ${identifier}. segment [1] should be \"${collection}\", found \"${segments[1]}\"`,\n );\n }\n\n const vaultUrl = `${baseUri.protocol}//${baseUri.host}`;\n const name = segments[2];\n const version = segments.length === 4 ? segments[3] : undefined;\n return {\n vaultUrl,\n name,\n version,\n };\n}\n"]}
|
|
@@ -1,35 +1,47 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
1
3
|
const validWWWAuthenticateProperties = [
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
4
|
+
"authorization",
|
|
5
|
+
"authorization_url",
|
|
6
|
+
"resource",
|
|
7
|
+
"scope",
|
|
8
|
+
"tenantId",
|
|
9
|
+
"claims",
|
|
10
|
+
"error",
|
|
9
11
|
];
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
12
|
+
/**
|
|
13
|
+
* Parses an WWW-Authenticate response header.
|
|
14
|
+
* This transforms a string value like:
|
|
15
|
+
* `Bearer authorization="https://some.url/tenantId", resource="https://some.url"`
|
|
16
|
+
* into an object like:
|
|
17
|
+
* `{ authorization: "https://some.url/tenantId", resource: "https://some.url" }`
|
|
18
|
+
* @param headerValue - String value in the WWW-Authenticate header
|
|
19
|
+
*/
|
|
20
|
+
export function parseWWWAuthenticateHeader(headerValue) {
|
|
21
|
+
const pairDelimiter = /,? +/;
|
|
22
|
+
const parsed = headerValue.split(pairDelimiter).reduce((kvPairs, p) => {
|
|
23
|
+
if (p.match(/\w="/)) {
|
|
24
|
+
// 'sampleKey="sample_value"' -> [sampleKey, "sample_value"] -> { sampleKey: sample_value }
|
|
25
|
+
const [key, ...value] = p.split("=");
|
|
26
|
+
if (validWWWAuthenticateProperties.includes(key)) {
|
|
27
|
+
// The values will be wrapped in quotes, which need to be stripped out.
|
|
28
|
+
return { ...kvPairs, [key]: value.join("=").slice(1, -1) };
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
return kvPairs;
|
|
32
|
+
}, {});
|
|
33
|
+
// Finally, we pull the tenantId from the authorization header to support multi-tenant authentication.
|
|
34
|
+
if (parsed.authorization) {
|
|
35
|
+
try {
|
|
36
|
+
const tenantId = new URL(parsed.authorization).pathname.substring(1);
|
|
37
|
+
if (tenantId) {
|
|
38
|
+
parsed.tenantId = tenantId;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
catch (_) {
|
|
42
|
+
throw new Error(`The challenge authorization URI '${parsed.authorization}' is invalid.`);
|
|
43
|
+
}
|
|
18
44
|
}
|
|
19
|
-
return
|
|
20
|
-
}, {});
|
|
21
|
-
if (parsed.authorization) {
|
|
22
|
-
try {
|
|
23
|
-
const tenantId = new URL(parsed.authorization).pathname.substring(1);
|
|
24
|
-
if (tenantId) {
|
|
25
|
-
parsed.tenantId = tenantId;
|
|
26
|
-
}
|
|
27
|
-
} catch (_) {
|
|
28
|
-
throw new Error(`The challenge authorization URI '${parsed.authorization}' is invalid.`);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return parsed;
|
|
45
|
+
return parsed;
|
|
32
46
|
}
|
|
33
|
-
|
|
34
|
-
parseWWWAuthenticateHeader
|
|
35
|
-
};
|
|
47
|
+
//# sourceMappingURL=parseWWWAuthenticate.js.map
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["/mnt/vss/_work/1/s/sdk/keyvault/keyvault-common/src/parseWWWAuthenticate.ts"],
|
|
4
|
-
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n/**\n * Parameters parsed out of the WWW-Authenticate header value by the parseWWWAuthenticate function.\n */\nexport interface WWWAuthenticate {\n /**\n * The authorization parameter, if present.\n */\n authorization?: string;\n\n /**\n * The authorization_url parameter, if present.\n */\n authorization_url?: string;\n\n /**\n * The resource parameter, if present.\n */\n resource?: string;\n\n /**\n * The scope parameter, if present.\n */\n scope?: string;\n\n /**\n * The tenantId parameter, if present.\n */\n tenantId?: string;\n\n /**\n * The claims parameter, if present.\n */\n claims?: string;\n\n /**\n * The error parameter, if present.\n */\n error?: string;\n}\n\nconst validWWWAuthenticateProperties: readonly (keyof WWWAuthenticate)[] = [\n \"authorization\",\n \"authorization_url\",\n \"resource\",\n \"scope\",\n \"tenantId\",\n \"claims\",\n \"error\",\n] as const;\n\n/**\n * Parses an WWW-Authenticate response header.\n * This transforms a string value like:\n * `Bearer authorization=\"https://some.url/tenantId\", resource=\"https://some.url\"`\n * into an object like:\n * `{ authorization: \"https://some.url/tenantId\", resource: \"https://some.url\" }`\n * @param headerValue - String value in the WWW-Authenticate header\n */\nexport function parseWWWAuthenticateHeader(headerValue: string): WWWAuthenticate {\n const pairDelimiter = /,? +/;\n const parsed = headerValue.split(pairDelimiter).reduce<WWWAuthenticate>((kvPairs, p) => {\n if (p.match(/\\w=\"/)) {\n // 'sampleKey=\"sample_value\"' -> [sampleKey, \"sample_value\"] -> { sampleKey: sample_value }\n const [key, ...value] = p.split(\"=\");\n if (validWWWAuthenticateProperties.includes(key as keyof WWWAuthenticate)) {\n // The values will be wrapped in quotes, which need to be stripped out.\n return { ...kvPairs, [key]: value.join(\"=\").slice(1, -1) };\n }\n }\n return kvPairs;\n }, {});\n\n // Finally, we pull the tenantId from the authorization header to support multi-tenant authentication.\n if (parsed.authorization) {\n try {\n const tenantId = new URL(parsed.authorization).pathname.substring(1);\n if (tenantId) {\n parsed.tenantId = tenantId;\n }\n } catch (_) {\n throw new Error(`The challenge authorization URI '${parsed.authorization}' is invalid.`);\n }\n }\n\n return parsed;\n}\n"],
|
|
5
|
-
"mappings": "AA2CA,MAAM,iCAAqE;AAAA,EACzE;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AAAA,EACA;AACF;AAUO,SAAS,2BAA2B,aAAsC;AAC/E,QAAM,gBAAgB;AACtB,QAAM,SAAS,YAAY,MAAM,aAAa,EAAE,OAAwB,CAAC,SAAS,MAAM;AACtF,QAAI,EAAE,MAAM,MAAM,GAAG;AAEnB,YAAM,CAAC,KAAK,GAAG,KAAK,IAAI,EAAE,MAAM,GAAG;AACnC,UAAI,+BAA+B,SAAS,GAA4B,GAAG;AAEzE,eAAO,EAAE,GAAG,SAAS,CAAC,GAAG,GAAG,MAAM,KAAK,GAAG,EAAE,MAAM,GAAG,EAAE,EAAE;AAAA,MAC3D;AAAA,IACF;AACA,WAAO;AAAA,EACT,GAAG,CAAC,CAAC;AAGL,MAAI,OAAO,eAAe;AACxB,QAAI;AACF,YAAM,WAAW,IAAI,IAAI,OAAO,aAAa,EAAE,SAAS,UAAU,CAAC;AACnE,UAAI,UAAU;AACZ,eAAO,WAAW;AAAA,MACpB;AAAA,IACF,SAAS,GAAG;AACV,YAAM,IAAI,MAAM,oCAAoC,OAAO,aAAa,eAAe;AAAA,IACzF;AAAA,EACF;AAEA,SAAO;AACT;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
1
|
+
{"version":3,"file":"parseWWWAuthenticate.js","sourceRoot":"","sources":["../../src/parseWWWAuthenticate.ts"],"names":[],"mappings":"AAAA,uCAAuC;AACvC,kCAAkC;AA0ClC,MAAM,8BAA8B,GAAuC;IACzE,eAAe;IACf,mBAAmB;IACnB,UAAU;IACV,OAAO;IACP,UAAU;IACV,QAAQ;IACR,OAAO;CACC,CAAC;AAEX;;;;;;;GAOG;AACH,MAAM,UAAU,0BAA0B,CAAC,WAAmB;IAC5D,MAAM,aAAa,GAAG,MAAM,CAAC;IAC7B,MAAM,MAAM,GAAG,WAAW,CAAC,KAAK,CAAC,aAAa,CAAC,CAAC,MAAM,CAAkB,CAAC,OAAO,EAAE,CAAC,EAAE,EAAE;QACrF,IAAI,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,EAAE,CAAC;YACpB,2FAA2F;YAC3F,MAAM,CAAC,GAAG,EAAE,GAAG,KAAK,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC;YACrC,IAAI,8BAA8B,CAAC,QAAQ,CAAC,GAA4B,CAAC,EAAE,CAAC;gBAC1E,uEAAuE;gBACvE,OAAO,EAAE,GAAG,OAAO,EAAE,CAAC,GAAG,CAAC,EAAE,KAAK,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;YAC7D,CAAC;QACH,CAAC;QACD,OAAO,OAAO,CAAC;IACjB,CAAC,EAAE,EAAE,CAAC,CAAC;IAEP,sGAAsG;IACtG,IAAI,MAAM,CAAC,aAAa,EAAE,CAAC;QACzB,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,aAAa,CAAC,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;YACrE,IAAI,QAAQ,EAAE,CAAC;gBACb,MAAM,CAAC,QAAQ,GAAG,QAAQ,CAAC;YAC7B,CAAC;QACH,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,MAAM,IAAI,KAAK,CAAC,oCAAoC,MAAM,CAAC,aAAa,eAAe,CAAC,CAAC;QAC3F,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC","sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n/**\n * Parameters parsed out of the WWW-Authenticate header value by the parseWWWAuthenticate function.\n */\nexport interface WWWAuthenticate {\n /**\n * The authorization parameter, if present.\n */\n authorization?: string;\n\n /**\n * The authorization_url parameter, if present.\n */\n authorization_url?: string;\n\n /**\n * The resource parameter, if present.\n */\n resource?: string;\n\n /**\n * The scope parameter, if present.\n */\n scope?: string;\n\n /**\n * The tenantId parameter, if present.\n */\n tenantId?: string;\n\n /**\n * The claims parameter, if present.\n */\n claims?: string;\n\n /**\n * The error parameter, if present.\n */\n error?: string;\n}\n\nconst validWWWAuthenticateProperties: readonly (keyof WWWAuthenticate)[] = [\n \"authorization\",\n \"authorization_url\",\n \"resource\",\n \"scope\",\n \"tenantId\",\n \"claims\",\n \"error\",\n] as const;\n\n/**\n * Parses an WWW-Authenticate response header.\n * This transforms a string value like:\n * `Bearer authorization=\"https://some.url/tenantId\", resource=\"https://some.url\"`\n * into an object like:\n * `{ authorization: \"https://some.url/tenantId\", resource: \"https://some.url\" }`\n * @param headerValue - String value in the WWW-Authenticate header\n */\nexport function parseWWWAuthenticateHeader(headerValue: string): WWWAuthenticate {\n const pairDelimiter = /,? +/;\n const parsed = headerValue.split(pairDelimiter).reduce<WWWAuthenticate>((kvPairs, p) => {\n if (p.match(/\\w=\"/)) {\n // 'sampleKey=\"sample_value\"' -> [sampleKey, \"sample_value\"] -> { sampleKey: sample_value }\n const [key, ...value] = p.split(\"=\");\n if (validWWWAuthenticateProperties.includes(key as keyof WWWAuthenticate)) {\n // The values will be wrapped in quotes, which need to be stripped out.\n return { ...kvPairs, [key]: value.join(\"=\").slice(1, -1) };\n }\n }\n return kvPairs;\n }, {});\n\n // Finally, we pull the tenantId from the authorization header to support multi-tenant authentication.\n if (parsed.authorization) {\n try {\n const tenantId = new URL(parsed.authorization).pathname.substring(1);\n if (tenantId) {\n parsed.tenantId = tenantId;\n }\n } catch (_) {\n throw new Error(`The challenge authorization URI '${parsed.authorization}' is invalid.`);\n }\n }\n\n return parsed;\n}\n"]}
|
package/dist/esm/tokenCycler.js
CHANGED
|
@@ -1,110 +1,163 @@
|
|
|
1
|
+
// Copyright (c) Microsoft Corporation.
|
|
2
|
+
// Licensed under the MIT License.
|
|
1
3
|
import { delay } from "@azure/core-util";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
refreshWindowInMs: 1e3 * 60 * 2
|
|
8
|
-
// Start refreshing 2m before expiry
|
|
4
|
+
// Default options for the cycler if none are provided
|
|
5
|
+
export const DEFAULT_CYCLER_OPTIONS = {
|
|
6
|
+
forcedRefreshWindowInMs: 1000, // Force waiting for a refresh 1s before the token expires
|
|
7
|
+
retryIntervalInMs: 3000, // Allow refresh attempts every 3s
|
|
8
|
+
refreshWindowInMs: 1000 * 60 * 2, // Start refreshing 2m before expiry
|
|
9
9
|
};
|
|
10
|
+
/**
|
|
11
|
+
* Converts an an unreliable access token getter (which may resolve with null)
|
|
12
|
+
* into an AccessTokenGetter by retrying the unreliable getter in a regular
|
|
13
|
+
* interval.
|
|
14
|
+
*
|
|
15
|
+
* @param getAccessToken - A function that produces a promise of an access token that may fail by returning null.
|
|
16
|
+
* @param retryIntervalInMs - The time (in milliseconds) to wait between retry attempts.
|
|
17
|
+
* @param refreshTimeout - The timestamp after which the refresh attempt will fail, throwing an exception.
|
|
18
|
+
* @returns - A promise that, if it resolves, will resolve with an access token.
|
|
19
|
+
*/
|
|
10
20
|
async function beginRefresh(getAccessToken, retryIntervalInMs, refreshTimeout) {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
21
|
+
// This wrapper handles exceptions gracefully as long as we haven't exceeded
|
|
22
|
+
// the timeout.
|
|
23
|
+
async function tryGetAccessToken() {
|
|
24
|
+
if (Date.now() < refreshTimeout) {
|
|
25
|
+
try {
|
|
26
|
+
return await getAccessToken();
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return null;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
else {
|
|
33
|
+
const finalToken = await getAccessToken();
|
|
34
|
+
// Timeout is up, so throw if it's still null
|
|
35
|
+
if (finalToken === null) {
|
|
36
|
+
throw new Error("Failed to refresh access token.");
|
|
37
|
+
}
|
|
38
|
+
return finalToken;
|
|
39
|
+
}
|
|
24
40
|
}
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
return token;
|
|
41
|
+
let token = await tryGetAccessToken();
|
|
42
|
+
while (token === null) {
|
|
43
|
+
await delay(retryIntervalInMs);
|
|
44
|
+
token = await tryGetAccessToken();
|
|
45
|
+
}
|
|
46
|
+
return token;
|
|
32
47
|
}
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
48
|
+
/**
|
|
49
|
+
* Creates a token cycler from a credential, scopes, and optional settings.
|
|
50
|
+
*
|
|
51
|
+
* A token cycler represents a way to reliably retrieve a valid access token
|
|
52
|
+
* from a TokenCredential. It will handle initializing the token, refreshing it
|
|
53
|
+
* when it nears expiration, and synchronizes refresh attempts to avoid
|
|
54
|
+
* concurrency hazards.
|
|
55
|
+
*
|
|
56
|
+
* @param credential - the underlying TokenCredential that provides the access
|
|
57
|
+
* token
|
|
58
|
+
* @param tokenCyclerOptions - optionally override default settings for the cycler
|
|
59
|
+
*
|
|
60
|
+
* @returns - a function that reliably produces a valid access token
|
|
61
|
+
*/
|
|
62
|
+
export function createTokenCycler(credential, tokenCyclerOptions) {
|
|
63
|
+
let refreshWorker = null;
|
|
64
|
+
let token = null;
|
|
65
|
+
let tenantId;
|
|
66
|
+
const options = {
|
|
67
|
+
...DEFAULT_CYCLER_OPTIONS,
|
|
68
|
+
...tokenCyclerOptions,
|
|
69
|
+
};
|
|
42
70
|
/**
|
|
43
|
-
*
|
|
71
|
+
* This little holder defines several predicates that we use to construct
|
|
72
|
+
* the rules of refreshing the token.
|
|
44
73
|
*/
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
74
|
+
const cycler = {
|
|
75
|
+
/**
|
|
76
|
+
* Produces true if a refresh job is currently in progress.
|
|
77
|
+
*/
|
|
78
|
+
get isRefreshing() {
|
|
79
|
+
return refreshWorker !== null;
|
|
80
|
+
},
|
|
81
|
+
/**
|
|
82
|
+
* Produces true if the cycler SHOULD refresh (we are within the refresh
|
|
83
|
+
* window and not already refreshing)
|
|
84
|
+
*/
|
|
85
|
+
get shouldRefresh() {
|
|
86
|
+
if (cycler.isRefreshing) {
|
|
87
|
+
return false;
|
|
88
|
+
}
|
|
89
|
+
if (token?.refreshAfterTimestamp && token.refreshAfterTimestamp < Date.now()) {
|
|
90
|
+
return true;
|
|
91
|
+
}
|
|
92
|
+
return (token?.expiresOnTimestamp ?? 0) - options.refreshWindowInMs < Date.now();
|
|
93
|
+
},
|
|
94
|
+
/**
|
|
95
|
+
* Produces true if the cycler MUST refresh (null or nearly-expired
|
|
96
|
+
* token).
|
|
97
|
+
*/
|
|
98
|
+
get mustRefresh() {
|
|
99
|
+
return (token === null || token.expiresOnTimestamp - options.forcedRefreshWindowInMs < Date.now());
|
|
100
|
+
},
|
|
101
|
+
};
|
|
48
102
|
/**
|
|
49
|
-
*
|
|
50
|
-
*
|
|
103
|
+
* Starts a refresh job or returns the existing job if one is already
|
|
104
|
+
* running.
|
|
51
105
|
*/
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
106
|
+
function refresh(scopes, getTokenOptions) {
|
|
107
|
+
if (!cycler.isRefreshing) {
|
|
108
|
+
// We bind `scopes` here to avoid passing it around a lot
|
|
109
|
+
const tryGetAccessToken = () => credential.getToken(scopes, getTokenOptions);
|
|
110
|
+
// Take advantage of promise chaining to insert an assignment to `token`
|
|
111
|
+
// before the refresh can be considered done.
|
|
112
|
+
refreshWorker = beginRefresh(tryGetAccessToken, options.retryIntervalInMs,
|
|
113
|
+
// If we don't have a token, then we should timeout immediately
|
|
114
|
+
token?.expiresOnTimestamp ?? Date.now())
|
|
115
|
+
.then((_token) => {
|
|
116
|
+
refreshWorker = null;
|
|
117
|
+
token = _token;
|
|
118
|
+
tenantId = getTokenOptions.tenantId;
|
|
119
|
+
return token;
|
|
120
|
+
})
|
|
121
|
+
.catch((reason) => {
|
|
122
|
+
// We also should reset the refresher if we enter a failed state. All
|
|
123
|
+
// existing awaiters will throw, but subsequent requests will start a
|
|
124
|
+
// new retry chain.
|
|
125
|
+
refreshWorker = null;
|
|
126
|
+
token = null;
|
|
127
|
+
tenantId = undefined;
|
|
128
|
+
throw reason;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
return refreshWorker;
|
|
67
132
|
}
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
//
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
133
|
+
return async (scopes, tokenOptions) => {
|
|
134
|
+
//
|
|
135
|
+
// Simple rules:
|
|
136
|
+
// - If we MUST refresh, then return the refresh task, blocking
|
|
137
|
+
// the pipeline until a token is available.
|
|
138
|
+
// - If we SHOULD refresh, then run refresh but don't return it
|
|
139
|
+
// (we can still use the cached token).
|
|
140
|
+
// - Return the token, since it's fine if we didn't return in
|
|
141
|
+
// step 1.
|
|
142
|
+
//
|
|
143
|
+
const hasClaimChallenge = Boolean(tokenOptions.claims);
|
|
144
|
+
const tenantIdChanged = tenantId !== tokenOptions.tenantId;
|
|
145
|
+
if (hasClaimChallenge) {
|
|
146
|
+
// If we've received a claim, we know the existing token isn't valid
|
|
147
|
+
// We want to clear it so that that refresh worker won't use the old expiration time as a timeout
|
|
148
|
+
token = null;
|
|
149
|
+
}
|
|
150
|
+
// If the tenantId passed in token options is different to the one we have
|
|
151
|
+
// Or if we are in claim challenge and the token was rejected and a new access token need to be issued, we need to
|
|
152
|
+
// refresh the token with the new tenantId or token.
|
|
153
|
+
const mustRefresh = tenantIdChanged || hasClaimChallenge || cycler.mustRefresh;
|
|
154
|
+
if (mustRefresh) {
|
|
155
|
+
return refresh(scopes, tokenOptions);
|
|
156
|
+
}
|
|
157
|
+
if (cycler.shouldRefresh) {
|
|
158
|
+
refresh(scopes, tokenOptions);
|
|
159
|
+
}
|
|
81
160
|
return token;
|
|
82
|
-
|
|
83
|
-
refreshWorker = null;
|
|
84
|
-
token = null;
|
|
85
|
-
tenantId = void 0;
|
|
86
|
-
throw reason;
|
|
87
|
-
});
|
|
88
|
-
}
|
|
89
|
-
return refreshWorker;
|
|
90
|
-
}
|
|
91
|
-
return async (scopes, tokenOptions) => {
|
|
92
|
-
const hasClaimChallenge = Boolean(tokenOptions.claims);
|
|
93
|
-
const tenantIdChanged = tenantId !== tokenOptions.tenantId;
|
|
94
|
-
if (hasClaimChallenge) {
|
|
95
|
-
token = null;
|
|
96
|
-
}
|
|
97
|
-
const mustRefresh = tenantIdChanged || hasClaimChallenge || cycler.mustRefresh;
|
|
98
|
-
if (mustRefresh) {
|
|
99
|
-
return refresh(scopes, tokenOptions);
|
|
100
|
-
}
|
|
101
|
-
if (cycler.shouldRefresh) {
|
|
102
|
-
refresh(scopes, tokenOptions);
|
|
103
|
-
}
|
|
104
|
-
return token;
|
|
105
|
-
};
|
|
161
|
+
};
|
|
106
162
|
}
|
|
107
|
-
|
|
108
|
-
DEFAULT_CYCLER_OPTIONS,
|
|
109
|
-
createTokenCycler
|
|
110
|
-
};
|
|
163
|
+
//# sourceMappingURL=tokenCycler.js.map
|
|
@@ -1,7 +1 @@
|
|
|
1
|
-
{
|
|
2
|
-
"version": 3,
|
|
3
|
-
"sources": ["/mnt/vss/_work/1/s/sdk/keyvault/keyvault-common/src/tokenCycler.ts"],
|
|
4
|
-
"sourcesContent": ["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// This file is a direct copy of the tokenCycler implementation in core-rest-pipeline.\n\nimport type { AccessToken, GetTokenOptions, TokenCredential } from \"@azure/core-auth\";\nimport { delay } from \"@azure/core-util\";\n\n/**\n * A function that gets a promise of an access token and allows providing\n * options.\n *\n * @param options - the options to pass to the underlying token provider\n */\nexport type AccessTokenGetter = (\n scopes: string | string[],\n options: GetTokenOptions,\n) => Promise<AccessToken>;\n\nexport interface TokenCyclerOptions {\n /**\n * The window of time before token expiration during which the token will be\n * considered unusable due to risk of the token expiring before sending the\n * request.\n *\n * This will only become meaningful if the refresh fails for over\n * (refreshWindow - forcedRefreshWindow) milliseconds.\n */\n forcedRefreshWindowInMs: number;\n /**\n * Interval in milliseconds to retry failed token refreshes.\n */\n retryIntervalInMs: number;\n /**\n * The window of time before token expiration during which\n * we will attempt to refresh the token.\n */\n refreshWindowInMs: number;\n}\n\n// Default options for the cycler if none are provided\nexport const DEFAULT_CYCLER_OPTIONS: TokenCyclerOptions = {\n forcedRefreshWindowInMs: 1000, // Force waiting for a refresh 1s before the token expires\n retryIntervalInMs: 3000, // Allow refresh attempts every 3s\n refreshWindowInMs: 1000 * 60 * 2, // Start refreshing 2m before expiry\n};\n\n/**\n * Converts an an unreliable access token getter (which may resolve with null)\n * into an AccessTokenGetter by retrying the unreliable getter in a regular\n * interval.\n *\n * @param getAccessToken - A function that produces a promise of an access token that may fail by returning null.\n * @param retryIntervalInMs - The time (in milliseconds) to wait between retry attempts.\n * @param refreshTimeout - The timestamp after which the refresh attempt will fail, throwing an exception.\n * @returns - A promise that, if it resolves, will resolve with an access token.\n */\nasync function beginRefresh(\n getAccessToken: () => Promise<AccessToken | null>,\n retryIntervalInMs: number,\n refreshTimeout: number,\n): Promise<AccessToken> {\n // This wrapper handles exceptions gracefully as long as we haven't exceeded\n // the timeout.\n async function tryGetAccessToken(): Promise<AccessToken | null> {\n if (Date.now() < refreshTimeout) {\n try {\n return await getAccessToken();\n } catch {\n return null;\n }\n } else {\n const finalToken = await getAccessToken();\n\n // Timeout is up, so throw if it's still null\n if (finalToken === null) {\n throw new Error(\"Failed to refresh access token.\");\n }\n\n return finalToken;\n }\n }\n\n let token: AccessToken | null = await tryGetAccessToken();\n\n while (token === null) {\n await delay(retryIntervalInMs);\n\n token = await tryGetAccessToken();\n }\n\n return token;\n}\n\n/**\n * Creates a token cycler from a credential, scopes, and optional settings.\n *\n * A token cycler represents a way to reliably retrieve a valid access token\n * from a TokenCredential. It will handle initializing the token, refreshing it\n * when it nears expiration, and synchronizes refresh attempts to avoid\n * concurrency hazards.\n *\n * @param credential - the underlying TokenCredential that provides the access\n * token\n * @param tokenCyclerOptions - optionally override default settings for the cycler\n *\n * @returns - a function that reliably produces a valid access token\n */\nexport function createTokenCycler(\n credential: TokenCredential,\n tokenCyclerOptions?: Partial<TokenCyclerOptions>,\n): AccessTokenGetter {\n let refreshWorker: Promise<AccessToken> | null = null;\n let token: AccessToken | null = null;\n let tenantId: string | undefined;\n\n const options = {\n ...DEFAULT_CYCLER_OPTIONS,\n ...tokenCyclerOptions,\n };\n\n /**\n * This little holder defines several predicates that we use to construct\n * the rules of refreshing the token.\n */\n const cycler = {\n /**\n * Produces true if a refresh job is currently in progress.\n */\n get isRefreshing(): boolean {\n return refreshWorker !== null;\n },\n /**\n * Produces true if the cycler SHOULD refresh (we are within the refresh\n * window and not already refreshing)\n */\n get shouldRefresh(): boolean {\n if (cycler.isRefreshing) {\n return false;\n }\n if (token?.refreshAfterTimestamp && token.refreshAfterTimestamp < Date.now()) {\n return true;\n }\n\n return (token?.expiresOnTimestamp ?? 0) - options.refreshWindowInMs < Date.now();\n },\n /**\n * Produces true if the cycler MUST refresh (null or nearly-expired\n * token).\n */\n get mustRefresh(): boolean {\n return (\n token === null || token.expiresOnTimestamp - options.forcedRefreshWindowInMs < Date.now()\n );\n },\n };\n\n /**\n * Starts a refresh job or returns the existing job if one is already\n * running.\n */\n function refresh(\n scopes: string | string[],\n getTokenOptions: GetTokenOptions,\n ): Promise<AccessToken> {\n if (!cycler.isRefreshing) {\n // We bind `scopes` here to avoid passing it around a lot\n const tryGetAccessToken = (): Promise<AccessToken | null> =>\n credential.getToken(scopes, getTokenOptions);\n\n // Take advantage of promise chaining to insert an assignment to `token`\n // before the refresh can be considered done.\n refreshWorker = beginRefresh(\n tryGetAccessToken,\n options.retryIntervalInMs,\n // If we don't have a token, then we should timeout immediately\n token?.expiresOnTimestamp ?? Date.now(),\n )\n .then((_token) => {\n refreshWorker = null;\n token = _token;\n tenantId = getTokenOptions.tenantId;\n return token;\n })\n .catch((reason) => {\n // We also should reset the refresher if we enter a failed state. All\n // existing awaiters will throw, but subsequent requests will start a\n // new retry chain.\n refreshWorker = null;\n token = null;\n tenantId = undefined;\n throw reason;\n });\n }\n\n return refreshWorker as Promise<AccessToken>;\n }\n\n return async (scopes: string | string[], tokenOptions: GetTokenOptions): Promise<AccessToken> => {\n //\n // Simple rules:\n // - If we MUST refresh, then return the refresh task, blocking\n // the pipeline until a token is available.\n // - If we SHOULD refresh, then run refresh but don't return it\n // (we can still use the cached token).\n // - Return the token, since it's fine if we didn't return in\n // step 1.\n //\n\n const hasClaimChallenge = Boolean(tokenOptions.claims);\n const tenantIdChanged = tenantId !== tokenOptions.tenantId;\n\n if (hasClaimChallenge) {\n // If we've received a claim, we know the existing token isn't valid\n // We want to clear it so that that refresh worker won't use the old expiration time as a timeout\n token = null;\n }\n\n // If the tenantId passed in token options is different to the one we have\n // Or if we are in claim challenge and the token was rejected and a new access token need to be issued, we need to\n // refresh the token with the new tenantId or token.\n const mustRefresh = tenantIdChanged || hasClaimChallenge || cycler.mustRefresh;\n\n if (mustRefresh) {\n return refresh(scopes, tokenOptions);\n }\n\n if (cycler.shouldRefresh) {\n refresh(scopes, tokenOptions);\n }\n\n return token as AccessToken;\n };\n}\n"],
|
|
5
|
-
"mappings": "AAMA,SAAS,aAAa;AAmCf,MAAM,yBAA6C;AAAA,EACxD,yBAAyB;AAAA;AAAA,EACzB,mBAAmB;AAAA;AAAA,EACnB,mBAAmB,MAAO,KAAK;AAAA;AACjC;AAYA,eAAe,aACb,gBACA,mBACA,gBACsB;AAGtB,iBAAe,oBAAiD;AAC9D,QAAI,KAAK,IAAI,IAAI,gBAAgB;AAC/B,UAAI;AACF,eAAO,MAAM,eAAe;AAAA,MAC9B,QAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,OAAO;AACL,YAAM,aAAa,MAAM,eAAe;AAGxC,UAAI,eAAe,MAAM;AACvB,cAAM,IAAI,MAAM,iCAAiC;AAAA,MACnD;AAEA,aAAO;AAAA,IACT;AAAA,EACF;AAEA,MAAI,QAA4B,MAAM,kBAAkB;AAExD,SAAO,UAAU,MAAM;AACrB,UAAM,MAAM,iBAAiB;AAE7B,YAAQ,MAAM,kBAAkB;AAAA,EAClC;AAEA,SAAO;AACT;AAgBO,SAAS,kBACd,YACA,oBACmB;AACnB,MAAI,gBAA6C;AACjD,MAAI,QAA4B;AAChC,MAAI;AAEJ,QAAM,UAAU;AAAA,IACd,GAAG;AAAA,IACH,GAAG;AAAA,EACL;AAMA,QAAM,SAAS;AAAA;AAAA;AAAA;AAAA,IAIb,IAAI,eAAwB;AAC1B,aAAO,kBAAkB;AAAA,IAC3B;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,IAAI,gBAAyB;AAC3B,UAAI,OAAO,cAAc;AACvB,eAAO;AAAA,MACT;AACA,UAAI,OAAO,yBAAyB,MAAM,wBAAwB,KAAK,IAAI,GAAG;AAC5E,eAAO;AAAA,MACT;AAEA,cAAQ,OAAO,sBAAsB,KAAK,QAAQ,oBAAoB,KAAK,IAAI;AAAA,IACjF;AAAA;AAAA;AAAA;AAAA;AAAA,IAKA,IAAI,cAAuB;AACzB,aACE,UAAU,QAAQ,MAAM,qBAAqB,QAAQ,0BAA0B,KAAK,IAAI;AAAA,IAE5F;AAAA,EACF;AAMA,WAAS,QACP,QACA,iBACsB;AACtB,QAAI,CAAC,OAAO,cAAc;AAExB,YAAM,oBAAoB,MACxB,WAAW,SAAS,QAAQ,eAAe;AAI7C,sBAAgB;AAAA,QACd;AAAA,QACA,QAAQ;AAAA;AAAA,QAER,OAAO,sBAAsB,KAAK,IAAI;AAAA,MACxC,EACG,KAAK,CAAC,WAAW;AAChB,wBAAgB;AAChB,gBAAQ;AACR,mBAAW,gBAAgB;AAC3B,eAAO;AAAA,MACT,CAAC,EACA,MAAM,CAAC,WAAW;AAIjB,wBAAgB;AAChB,gBAAQ;AACR,mBAAW;AACX,cAAM;AAAA,MACR,CAAC;AAAA,IACL;AAEA,WAAO;AAAA,EACT;AAEA,SAAO,OAAO,QAA2B,iBAAwD;AAW/F,UAAM,oBAAoB,QAAQ,aAAa,MAAM;AACrD,UAAM,kBAAkB,aAAa,aAAa;AAElD,QAAI,mBAAmB;AAGrB,cAAQ;AAAA,IACV;AAKA,UAAM,cAAc,mBAAmB,qBAAqB,OAAO;AAEnE,QAAI,aAAa;AACf,aAAO,QAAQ,QAAQ,YAAY;AAAA,IACrC;AAEA,QAAI,OAAO,eAAe;AACxB,cAAQ,QAAQ,YAAY;AAAA,IAC9B;AAEA,WAAO;AAAA,EACT;AACF;",
|
|
6
|
-
"names": []
|
|
7
|
-
}
|
|
1
|
+
{"version":3,"file":"tokenCycler.js","sourceRoot":"","sources":["../../src/tokenCycler.ts"],"names":[],"mappings":"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","sourcesContent":["// Copyright (c) Microsoft Corporation.\n// Licensed under the MIT License.\n\n// This file is a direct copy of the tokenCycler implementation in core-rest-pipeline.\n\nimport type { AccessToken, GetTokenOptions, TokenCredential } from \"@azure/core-auth\";\nimport { delay } from \"@azure/core-util\";\n\n/**\n * A function that gets a promise of an access token and allows providing\n * options.\n *\n * @param options - the options to pass to the underlying token provider\n */\nexport type AccessTokenGetter = (\n scopes: string | string[],\n options: GetTokenOptions,\n) => Promise<AccessToken>;\n\nexport interface TokenCyclerOptions {\n /**\n * The window of time before token expiration during which the token will be\n * considered unusable due to risk of the token expiring before sending the\n * request.\n *\n * This will only become meaningful if the refresh fails for over\n * (refreshWindow - forcedRefreshWindow) milliseconds.\n */\n forcedRefreshWindowInMs: number;\n /**\n * Interval in milliseconds to retry failed token refreshes.\n */\n retryIntervalInMs: number;\n /**\n * The window of time before token expiration during which\n * we will attempt to refresh the token.\n */\n refreshWindowInMs: number;\n}\n\n// Default options for the cycler if none are provided\nexport const DEFAULT_CYCLER_OPTIONS: TokenCyclerOptions = {\n forcedRefreshWindowInMs: 1000, // Force waiting for a refresh 1s before the token expires\n retryIntervalInMs: 3000, // Allow refresh attempts every 3s\n refreshWindowInMs: 1000 * 60 * 2, // Start refreshing 2m before expiry\n};\n\n/**\n * Converts an an unreliable access token getter (which may resolve with null)\n * into an AccessTokenGetter by retrying the unreliable getter in a regular\n * interval.\n *\n * @param getAccessToken - A function that produces a promise of an access token that may fail by returning null.\n * @param retryIntervalInMs - The time (in milliseconds) to wait between retry attempts.\n * @param refreshTimeout - The timestamp after which the refresh attempt will fail, throwing an exception.\n * @returns - A promise that, if it resolves, will resolve with an access token.\n */\nasync function beginRefresh(\n getAccessToken: () => Promise<AccessToken | null>,\n retryIntervalInMs: number,\n refreshTimeout: number,\n): Promise<AccessToken> {\n // This wrapper handles exceptions gracefully as long as we haven't exceeded\n // the timeout.\n async function tryGetAccessToken(): Promise<AccessToken | null> {\n if (Date.now() < refreshTimeout) {\n try {\n return await getAccessToken();\n } catch {\n return null;\n }\n } else {\n const finalToken = await getAccessToken();\n\n // Timeout is up, so throw if it's still null\n if (finalToken === null) {\n throw new Error(\"Failed to refresh access token.\");\n }\n\n return finalToken;\n }\n }\n\n let token: AccessToken | null = await tryGetAccessToken();\n\n while (token === null) {\n await delay(retryIntervalInMs);\n\n token = await tryGetAccessToken();\n }\n\n return token;\n}\n\n/**\n * Creates a token cycler from a credential, scopes, and optional settings.\n *\n * A token cycler represents a way to reliably retrieve a valid access token\n * from a TokenCredential. It will handle initializing the token, refreshing it\n * when it nears expiration, and synchronizes refresh attempts to avoid\n * concurrency hazards.\n *\n * @param credential - the underlying TokenCredential that provides the access\n * token\n * @param tokenCyclerOptions - optionally override default settings for the cycler\n *\n * @returns - a function that reliably produces a valid access token\n */\nexport function createTokenCycler(\n credential: TokenCredential,\n tokenCyclerOptions?: Partial<TokenCyclerOptions>,\n): AccessTokenGetter {\n let refreshWorker: Promise<AccessToken> | null = null;\n let token: AccessToken | null = null;\n let tenantId: string | undefined;\n\n const options = {\n ...DEFAULT_CYCLER_OPTIONS,\n ...tokenCyclerOptions,\n };\n\n /**\n * This little holder defines several predicates that we use to construct\n * the rules of refreshing the token.\n */\n const cycler = {\n /**\n * Produces true if a refresh job is currently in progress.\n */\n get isRefreshing(): boolean {\n return refreshWorker !== null;\n },\n /**\n * Produces true if the cycler SHOULD refresh (we are within the refresh\n * window and not already refreshing)\n */\n get shouldRefresh(): boolean {\n if (cycler.isRefreshing) {\n return false;\n }\n if (token?.refreshAfterTimestamp && token.refreshAfterTimestamp < Date.now()) {\n return true;\n }\n\n return (token?.expiresOnTimestamp ?? 0) - options.refreshWindowInMs < Date.now();\n },\n /**\n * Produces true if the cycler MUST refresh (null or nearly-expired\n * token).\n */\n get mustRefresh(): boolean {\n return (\n token === null || token.expiresOnTimestamp - options.forcedRefreshWindowInMs < Date.now()\n );\n },\n };\n\n /**\n * Starts a refresh job or returns the existing job if one is already\n * running.\n */\n function refresh(\n scopes: string | string[],\n getTokenOptions: GetTokenOptions,\n ): Promise<AccessToken> {\n if (!cycler.isRefreshing) {\n // We bind `scopes` here to avoid passing it around a lot\n const tryGetAccessToken = (): Promise<AccessToken | null> =>\n credential.getToken(scopes, getTokenOptions);\n\n // Take advantage of promise chaining to insert an assignment to `token`\n // before the refresh can be considered done.\n refreshWorker = beginRefresh(\n tryGetAccessToken,\n options.retryIntervalInMs,\n // If we don't have a token, then we should timeout immediately\n token?.expiresOnTimestamp ?? Date.now(),\n )\n .then((_token) => {\n refreshWorker = null;\n token = _token;\n tenantId = getTokenOptions.tenantId;\n return token;\n })\n .catch((reason) => {\n // We also should reset the refresher if we enter a failed state. All\n // existing awaiters will throw, but subsequent requests will start a\n // new retry chain.\n refreshWorker = null;\n token = null;\n tenantId = undefined;\n throw reason;\n });\n }\n\n return refreshWorker as Promise<AccessToken>;\n }\n\n return async (scopes: string | string[], tokenOptions: GetTokenOptions): Promise<AccessToken> => {\n //\n // Simple rules:\n // - If we MUST refresh, then return the refresh task, blocking\n // the pipeline until a token is available.\n // - If we SHOULD refresh, then run refresh but don't return it\n // (we can still use the cached token).\n // - Return the token, since it's fine if we didn't return in\n // step 1.\n //\n\n const hasClaimChallenge = Boolean(tokenOptions.claims);\n const tenantIdChanged = tenantId !== tokenOptions.tenantId;\n\n if (hasClaimChallenge) {\n // If we've received a claim, we know the existing token isn't valid\n // We want to clear it so that that refresh worker won't use the old expiration time as a timeout\n token = null;\n }\n\n // If the tenantId passed in token options is different to the one we have\n // Or if we are in claim challenge and the token was rejected and a new access token need to be issued, we need to\n // refresh the token with the new tenantId or token.\n const mustRefresh = tenantIdChanged || hasClaimChallenge || cycler.mustRefresh;\n\n if (mustRefresh) {\n return refresh(scopes, tokenOptions);\n }\n\n if (cycler.shouldRefresh) {\n refresh(scopes, tokenOptions);\n }\n\n return token as AccessToken;\n };\n}\n"]}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@azure/keyvault-common",
|
|
3
|
-
"version": "2.0
|
|
3
|
+
"version": "2.1.0",
|
|
4
4
|
"description": "Common internal functionality for all of the Azure Key Vault clients in the Azure SDK for JavaScript",
|
|
5
5
|
"sdk-type": "client",
|
|
6
6
|
"author": "Microsoft Corporation",
|
|
@@ -30,29 +30,29 @@
|
|
|
30
30
|
"sideEffects": false,
|
|
31
31
|
"prettier": "@azure/eslint-plugin-azure-sdk/prettier.json",
|
|
32
32
|
"dependencies": {
|
|
33
|
-
"@azure-rest/core-client": "
|
|
34
|
-
"@azure/abort-controller": "
|
|
35
|
-
"@azure/core-auth": "
|
|
36
|
-
"@azure/core-rest-pipeline": "
|
|
37
|
-
"@azure/core-tracing": "
|
|
38
|
-
"@azure/core-util": "
|
|
39
|
-
"@azure/logger": "
|
|
33
|
+
"@azure-rest/core-client": "^2.3.3",
|
|
34
|
+
"@azure/abort-controller": "^2.0.0",
|
|
35
|
+
"@azure/core-auth": "^1.3.0",
|
|
36
|
+
"@azure/core-rest-pipeline": "^1.8.0",
|
|
37
|
+
"@azure/core-tracing": "^1.0.0",
|
|
38
|
+
"@azure/core-util": "^1.10.0",
|
|
39
|
+
"@azure/logger": "^1.1.4",
|
|
40
40
|
"tslib": "^2.2.0"
|
|
41
41
|
},
|
|
42
42
|
"devDependencies": {
|
|
43
43
|
"@types/node": "^20.19.25",
|
|
44
|
-
"@vitest/browser-playwright": "^4.
|
|
45
|
-
"@vitest/coverage-istanbul": "^4.
|
|
44
|
+
"@vitest/browser-playwright": "^4.1.2",
|
|
45
|
+
"@vitest/coverage-istanbul": "^4.1.2",
|
|
46
46
|
"cross-env": "^10.1.0",
|
|
47
47
|
"eslint": "^9.39.1",
|
|
48
|
-
"playwright": "^1.
|
|
48
|
+
"playwright": "^1.58.2",
|
|
49
49
|
"prettier": "^3.6.2",
|
|
50
50
|
"rimraf": "^6.1.0",
|
|
51
|
-
"typescript": "~
|
|
52
|
-
"vitest": "^4.
|
|
51
|
+
"typescript": "~6.0.2",
|
|
52
|
+
"vitest": "^4.1.2",
|
|
53
53
|
"@azure-tools/test-utils-vitest": "^2.0.1",
|
|
54
|
-
"@azure/
|
|
55
|
-
"@azure/
|
|
54
|
+
"@azure/eslint-plugin-azure-sdk": "^3.0.0",
|
|
55
|
+
"@azure/dev-tool": "^1.0.0"
|
|
56
56
|
},
|
|
57
57
|
"type": "module",
|
|
58
58
|
"exports": {
|