diff --git a/sdk/keyvault/keyvault-admin/CHANGELOG.md b/sdk/keyvault/keyvault-admin/CHANGELOG.md
index 79feda736ad2..0f7a1dda0cc2 100644
--- a/sdk/keyvault/keyvault-admin/CHANGELOG.md
+++ b/sdk/keyvault/keyvault-admin/CHANGELOG.md
@@ -1,9 +1,11 @@
# Release History
-## 4.5.1 (Unreleased)
+## 4.6.0 (Unreleased)
### Features Added
+- Add support for Continuous Access Evaluation (CAE). [#31140](https://github.com/Azure/azure-sdk-for-js/pull/31140)
+
### Breaking Changes
### Bugs Fixed
diff --git a/sdk/keyvault/keyvault-admin/package.json b/sdk/keyvault/keyvault-admin/package.json
index 7fa0dfa92bb0..7506a56652a2 100644
--- a/sdk/keyvault/keyvault-admin/package.json
+++ b/sdk/keyvault/keyvault-admin/package.json
@@ -2,7 +2,7 @@
"name": "@azure/keyvault-admin",
"sdk-type": "client",
"author": "Microsoft Corporation",
- "version": "4.5.1",
+ "version": "4.6.0",
"license": "MIT",
"description": "Isomorphic client library for Azure KeyVault's administrative functions.",
"homepage": "https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/keyvault/keyvault-admin/README.md",
@@ -99,7 +99,7 @@
"@azure/core-rest-pipeline": "^1.1.0",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.0.0",
- "@azure/keyvault-common": "^1.0.0",
+ "@azure/keyvault-common": "^2.0.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0"
},
diff --git a/sdk/keyvault/keyvault-admin/src/accessControlClient.ts b/sdk/keyvault/keyvault-admin/src/accessControlClient.ts
index c682bf3b022d..b832cd2774a7 100644
--- a/sdk/keyvault/keyvault-admin/src/accessControlClient.ts
+++ b/sdk/keyvault/keyvault-admin/src/accessControlClient.ts
@@ -23,8 +23,7 @@ import { LATEST_API_VERSION } from "./constants.js";
import { PagedAsyncIterableIterator } from "@azure/core-paging";
import { RoleAssignmentsListForScopeOptionalParams } from "./generated/models/index.js";
import { TokenCredential } from "@azure/core-auth";
-import { bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline";
-import { createKeyVaultChallengeCallbacks } from "@azure/keyvault-common";
+import { keyVaultAuthenticationPolicy } from "@azure/keyvault-common";
import { logger } from "./log.js";
import { mappings } from "./mappings.js";
import { tracingClient } from "./tracing.js";
@@ -87,15 +86,11 @@ export class KeyVaultAccessControlClient {
this.client = new KeyVaultClient(serviceVersion, clientOptions);
- this.client.pipeline.addPolicy(
- bearerTokenAuthenticationPolicy({
- credential,
- // The scopes will be populated in the challenge callbacks based on the WWW-authenticate header
- // returned by the challenge, so pass an empty array as a placeholder.
- scopes: [],
- challengeCallbacks: createKeyVaultChallengeCallbacks(options),
- }),
- );
+ // The authentication policy must come after the deserialization policy since the deserialization policy
+ // converts 401 responses to an Error, and we don't want to deal with that.
+ this.client.pipeline.addPolicy(keyVaultAuthenticationPolicy(credential, clientOptions), {
+ afterPolicies: ["deserializationPolicy"],
+ });
}
/**
diff --git a/sdk/keyvault/keyvault-admin/src/backupClient.ts b/sdk/keyvault/keyvault-admin/src/backupClient.ts
index 628c7560475b..ff8b0ee9a370 100644
--- a/sdk/keyvault/keyvault-admin/src/backupClient.ts
+++ b/sdk/keyvault/keyvault-admin/src/backupClient.ts
@@ -21,8 +21,7 @@ import { KeyVaultSelectiveKeyRestorePoller } from "./lro/selectiveKeyRestore/pol
import { LATEST_API_VERSION } from "./constants.js";
import { PollerLike } from "@azure/core-lro";
import { TokenCredential } from "@azure/core-auth";
-import { bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline";
-import { createKeyVaultChallengeCallbacks } from "@azure/keyvault-common";
+import { keyVaultAuthenticationPolicy } from "@azure/keyvault-common";
import { logger } from "./log.js";
import { mappings } from "./mappings.js";
@@ -89,15 +88,11 @@ export class KeyVaultBackupClient {
};
this.client = new KeyVaultClient(apiVersion, clientOptions);
- this.client.pipeline.addPolicy(
- bearerTokenAuthenticationPolicy({
- credential,
- // The scopes will be populated in the challenge callbacks based on the WWW-authenticate header
- // returned by the challenge, so pass an empty array as a placeholder.
- scopes: [],
- challengeCallbacks: createKeyVaultChallengeCallbacks(options),
- }),
- );
+ // The authentication policy must come after the deserialization policy since the deserialization policy
+ // converts 401 responses to an Error, and we don't want to deal with that.
+ this.client.pipeline.addPolicy(keyVaultAuthenticationPolicy(credential, clientOptions), {
+ afterPolicies: ["deserializationPolicy"],
+ });
}
/**
diff --git a/sdk/keyvault/keyvault-admin/src/constants.ts b/sdk/keyvault/keyvault-admin/src/constants.ts
index 2eb876869020..708e4c6d7a33 100644
--- a/sdk/keyvault/keyvault-admin/src/constants.ts
+++ b/sdk/keyvault/keyvault-admin/src/constants.ts
@@ -4,7 +4,7 @@
/**
* Current version of the Key Vault Admin SDK.
*/
-export const SDK_VERSION: string = "4.5.1";
+export const SDK_VERSION: string = "4.6.0";
/**
* The latest supported Key Vault service API version.
diff --git a/sdk/keyvault/keyvault-admin/src/generated/keyVaultClientContext.ts b/sdk/keyvault/keyvault-admin/src/generated/keyVaultClientContext.ts
index 1802c70425c8..7dadb47ee170 100644
--- a/sdk/keyvault/keyvault-admin/src/generated/keyVaultClientContext.ts
+++ b/sdk/keyvault/keyvault-admin/src/generated/keyVaultClientContext.ts
@@ -33,7 +33,7 @@ export class KeyVaultClientContext extends coreClient.ServiceClient {
requestContentType: "application/json; charset=utf-8"
};
- const packageDetails = `azsdk-js-keyvault-admin/4.5.1`;
+ const packageDetails = `azsdk-js-keyvault-admin/4.6.0`;
const userAgentPrefix =
options.userAgentOptions && options.userAgentOptions.userAgentPrefix
? `${options.userAgentOptions.userAgentPrefix} ${packageDetails}`
diff --git a/sdk/keyvault/keyvault-admin/src/settingsClient.ts b/sdk/keyvault/keyvault-admin/src/settingsClient.ts
index 65216baa1148..86ff463e9041 100644
--- a/sdk/keyvault/keyvault-admin/src/settingsClient.ts
+++ b/sdk/keyvault/keyvault-admin/src/settingsClient.ts
@@ -2,8 +2,7 @@
// Licensed under the MIT License.
import { TokenCredential } from "@azure/core-auth";
-import { bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline";
-import { createKeyVaultChallengeCallbacks } from "@azure/keyvault-common";
+import { keyVaultAuthenticationPolicy } from "@azure/keyvault-common";
import { LATEST_API_VERSION } from "./constants.js";
import { KeyVaultClient, Setting as GeneratedSetting } from "./generated/index.js";
import { logger } from "./log.js";
@@ -92,13 +91,12 @@ export class KeyVaultSettingsClient {
};
this.client = new KeyVaultClient(apiVersion, clientOptions);
- this.client.pipeline.addPolicy(
- bearerTokenAuthenticationPolicy({
- credential,
- scopes: [],
- challengeCallbacks: createKeyVaultChallengeCallbacks(options),
- }),
- );
+
+ // The authentication policy must come after the deserialization policy since the deserialization policy
+ // converts 401 responses to an Error, and we don't want to deal with that.
+ this.client.pipeline.addPolicy(keyVaultAuthenticationPolicy(credential, clientOptions), {
+ afterPolicies: ["deserializationPolicy"],
+ });
}
/**
diff --git a/sdk/keyvault/keyvault-admin/swagger/README.md b/sdk/keyvault/keyvault-admin/swagger/README.md
index a0e14971e5e4..838cc3d4c478 100644
--- a/sdk/keyvault/keyvault-admin/swagger/README.md
+++ b/sdk/keyvault/keyvault-admin/swagger/README.md
@@ -16,7 +16,7 @@ input-file:
- https://raw.githubusercontent.com/Azure/azure-rest-api-specs/7452e1cc7db72fbc6cd9539b390d8b8e5c2a1864/specification/keyvault/data-plane/Microsoft.KeyVault/stable/7.5/settings.json
output-folder: ../
source-code-folder-path: ./src/generated
-package-version: 4.5.1
+package-version: 4.6.0
use-extension:
"@autorest/typescript": "6.0.0-beta.15"
```
diff --git a/sdk/keyvault/keyvault-certificates/CHANGELOG.md b/sdk/keyvault/keyvault-certificates/CHANGELOG.md
index f26ed2f7b72a..d986666eb875 100644
--- a/sdk/keyvault/keyvault-certificates/CHANGELOG.md
+++ b/sdk/keyvault/keyvault-certificates/CHANGELOG.md
@@ -1,9 +1,11 @@
# Release History
-## 4.8.1 (Unreleased)
+## 4.9.0 (Unreleased)
### Features Added
+- Add support for Continuous Access Evaluation (CAE). [#31140](https://github.com/Azure/azure-sdk-for-js/pull/31140)
+
### Breaking Changes
### Bugs Fixed
diff --git a/sdk/keyvault/keyvault-certificates/package.json b/sdk/keyvault/keyvault-certificates/package.json
index 9b34eaa28e81..535d2e7d3ecb 100644
--- a/sdk/keyvault/keyvault-certificates/package.json
+++ b/sdk/keyvault/keyvault-certificates/package.json
@@ -2,7 +2,7 @@
"name": "@azure/keyvault-certificates",
"sdk-type": "client",
"author": "Microsoft Corporation",
- "version": "4.8.1",
+ "version": "4.9.0",
"license": "MIT",
"description": "Isomorphic client library for Azure KeyVault's certificates.",
"homepage": "https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/keyvault/keyvault-certificates/README.md",
@@ -103,7 +103,7 @@
"@azure/core-rest-pipeline": "^1.8.0",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.6.1",
- "@azure/keyvault-common": "^1.0.0",
+ "@azure/keyvault-common": "^2.0.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0"
},
diff --git a/sdk/keyvault/keyvault-certificates/src/constants.ts b/sdk/keyvault/keyvault-certificates/src/constants.ts
index 59040838a911..6d660debf3a8 100644
--- a/sdk/keyvault/keyvault-certificates/src/constants.ts
+++ b/sdk/keyvault/keyvault-certificates/src/constants.ts
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-export const SDK_VERSION: string = "4.8.1";
+export const SDK_VERSION: string = "4.9.0";
diff --git a/sdk/keyvault/keyvault-certificates/src/generated/keyVaultClient.ts b/sdk/keyvault/keyvault-certificates/src/generated/keyVaultClient.ts
index 6222c444c032..f410d831df8c 100644
--- a/sdk/keyvault/keyvault-certificates/src/generated/keyVaultClient.ts
+++ b/sdk/keyvault/keyvault-certificates/src/generated/keyVaultClient.ts
@@ -103,7 +103,7 @@ export class KeyVaultClient extends coreHttpCompat.ExtendedServiceClient {
requestContentType: "application/json; charset=utf-8"
};
- const packageDetails = `azsdk-js-keyvault-certificates/4.8.1`;
+ const packageDetails = `azsdk-js-keyvault-certificates/4.9.0`;
const userAgentPrefix =
options.userAgentOptions && options.userAgentOptions.userAgentPrefix
? `${options.userAgentOptions.userAgentPrefix} ${packageDetails}`
diff --git a/sdk/keyvault/keyvault-certificates/src/index.ts b/sdk/keyvault/keyvault-certificates/src/index.ts
index 24858fcd7593..1ffc0066e6a9 100644
--- a/sdk/keyvault/keyvault-certificates/src/index.ts
+++ b/sdk/keyvault/keyvault-certificates/src/index.ts
@@ -6,7 +6,6 @@
///
import { InternalClientPipelineOptions } from "@azure/core-client";
-import { bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline";
import { TokenCredential } from "@azure/core-auth";
@@ -100,7 +99,7 @@ import {
} from "./generated/models/index.js";
import { KeyVaultClient } from "./generated/keyVaultClient.js";
import { PageSettings, PagedAsyncIterableIterator } from "@azure/core-paging";
-import { createKeyVaultChallengeCallbacks } from "@azure/keyvault-common";
+import { keyVaultAuthenticationPolicy } from "@azure/keyvault-common";
import { CreateCertificatePoller } from "./lro/create/poller.js";
import { CertificateOperationPoller } from "./lro/operation/poller.js";
import { DeleteCertificatePoller } from "./lro/delete/poller.js";
@@ -247,12 +246,6 @@ export class CertificateClient {
) {
this.vaultUrl = vaultUrl;
- const authPolicy = bearerTokenAuthenticationPolicy({
- credential,
- scopes: [],
- challengeCallbacks: createKeyVaultChallengeCallbacks(clientOptions),
- });
-
const internalClientPipelineOptions: InternalClientPipelineOptions = {
...clientOptions,
loggingOptions: {
@@ -269,7 +262,12 @@ export class CertificateClient {
clientOptions.serviceVersion || LATEST_API_VERSION,
internalClientPipelineOptions,
);
- this.client.pipeline.addPolicy(authPolicy);
+
+ // The authentication policy must come after the deserialization policy since the deserialization policy
+ // converts 401 responses to an Error, and we don't want to deal with that.
+ this.client.pipeline.addPolicy(keyVaultAuthenticationPolicy(credential, clientOptions), {
+ afterPolicies: ["deserializationPolicy"],
+ });
}
private async *listPropertiesOfCertificatesPage(
diff --git a/sdk/keyvault/keyvault-certificates/swagger/README.md b/sdk/keyvault/keyvault-certificates/swagger/README.md
index eb3438fb83e7..31d11451fc65 100644
--- a/sdk/keyvault/keyvault-certificates/swagger/README.md
+++ b/sdk/keyvault/keyvault-certificates/swagger/README.md
@@ -20,7 +20,7 @@ input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/7452e1c
output-folder: ../
source-code-folder-path: ./src/generated
hide-clients: true
-package-version: 4.8.1
+package-version: 4.9.0
openapi-type: data-plane
```
diff --git a/sdk/keyvault/keyvault-common/CHANGELOG.md b/sdk/keyvault/keyvault-common/CHANGELOG.md
index 6487b537e5ac..e4a11e55342f 100644
--- a/sdk/keyvault/keyvault-common/CHANGELOG.md
+++ b/sdk/keyvault/keyvault-common/CHANGELOG.md
@@ -1,11 +1,16 @@
# Release History
-## 1.0.1 (Unreleased)
+## 2.0.0 (Unreleased)
### Features Added
+- Add support for Continuous Access Evaluation (CAE).
+ - To take advantage of this support, the newly added `keyVaultAuthenticationPolicy` should be used in place of `bearerTokenAuthenticationPolicy`.
+
### Breaking Changes
+- Removed `createKeyVaultChallengeCallbacks`, which was used to add Key Vault specific handling to Core's `bearerTokenAuthenticationPolicy`. The new `keyVaultAuthenticationPolicy` should be used instead.
+
### Bugs Fixed
### Other Changes
diff --git a/sdk/keyvault/keyvault-common/package.json b/sdk/keyvault/keyvault-common/package.json
index d48f5e6b82e5..8f9d2b94b9fc 100644
--- a/sdk/keyvault/keyvault-common/package.json
+++ b/sdk/keyvault/keyvault-common/package.json
@@ -1,6 +1,6 @@
{
"name": "@azure/keyvault-common",
- "version": "1.0.1",
+ "version": "2.0.0",
"description": "Common internal functionality for all of the Azure Key Vault clients in the Azure SDK for JavaScript",
"sdk-type": "client",
"author": "Microsoft Corporation",
@@ -59,7 +59,9 @@
"@azure/core-client": "^1.5.0",
"@azure/core-rest-pipeline": "^1.8.0",
"@azure/core-tracing": "^1.0.0",
- "tslib": "^2.2.0"
+ "@azure/logger": "^1.1.5",
+ "tslib": "^2.2.0",
+ "@azure/core-util": "^1.10.1"
},
"devDependencies": {
"@azure-tools/test-utils-vitest": "^1.0.0",
diff --git a/sdk/keyvault/keyvault-common/review/keyvault-common.api.md b/sdk/keyvault/keyvault-common/review/keyvault-common.api.md
index 32c5277aff13..99d9590a6b8b 100644
--- a/sdk/keyvault/keyvault-common/review/keyvault-common.api.md
+++ b/sdk/keyvault/keyvault-common/review/keyvault-common.api.md
@@ -4,15 +4,19 @@
```ts
-import { ChallengeCallbacks } from '@azure/core-rest-pipeline';
+import { PipelinePolicy } from '@azure/core-rest-pipeline';
+import { TokenCredential } from '@azure/core-auth';
// @public
-export interface CreateChallengeCallbacksOptions {
- disableChallengeResourceVerification?: boolean;
-}
+export function keyVaultAuthenticationPolicy(credential: TokenCredential, options?: KeyVaultAuthenticationPolicyOptions): PipelinePolicy;
+
+// @public
+export const keyVaultAuthenticationPolicyName = "keyVaultAuthenticationPolicy";
// @public
-export function createKeyVaultChallengeCallbacks(options?: CreateChallengeCallbacksOptions): ChallengeCallbacks;
+export interface KeyVaultAuthenticationPolicyOptions {
+ disableChallengeResourceVerification?: boolean;
+}
// @public
export interface KeyVaultEntityIdentifier {
diff --git a/sdk/keyvault/keyvault-common/src/index.ts b/sdk/keyvault/keyvault-common/src/index.ts
index e9fc51a25090..e3c7747b9a97 100644
--- a/sdk/keyvault/keyvault-common/src/index.ts
+++ b/sdk/keyvault/keyvault-common/src/index.ts
@@ -1,5 +1,5 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-export * from "./challengeBasedAuthenticationPolicy.js";
+export * from "./keyVaultAuthenticationPolicy.js";
export * from "./parseKeyVaultIdentifier.js";
diff --git a/sdk/keyvault/keyvault-common/src/challengeBasedAuthenticationPolicy.ts b/sdk/keyvault/keyvault-common/src/keyVaultAuthenticationPolicy.ts
similarity index 55%
rename from sdk/keyvault/keyvault-common/src/challengeBasedAuthenticationPolicy.ts
rename to sdk/keyvault/keyvault-common/src/keyVaultAuthenticationPolicy.ts
index 54f8eba7d07c..017978f5ea4c 100644
--- a/sdk/keyvault/keyvault-common/src/challengeBasedAuthenticationPolicy.ts
+++ b/sdk/keyvault/keyvault-common/src/keyVaultAuthenticationPolicy.ts
@@ -2,15 +2,17 @@
// Licensed under the MIT License.
import {
- AuthorizeRequestOnChallengeOptions,
- AuthorizeRequestOptions,
- ChallengeCallbacks,
+ PipelinePolicy,
PipelineRequest,
+ PipelineResponse,
RequestBodyType,
+ SendRequest,
} from "@azure/core-rest-pipeline";
import { WWWAuthenticate, parseWWWAuthenticateHeader } from "./parseWWWAuthenticate.js";
-import { GetTokenOptions } from "@azure/core-auth";
+import { GetTokenOptions, TokenCredential } from "@azure/core-auth";
+import { createTokenCycler } from "./tokenCycler.js";
+import { logger } from "./logger.js";
/**
* @internal
@@ -35,12 +37,13 @@ type ChallengeState =
| {
status: "complete";
scopes: string[];
+ tenantId?: string;
};
/**
* Additional options for the challenge based authentication policy.
*/
-export interface CreateChallengeCallbacksOptions {
+export interface KeyVaultAuthenticationPolicyOptions {
/**
* Whether to disable verification that the challenge resource matches the Key Vault or Managed HSM domain.
*
@@ -67,7 +70,12 @@ function verifyChallengeResource(scope: string, request: PipelineRequest): void
}
/**
- * Creates challenge callback handlers to manage CAE lifecycle in Azure Key Vault.
+ * Name of the Key Vault authentication policy.
+ */
+export const keyVaultAuthenticationPolicyName = "keyVaultAuthenticationPolicy";
+
+/**
+ * A custom implementation of the bearer-token authentication policy that handles Key Vault and CAE challenges.
*
* Key Vault supports other authentication schemes, but we ensure challenge authentication
* is used by first sending a copy of the request, without authorization or content.
@@ -79,11 +87,13 @@ function verifyChallengeResource(scope: string, request: PipelineRequest): void
* if possible.
*
*/
-export function createKeyVaultChallengeCallbacks(
- options: CreateChallengeCallbacksOptions = {},
-): ChallengeCallbacks {
+export function keyVaultAuthenticationPolicy(
+ credential: TokenCredential,
+ options: KeyVaultAuthenticationPolicyOptions = {},
+): PipelinePolicy {
const { disableChallengeResourceVerification } = options;
let challengeState: ChallengeState = { status: "none" };
+ const getAccessToken = createTokenCycler(credential);
function requestToOptions(request: PipelineRequest): GetTokenOptions {
return {
@@ -95,10 +105,7 @@ export function createKeyVaultChallengeCallbacks(
};
}
- async function authorizeRequest({
- request,
- getAccessToken,
- }: AuthorizeRequestOptions): Promise {
+ async function authorizeRequest(request: PipelineRequest): Promise {
const requestOptions: GetTokenOptions = requestToOptions(request);
switch (challengeState.status) {
@@ -112,21 +119,29 @@ export function createKeyVaultChallengeCallbacks(
case "started":
break; // Retry, we should not overwrite the original body
case "complete": {
- const token = await getAccessToken(challengeState.scopes, requestOptions);
+ const token = await getAccessToken(challengeState.scopes, {
+ ...requestOptions,
+ enableCae: true,
+ tenantId: challengeState.tenantId,
+ });
if (token) {
request.headers.set("authorization", `Bearer ${token.token}`);
}
break;
}
}
- return Promise.resolve();
}
- async function authorizeRequestOnChallenge({
- request,
- response,
- getAccessToken,
- }: AuthorizeRequestOnChallengeOptions): Promise {
+ async function handleChallenge(
+ request: PipelineRequest,
+ response: PipelineResponse,
+ next: SendRequest,
+ ): Promise {
+ // If status is not 401, this is a no-op
+ if (response.status !== 401) {
+ return response;
+ }
+
if (request.body === null && challengeState.status === "started") {
// Reset the original body before doing anything else.
// Note: If successful status will be "complete", otherwise "none" will
@@ -138,16 +153,20 @@ export function createKeyVaultChallengeCallbacks(
const challenge = response.headers.get("WWW-Authenticate");
if (!challenge) {
- throw new Error("Missing challenge.");
+ logger.warning(
+ "keyVaultAuthentication policy encountered a 401 response without a corresponding WWW-Authenticate header. This is unexpected. Not handling the 401 response.",
+ );
+ return response;
}
- const parsedChallenge: WWWAuthenticate = parseWWWAuthenticateHeader(challenge) || {};
+ const parsedChallenge: WWWAuthenticate = parseWWWAuthenticateHeader(challenge);
const scope = parsedChallenge.resource
? parsedChallenge.resource + "/.default"
: parsedChallenge.scope;
if (!scope) {
- throw new Error("Missing scope.");
+ // Cannot handle this kind of challenge here (if scope is not present, may be a CAE challenge)
+ return response;
}
if (!disableChallengeResourceVerification) {
@@ -156,11 +175,13 @@ export function createKeyVaultChallengeCallbacks(
const accessToken = await getAccessToken([scope], {
...getTokenOptions,
+ enableCae: true,
tenantId: parsedChallenge.tenantId,
});
if (!accessToken) {
- return false;
+ // No access token provided, treat as no-op
+ return response;
}
request.headers.set("Authorization", `Bearer ${accessToken.token}`);
@@ -168,13 +189,76 @@ export function createKeyVaultChallengeCallbacks(
challengeState = {
status: "complete",
scopes: [scope],
+ tenantId: parsedChallenge.tenantId,
};
- return true;
+ // We have a token now, so try send the request again
+ return next(request);
+ }
+
+ async function handleCaeChallenge(
+ request: PipelineRequest,
+ response: PipelineResponse,
+ next: SendRequest,
+ ): Promise {
+ // Cannot handle CAE challenge if a regular challenge has not been completed first
+ if (challengeState.status !== "complete") {
+ return response;
+ }
+
+ // If status is not 401, this is a no-op
+ if (response.status !== 401) {
+ return response;
+ }
+
+ const getTokenOptions = requestToOptions(request);
+
+ const challenge = response.headers.get("WWW-Authenticate");
+ if (!challenge) {
+ return response;
+ }
+ const { claims: base64EncodedClaims, error }: WWWAuthenticate =
+ parseWWWAuthenticateHeader(challenge);
+
+ if (error !== "insufficient_claims" || base64EncodedClaims === undefined) {
+ return response;
+ }
+
+ const claims = atob(base64EncodedClaims);
+
+ const accessToken = await getAccessToken(challengeState.scopes, {
+ ...getTokenOptions,
+ enableCae: true,
+ tenantId: challengeState.tenantId,
+ claims,
+ });
+
+ request.headers.set("Authorization", `Bearer ${accessToken.token}`);
+
+ return next(request);
+ }
+
+ async function sendRequest(
+ request: PipelineRequest,
+ next: SendRequest,
+ ): Promise {
+ // Add token if possible
+ await authorizeRequest(request);
+
+ // Try send request (first attempt)
+ let response = await next(request);
+
+ // Handle standard challenge if present
+ response = await handleChallenge(request, response, next);
+
+ // Handle CAE challenge if present
+ response = await handleCaeChallenge(request, response, next);
+
+ return response;
}
return {
- authorizeRequest,
- authorizeRequestOnChallenge,
+ name: keyVaultAuthenticationPolicyName,
+ sendRequest,
};
}
diff --git a/sdk/keyvault/keyvault-common/src/logger.ts b/sdk/keyvault/keyvault-common/src/logger.ts
new file mode 100644
index 000000000000..fddc58835663
--- /dev/null
+++ b/sdk/keyvault/keyvault-common/src/logger.ts
@@ -0,0 +1,6 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import { createClientLogger } from "@azure/logger";
+
+export const logger = createClientLogger("keyvault-common");
diff --git a/sdk/keyvault/keyvault-common/src/parseWWWAuthenticate.ts b/sdk/keyvault/keyvault-common/src/parseWWWAuthenticate.ts
index 343d91a6fb23..1082ce3f6703 100644
--- a/sdk/keyvault/keyvault-common/src/parseWWWAuthenticate.ts
+++ b/sdk/keyvault/keyvault-common/src/parseWWWAuthenticate.ts
@@ -29,6 +29,16 @@ export interface WWWAuthenticate {
* The tenantId parameter, if present.
*/
tenantId?: string;
+
+ /**
+ * The claims parameter, if present.
+ */
+ claims?: string;
+
+ /**
+ * The error parameter, if present.
+ */
+ error?: string;
}
const validWWWAuthenticateProperties: readonly (keyof WWWAuthenticate)[] = [
@@ -37,6 +47,8 @@ const validWWWAuthenticateProperties: readonly (keyof WWWAuthenticate)[] = [
"resource",
"scope",
"tenantId",
+ "claims",
+ "error",
] as const;
/**
@@ -52,10 +64,10 @@ export function parseWWWAuthenticateHeader(headerValue: string): WWWAuthenticate
const parsed = headerValue.split(pairDelimiter).reduce((kvPairs, p) => {
if (p.match(/\w="/)) {
// 'sampleKey="sample_value"' -> [sampleKey, "sample_value"] -> { sampleKey: sample_value }
- const [key, value] = p.split("=");
+ const [key, ...value] = p.split("=");
if (validWWWAuthenticateProperties.includes(key as keyof WWWAuthenticate)) {
// The values will be wrapped in quotes, which need to be stripped out.
- return { ...kvPairs, [key]: value.slice(1, -1) };
+ return { ...kvPairs, [key]: value.join("=").slice(1, -1) };
}
}
return kvPairs;
diff --git a/sdk/keyvault/keyvault-common/src/tokenCycler.ts b/sdk/keyvault/keyvault-common/src/tokenCycler.ts
new file mode 100644
index 000000000000..06de02bbcc70
--- /dev/null
+++ b/sdk/keyvault/keyvault-common/src/tokenCycler.ts
@@ -0,0 +1,234 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+// This file is a direct copy of the tokenCycler implementation in core-rest-pipeline.
+
+import type { AccessToken, GetTokenOptions, TokenCredential } from "@azure/core-auth";
+import { delay } from "@azure/core-util";
+
+/**
+ * A function that gets a promise of an access token and allows providing
+ * options.
+ *
+ * @param options - the options to pass to the underlying token provider
+ */
+export type AccessTokenGetter = (
+ scopes: string | string[],
+ options: GetTokenOptions,
+) => Promise;
+
+export interface TokenCyclerOptions {
+ /**
+ * The window of time before token expiration during which the token will be
+ * considered unusable due to risk of the token expiring before sending the
+ * request.
+ *
+ * This will only become meaningful if the refresh fails for over
+ * (refreshWindow - forcedRefreshWindow) milliseconds.
+ */
+ forcedRefreshWindowInMs: number;
+ /**
+ * Interval in milliseconds to retry failed token refreshes.
+ */
+ retryIntervalInMs: number;
+ /**
+ * The window of time before token expiration during which
+ * we will attempt to refresh the token.
+ */
+ refreshWindowInMs: number;
+}
+
+// Default options for the cycler if none are provided
+export const DEFAULT_CYCLER_OPTIONS: TokenCyclerOptions = {
+ forcedRefreshWindowInMs: 1000, // Force waiting for a refresh 1s before the token expires
+ retryIntervalInMs: 3000, // Allow refresh attempts every 3s
+ refreshWindowInMs: 1000 * 60 * 2, // Start refreshing 2m before expiry
+};
+
+/**
+ * Converts an an unreliable access token getter (which may resolve with null)
+ * into an AccessTokenGetter by retrying the unreliable getter in a regular
+ * interval.
+ *
+ * @param getAccessToken - A function that produces a promise of an access token that may fail by returning null.
+ * @param retryIntervalInMs - The time (in milliseconds) to wait between retry attempts.
+ * @param refreshTimeout - The timestamp after which the refresh attempt will fail, throwing an exception.
+ * @returns - A promise that, if it resolves, will resolve with an access token.
+ */
+async function beginRefresh(
+ getAccessToken: () => Promise,
+ retryIntervalInMs: number,
+ refreshTimeout: number,
+): Promise {
+ // This wrapper handles exceptions gracefully as long as we haven't exceeded
+ // the timeout.
+ async function tryGetAccessToken(): Promise {
+ if (Date.now() < refreshTimeout) {
+ try {
+ return await getAccessToken();
+ } catch {
+ return null;
+ }
+ } else {
+ const finalToken = await getAccessToken();
+
+ // Timeout is up, so throw if it's still null
+ if (finalToken === null) {
+ throw new Error("Failed to refresh access token.");
+ }
+
+ return finalToken;
+ }
+ }
+
+ let token: AccessToken | null = await tryGetAccessToken();
+
+ while (token === null) {
+ await delay(retryIntervalInMs);
+
+ token = await tryGetAccessToken();
+ }
+
+ return token;
+}
+
+/**
+ * Creates a token cycler from a credential, scopes, and optional settings.
+ *
+ * A token cycler represents a way to reliably retrieve a valid access token
+ * from a TokenCredential. It will handle initializing the token, refreshing it
+ * when it nears expiration, and synchronizes refresh attempts to avoid
+ * concurrency hazards.
+ *
+ * @param credential - the underlying TokenCredential that provides the access
+ * token
+ * @param tokenCyclerOptions - optionally override default settings for the cycler
+ *
+ * @returns - a function that reliably produces a valid access token
+ */
+export function createTokenCycler(
+ credential: TokenCredential,
+ tokenCyclerOptions?: Partial,
+): AccessTokenGetter {
+ let refreshWorker: Promise | null = null;
+ let token: AccessToken | null = null;
+ let tenantId: string | undefined;
+
+ const options = {
+ ...DEFAULT_CYCLER_OPTIONS,
+ ...tokenCyclerOptions,
+ };
+
+ /**
+ * This little holder defines several predicates that we use to construct
+ * the rules of refreshing the token.
+ */
+ const cycler = {
+ /**
+ * Produces true if a refresh job is currently in progress.
+ */
+ get isRefreshing(): boolean {
+ return refreshWorker !== null;
+ },
+ /**
+ * Produces true if the cycler SHOULD refresh (we are within the refresh
+ * window and not already refreshing)
+ */
+ get shouldRefresh(): boolean {
+ if (cycler.isRefreshing) {
+ return false;
+ }
+ if (token?.refreshAfterTimestamp && token.refreshAfterTimestamp < Date.now()) {
+ return true;
+ }
+
+ return (token?.expiresOnTimestamp ?? 0) - options.refreshWindowInMs < Date.now();
+ },
+ /**
+ * Produces true if the cycler MUST refresh (null or nearly-expired
+ * token).
+ */
+ get mustRefresh(): boolean {
+ return (
+ token === null || token.expiresOnTimestamp - options.forcedRefreshWindowInMs < Date.now()
+ );
+ },
+ };
+
+ /**
+ * Starts a refresh job or returns the existing job if one is already
+ * running.
+ */
+ function refresh(
+ scopes: string | string[],
+ getTokenOptions: GetTokenOptions,
+ ): Promise {
+ if (!cycler.isRefreshing) {
+ // We bind `scopes` here to avoid passing it around a lot
+ const tryGetAccessToken = (): Promise =>
+ credential.getToken(scopes, getTokenOptions);
+
+ // Take advantage of promise chaining to insert an assignment to `token`
+ // before the refresh can be considered done.
+ refreshWorker = beginRefresh(
+ tryGetAccessToken,
+ options.retryIntervalInMs,
+ // If we don't have a token, then we should timeout immediately
+ token?.expiresOnTimestamp ?? Date.now(),
+ )
+ .then((_token) => {
+ refreshWorker = null;
+ token = _token;
+ tenantId = getTokenOptions.tenantId;
+ return token;
+ })
+ .catch((reason) => {
+ // We also should reset the refresher if we enter a failed state. All
+ // existing awaiters will throw, but subsequent requests will start a
+ // new retry chain.
+ refreshWorker = null;
+ token = null;
+ tenantId = undefined;
+ throw reason;
+ });
+ }
+
+ return refreshWorker as Promise;
+ }
+
+ return async (scopes: string | string[], tokenOptions: GetTokenOptions): Promise => {
+ //
+ // Simple rules:
+ // - If we MUST refresh, then return the refresh task, blocking
+ // the pipeline until a token is available.
+ // - If we SHOULD refresh, then run refresh but don't return it
+ // (we can still use the cached token).
+ // - Return the token, since it's fine if we didn't return in
+ // step 1.
+ //
+
+ const hasClaimChallenge = Boolean(tokenOptions.claims);
+ const tenantIdChanged = tenantId !== tokenOptions.tenantId;
+
+ if (hasClaimChallenge) {
+ // If we've received a claim, we know the existing token isn't valid
+ // We want to clear it so that that refresh worker won't use the old expiration time as a timeout
+ token = null;
+ }
+
+ // If the tenantId passed in token options is different to the one we have
+ // Or if we are in claim challenge and the token was rejected and a new access token need to be issued, we need to
+ // refresh the token with the new tenantId or token.
+ const mustRefresh = tenantIdChanged || hasClaimChallenge || cycler.mustRefresh;
+
+ if (mustRefresh) {
+ return refresh(scopes, tokenOptions);
+ }
+
+ if (cycler.shouldRefresh) {
+ refresh(scopes, tokenOptions);
+ }
+
+ return token as AccessToken;
+ };
+}
diff --git a/sdk/keyvault/keyvault-common/test/internal/challengeAuthenticationCallbacks.spec.ts b/sdk/keyvault/keyvault-common/test/internal/challengeAuthenticationCallbacks.spec.ts
deleted file mode 100644
index 1145edba04aa..000000000000
--- a/sdk/keyvault/keyvault-common/test/internal/challengeAuthenticationCallbacks.spec.ts
+++ /dev/null
@@ -1,317 +0,0 @@
-// Copyright (c) Microsoft Corporation.
-// Licensed under the MIT License.
-
-import {
- AuthorizeRequestOptions,
- ChallengeCallbacks,
- PipelineRequest,
- createHttpHeaders,
- createPipelineRequest,
-} from "@azure/core-rest-pipeline";
-import { createKeyVaultChallengeCallbacks } from "../../src/index.js";
-import { parseWWWAuthenticateHeader } from "../../src/parseWWWAuthenticate.js";
-import { describe, it, beforeEach, expect } from "vitest";
-
-describe("Challenge based authentication tests", function () {
- let request: PipelineRequest;
- let challengeCallbacks: ChallengeCallbacks;
-
- beforeEach(() => {
- request = createPipelineRequest({ url: "https://myvault.vault.azure.net" });
- challengeCallbacks = createKeyVaultChallengeCallbacks();
- });
-
- describe("authorizeRequest", () => {
- it("always starts the challenge on the first call", async () => {
- let getAccessTokenCallCount = 0;
- const options: AuthorizeRequestOptions = {
- getAccessToken: () => {
- getAccessTokenCallCount += 1;
- return Promise.resolve({ token: "access_token", expiresOnTimestamp: 1000 });
- },
- request,
- scopes: [],
- };
-
- await challengeCallbacks.authorizeRequest!(options);
-
- expect(options.request.headers.get("authorization")).toBeUndefined();
- // We do not call getAccessToken on the first request
- expect(getAccessTokenCallCount).toEqual(0);
- });
-
- it("sets the authorization token if it gets one on subsequent calls", async () => {
- let getAccessTokenCallCount = 0;
- const options: AuthorizeRequestOptions = {
- getAccessToken: () => {
- getAccessTokenCallCount += 1;
- return Promise.resolve({ token: "access_token", expiresOnTimestamp: 1000 });
- },
- request,
- scopes: [],
- };
-
- // Set up the challenge state to complete by calling authorizeRequestOnChallenge first
- await challengeCallbacks.authorizeRequestOnChallenge!({
- getAccessToken: () => {
- return Promise.resolve({ token: "successful_token", expiresOnTimestamp: 999999999 });
- },
- request,
- response: {
- headers: createHttpHeaders({
- "WWW-Authenticate": `Bearer resource="https://vault.azure.net"`,
- }),
- request,
- status: 200,
- },
- scopes: [],
- });
-
- await challengeCallbacks.authorizeRequest!(options);
-
- expect(options.request.headers.get("authorization")).toEqual("Bearer access_token");
- expect(getAccessTokenCallCount).toEqual(1);
- });
-
- it("does not modify headers when unable to get access token", async () => {
- const options: AuthorizeRequestOptions = {
- getAccessToken: () => {
- return Promise.resolve(null);
- },
- request: createPipelineRequest({
- url: "https://foo.bar",
- headers: createHttpHeaders(),
- }),
- scopes: ["any_scope"],
- };
-
- await challengeCallbacks.authorizeRequest!(options);
-
- expect(options.request.headers.get("authorization")).toBeUndefined();
- });
- });
-
- describe("authorizeRequestOnChallenge", () => {
- it("validates WWW-Authenticate exists", async () => {
- await expect(
- challengeCallbacks.authorizeRequestOnChallenge!({
- getAccessToken: () => Promise.resolve(null),
- request,
- response: {
- headers: createHttpHeaders(),
- request,
- status: 200,
- },
- scopes: [],
- }),
- ).rejects.toThrow("Missing challenge");
- });
-
- it("passes the correct scopes if provided", async () => {
- let getAccessTokenScopes: string[] = [];
- await challengeCallbacks.authorizeRequestOnChallenge!({
- getAccessToken: (scopes) => {
- getAccessTokenScopes = scopes;
- return Promise.resolve(null);
- },
- request,
- response: {
- headers: createHttpHeaders({
- "WWW-Authenticate": `Bearer resource="https://vault.azure.net"`,
- }),
- request,
- status: 200,
- },
- scopes: [],
- });
-
- expect(getAccessTokenScopes).to.deep.equal(["https://vault.azure.net/.default"]);
- });
-
- it("throws if the resource is not a valid URL", async () => {
- await expect(
- challengeCallbacks.authorizeRequestOnChallenge!({
- getAccessToken: () => Promise.resolve(null),
- request,
- response: {
- headers: createHttpHeaders({
- "WWW-Authenticate": `Bearer resource="invalid_scope"`,
- }),
- request,
- status: 200,
- },
- scopes: [],
- }),
- ).rejects.toThrow(`The challenge contains invalid scope 'invalid_scope/.default'`);
- });
-
- it("throws if the resource URI host does not match the request by default", async () => {
- await expect(
- challengeCallbacks.authorizeRequestOnChallenge!({
- getAccessToken: () => Promise.resolve(null),
- request: createPipelineRequest({ url: "https://foo.bar" }),
- response: {
- headers: createHttpHeaders({
- "WWW-Authenticate": `Bearer resource="https://vault.azure.net"`,
- }),
- request,
- status: 200,
- },
- scopes: [],
- }),
- ).rejects.toThrow(
- "The challenge resource 'vault.azure.net' 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.",
- );
- });
-
- it("throws if the request host is a prefix, but not a subdomain, of the resource URI host", async () => {
- await expect(
- challengeCallbacks.authorizeRequestOnChallenge!({
- getAccessToken: () => Promise.resolve(null),
- request: createPipelineRequest({ url: "https://myvault.azure.net" }),
- response: {
- headers: createHttpHeaders({
- "WWW-Authenticate": `Bearer resource="https://vault.azure.net"`,
- }),
- request,
- status: 200,
- },
- scopes: [],
- }),
- ).rejects.toThrow(
- "The challenge resource 'vault.azure.net' 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.",
- );
- });
-
- it("does not throw if the resource URI matches the request", async () => {
- await challengeCallbacks.authorizeRequestOnChallenge!({
- getAccessToken: () => Promise.resolve(null),
- request: createPipelineRequest({ url: "https://myvault.vault.azure.net" }),
- response: {
- headers: createHttpHeaders({
- "WWW-Authenticate": `Bearer resource="https://vault.azure.net"`,
- }),
- request,
- status: 200,
- },
- scopes: [],
- });
- });
-
- it("does not throw if the resource URI host does not match the request but verifyChallengeResource is false", async () => {
- challengeCallbacks = createKeyVaultChallengeCallbacks({
- disableChallengeResourceVerification: true,
- });
- await challengeCallbacks.authorizeRequestOnChallenge!({
- getAccessToken: () => Promise.resolve(null),
- request: createPipelineRequest({ url: "https://foo.bar" }),
- response: {
- headers: createHttpHeaders({
- "WWW-Authenticate": `Bearer resource="https://vault.azure.net"`,
- }),
- request,
- status: 200,
- },
- scopes: [],
- });
- });
-
- it("passes the tenantId if provided", async () => {
- const expectedTenantId = "expectedTenantId";
-
- let getAccessTokenTenantId: string | undefined = "";
-
- await challengeCallbacks.authorizeRequestOnChallenge!({
- getAccessToken: (_scopes, options) => {
- getAccessTokenTenantId = options.tenantId;
- return Promise.resolve(null);
- },
- request,
- response: {
- headers: createHttpHeaders({
- "WWW-Authenticate": `Bearer resource="https://vault.azure.net" authorization="http://login.windows.net/${expectedTenantId}"`,
- }),
- request,
- status: 200,
- },
- scopes: [],
- });
-
- expect(getAccessTokenTenantId).toEqual(expectedTenantId);
- });
-
- it("returns true and sets the authorization header if challenge succeeds", async () => {
- const result = await challengeCallbacks.authorizeRequestOnChallenge!({
- getAccessToken: () => {
- return Promise.resolve({ token: "successful_token", expiresOnTimestamp: 999999999 });
- },
- request,
- response: {
- headers: createHttpHeaders({
- "WWW-Authenticate": `Bearer resource="https://vault.azure.net"`,
- }),
- request,
- status: 200,
- },
- scopes: [],
- });
- expect(result).toEqual(true);
- });
-
- it("returns false and does not modify header if challenge fails", async () => {
- const result = await challengeCallbacks.authorizeRequestOnChallenge!({
- getAccessToken: () => {
- return Promise.resolve(null);
- },
- request,
- response: {
- headers: createHttpHeaders({
- "WWW-Authenticate": `Bearer resource="https://vault.azure.net"`,
- }),
- request,
- status: 200,
- },
- scopes: [],
- });
- expect(result).toEqual(false);
- });
- });
-
- describe("parseWWWAuthenticateHeader tests", () => {
- it("Should work for known shapes of the WWW-Authenticate header", () => {
- const wwwAuthenticate1 = `Bearer authorization="https://login.windows.net", resource="https://some.url"`;
- const parsed1 = parseWWWAuthenticateHeader(wwwAuthenticate1);
- expect(parsed1).to.deep.equal({
- authorization: "https://login.windows.net",
- resource: "https://some.url",
- });
-
- const wwwAuthenticate2 = `Bearer authorization="https://login.windows.net/", scope="https://some.url"`;
- const parsed2 = parseWWWAuthenticateHeader(wwwAuthenticate2);
- expect(parsed2).to.deep.equal({
- authorization: "https://login.windows.net/",
- scope: "https://some.url",
- });
- });
-
- it("Should ignore unknown values in the WWW-Authenticate header", () => {
- const wwwAuthenticate1 = `Bearer authorization="https://login.windows.net", resource="https://some.url" scope="scope", a="a", b="b"`;
- const parsed1 = parseWWWAuthenticateHeader(wwwAuthenticate1);
- expect(parsed1).to.deep.equal({
- authorization: "https://login.windows.net",
- resource: "https://some.url",
- scope: "scope",
- });
- });
-
- it("should include the tenantId when present", () => {
- const wwwAuthenticate1 = `Bearer authorization="https://login.windows.net/9999", resource="https://some.url"`;
- const parsed1 = parseWWWAuthenticateHeader(wwwAuthenticate1);
- expect(parsed1).to.deep.equal({
- authorization: "https://login.windows.net/9999",
- resource: "https://some.url",
- tenantId: "9999",
- });
- });
- });
-});
diff --git a/sdk/keyvault/keyvault-common/test/internal/keyVaultAuthenticationPolicy.spec.ts b/sdk/keyvault/keyvault-common/test/internal/keyVaultAuthenticationPolicy.spec.ts
new file mode 100644
index 000000000000..6c6e1cebef44
--- /dev/null
+++ b/sdk/keyvault/keyvault-common/test/internal/keyVaultAuthenticationPolicy.spec.ts
@@ -0,0 +1,296 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+import {
+ Pipeline,
+ PipelineRequest,
+ SendRequest,
+ createEmptyPipeline,
+ createHttpHeaders,
+ createPipelineRequest,
+} from "@azure/core-rest-pipeline";
+import { parseWWWAuthenticateHeader } from "../../src/parseWWWAuthenticate.js";
+import { describe, it, beforeEach, expect, vi } from "vitest";
+import { TokenCredential } from "@azure/core-auth";
+import { keyVaultAuthenticationPolicy } from "../../src/keyVaultAuthenticationPolicy.js";
+
+const caeChallenge = `Bearer realm="", authorization_uri="https://login.microsoftonline.com/common/oauth2/authorize", error="insufficient_claims", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ=="`;
+const caeClaims = `{"access_token":{"nbf":{"essential":true,"value":"1726077595"},"xms_caeerror":{"value":"10012"}}}`;
+
+const challengeResponse: SendRequest = async (req) => {
+ expect(req.body).toBeNull();
+ return {
+ headers: createHttpHeaders({
+ "WWW-Authenticate": `Bearer resource="https://vault.azure.net", authorization="http://login.windows.net/testTenantId"`,
+ }),
+ status: 401,
+ request: req,
+ };
+};
+
+const caeChallengeResponse: SendRequest = async (req) => ({
+ headers: createHttpHeaders({
+ "WWW-Authenticate": caeChallenge,
+ }),
+ request: req,
+ status: 401,
+});
+
+const successfulResponseWith =
+ (options: { expectAuthorizationHeader?: string; responseBody?: string } = {}): SendRequest =>
+ async (req) => {
+ if (options.expectAuthorizationHeader) {
+ expect(req.headers.get("Authorization")).toEqual(options.expectAuthorizationHeader);
+ }
+
+ return {
+ headers: createHttpHeaders({}),
+ request: req,
+ bodyAsText: options.responseBody,
+ status: options.responseBody ? 200 : 204,
+ };
+ };
+
+const TOKEN_EXPIRY = 10000;
+
+const mockCredential: TokenCredential = {
+ getToken: async (scopes, options) => {
+ const [scope] = Array.isArray(scopes) ? scopes : [scopes];
+ expect(scope).toBe("https://vault.azure.net/.default");
+ expect(options?.tenantId).toBe("testTenantId");
+ expect(options?.enableCae).toBe(true);
+ const later = new Date().getTime() + TOKEN_EXPIRY;
+
+ if (options?.claims) {
+ expect(options.claims).toBe(caeClaims);
+ return {
+ token: "cae_token",
+ expiresOnTimestamp: later,
+ };
+ } else {
+ return {
+ token: "access_token",
+ expiresOnTimestamp: later,
+ };
+ }
+ },
+};
+
+describe("Challenge based authentication tests", function () {
+ let request: PipelineRequest;
+ let pipeline: Pipeline;
+
+ beforeEach(() => {
+ request = createPipelineRequest({ url: "https://myvault.vault.azure.net" });
+ pipeline = createEmptyPipeline();
+ pipeline.addPolicy(keyVaultAuthenticationPolicy(mockCredential));
+ });
+
+ it("handles a challenge", async () => {
+ const sendRequest = vi
+ .fn()
+ .mockImplementationOnce(challengeResponse)
+ .mockImplementationOnce(
+ successfulResponseWith({ expectAuthorizationHeader: "Bearer access_token" }),
+ );
+
+ const rsp = await pipeline.sendRequest({ sendRequest }, request);
+ expect(rsp.status).toBe(204);
+ });
+
+ it("handles a CAE challenge that comes immediately after a successful Key Vault challenge", async () => {
+ const sendRequest = vi
+ .fn()
+ // First, send the standard challenge
+ .mockImplementationOnce(challengeResponse)
+ // Then send a CAE challenge immediately after
+ .mockImplementationOnce(caeChallengeResponse)
+ // Finally, send a successful response, but only after asserting that the token in the Authorization header was obtained by passing the scopes through to the credential.
+ .mockImplementationOnce(
+ successfulResponseWith({ expectAuthorizationHeader: "Bearer cae_token" }),
+ );
+
+ const rsp = await pipeline.sendRequest({ sendRequest }, request);
+ expect(rsp.status).toBe(204);
+ });
+
+ it("handles a CAE challenge where the Base64-encoded claims do not have the correct padding", async () => {
+ const sendRequest = vi
+ .fn()
+ // First, send the standard challenge
+ .mockImplementationOnce(challengeResponse)
+ // Then send a CAE challenge immediately after. In this case the padding == at the end of the `claims` has been removed
+ .mockImplementationOnce(async (req) => ({
+ headers: createHttpHeaders({
+ "WWW-Authenticate": `Bearer realm="", authorization_uri="https://login.microsoftonline.com/common/oauth2/authorize", error="insufficient_claims", claims="eyJhY2Nlc3NfdG9rZW4iOnsibmJmIjp7ImVzc2VudGlhbCI6dHJ1ZSwidmFsdWUiOiIxNzI2MDc3NTk1In0sInhtc19jYWVlcnJvciI6eyJ2YWx1ZSI6IjEwMDEyIn19fQ"`,
+ }),
+ request: req,
+ status: 401,
+ }))
+ // Finally, send a successful response, but only after asserting that the token in the Authorization header was obtained by passing the scopes through to the credential.
+ .mockImplementationOnce(
+ successfulResponseWith({ expectAuthorizationHeader: "Bearer cae_token" }),
+ );
+
+ const rsp = await pipeline.sendRequest({ sendRequest }, request);
+ expect(rsp.status).toBe(204);
+ });
+
+ it("handles a CAE challenge that comes a few requests after the initial request", async () => {
+ const sendRequest = vi
+ .fn()
+ // First, send the standard challenge
+ .mockImplementationOnce(challengeResponse)
+ // Then, send a successful response, but only after asserting that the token in the Authorization header was obtained
+ .mockImplementationOnce(
+ successfulResponseWith({
+ expectAuthorizationHeader: "Bearer access_token",
+ responseBody: "response 1",
+ }),
+ )
+ // Do that again
+ .mockImplementationOnce(
+ successfulResponseWith({
+ expectAuthorizationHeader: "Bearer access_token",
+ responseBody: "response 2",
+ }),
+ )
+ // Then provide a CAE challenge
+ .mockImplementationOnce(caeChallengeResponse)
+ // After the CAE challenge, send a successful response, but only after asserting that the token in the Authorization header was obtained by passing the claims through to the credential.
+ .mockImplementationOnce(
+ successfulResponseWith({
+ expectAuthorizationHeader: "Bearer cae_token",
+ responseBody: "response 3",
+ }),
+ )
+ // Next request, we should again get the token we got after the CAE challenge.
+ .mockImplementationOnce(
+ successfulResponseWith({
+ expectAuthorizationHeader: "Bearer cae_token",
+ responseBody: "response 4",
+ }),
+ );
+
+ // First request will get the standard challenge followed by a 200
+ const rsp = await pipeline.sendRequest({ sendRequest }, request);
+ expect(rsp.status).toBe(200);
+ expect(rsp.bodyAsText).toBe("response 1");
+
+ // Making the request a second time should result in a 200 immediately
+ const rsp2 = await pipeline.sendRequest({ sendRequest }, request);
+ expect(rsp2.status).toBe(200);
+ expect(rsp2.bodyAsText).toBe("response 2");
+
+ // The third request will get a CAE challenge, which will get handled and then we will get another 200.
+ const rsp3 = await pipeline.sendRequest({ sendRequest }, request);
+ expect(rsp3.status).toBe(200);
+ expect(rsp3.bodyAsText).toBe("response 3");
+
+ // The fourth request should not have any challenge to handle and we will ultimately get a 200 status.
+ const rsp4 = await pipeline.sendRequest({ sendRequest }, request);
+ expect(rsp4.status).toBe(200);
+ expect(rsp4.bodyAsText).toBe("response 4");
+ });
+
+ it("does not handle multiple consecutive CAE challenges", async () => {
+ const sendRequest = vi
+ .fn()
+ // First, send the standard challenge
+ .mockImplementationOnce(challengeResponse)
+ // Then provide a CAE challenge
+ .mockImplementationOnce(caeChallengeResponse)
+ // Then another CAE challenge. This challenge should not be handled
+ .mockImplementationOnce(async (req) => ({
+ headers: createHttpHeaders({
+ "WWW-Authenticate": caeChallenge,
+ }),
+ request: req,
+ bodyAsText: "CAE challenge 2",
+ status: 401,
+ }));
+
+ // Should be a 401
+ const rsp = await pipeline.sendRequest({ sendRequest }, request);
+ expect(rsp.status).toBe(401);
+ expect(rsp.bodyAsText).toBe("CAE challenge 2");
+ });
+
+ it("subsequent calls to getToken do not have claims after the initial call to CAE getToken", async () => {
+ vi.useFakeTimers();
+
+ const sendRequest = vi
+ .fn()
+ .mockImplementationOnce(challengeResponse)
+ .mockImplementationOnce(caeChallengeResponse)
+ .mockImplementationOnce(
+ successfulResponseWith({ expectAuthorizationHeader: "Bearer cae_token" }),
+ )
+ // This response will happen after we advance the system clock. Another call to getToken will be made. This
+ // call should not have `claims` set, and so the token retrieved from the mock credential will be access_token and not cae_token
+ .mockImplementationOnce(
+ successfulResponseWith({
+ expectAuthorizationHeader: "Bearer access_token",
+ responseBody: "response 2",
+ }),
+ );
+
+ const rsp1 = await pipeline.sendRequest({ sendRequest }, request);
+ expect(rsp1.status).toBe(204);
+
+ // Update system time to force token expiry
+ vi.setSystemTime(new Date().getTime() + TOKEN_EXPIRY);
+ const rsp2 = await pipeline.sendRequest({ sendRequest }, request);
+ expect(rsp2.status).toBe(200);
+ expect(rsp2.bodyAsText).toBe("response 2");
+
+ vi.useRealTimers();
+ });
+
+ describe("parseWWWAuthenticateHeader tests", () => {
+ it("Should work for known shapes of the WWW-Authenticate header", () => {
+ const wwwAuthenticate1 = `Bearer authorization="https://login.windows.net", resource="https://some.url"`;
+ const parsed1 = parseWWWAuthenticateHeader(wwwAuthenticate1);
+ expect(parsed1).to.deep.equal({
+ authorization: "https://login.windows.net",
+ resource: "https://some.url",
+ });
+
+ const wwwAuthenticate2 = `Bearer authorization="https://login.windows.net/", scope="https://some.url"`;
+ const parsed2 = parseWWWAuthenticateHeader(wwwAuthenticate2);
+ expect(parsed2).to.deep.equal({
+ authorization: "https://login.windows.net/",
+ scope: "https://some.url",
+ });
+ });
+
+ it("Should ignore unknown values in the WWW-Authenticate header", () => {
+ const wwwAuthenticate1 = `Bearer authorization="https://login.windows.net", resource="https://some.url" scope="scope", a="a", b="b"`;
+ const parsed1 = parseWWWAuthenticateHeader(wwwAuthenticate1);
+ expect(parsed1).to.deep.equal({
+ authorization: "https://login.windows.net",
+ resource: "https://some.url",
+ scope: "scope",
+ });
+ });
+
+ it("should include the tenantId when present", () => {
+ const wwwAuthenticate1 = `Bearer authorization="https://login.windows.net/9999", resource="https://some.url"`;
+ const parsed1 = parseWWWAuthenticateHeader(wwwAuthenticate1);
+ expect(parsed1).to.deep.equal({
+ authorization: "https://login.windows.net/9999",
+ resource: "https://some.url",
+ tenantId: "9999",
+ });
+ });
+
+ it("should handle Base64-encoded claims", () => {
+ const header = `Bearer claims="SGVsbG8=", error="insufficient_claims"`;
+ const parsed = parseWWWAuthenticateHeader(header);
+ expect(parsed).to.deep.equal({
+ claims: "SGVsbG8=",
+ error: "insufficient_claims",
+ });
+ });
+ });
+});
diff --git a/sdk/keyvault/keyvault-keys/CHANGELOG.md b/sdk/keyvault/keyvault-keys/CHANGELOG.md
index abf3b0006608..193a62c40a18 100644
--- a/sdk/keyvault/keyvault-keys/CHANGELOG.md
+++ b/sdk/keyvault/keyvault-keys/CHANGELOG.md
@@ -1,9 +1,11 @@
# Release History
-## 4.8.1 (Unreleased)
+## 4.9.0 (Unreleased)
### Features Added
+- Add support for Continuous Access Evaluation (CAE). [#31140](https://github.com/Azure/azure-sdk-for-js/pull/31140)
+
### Breaking Changes
### Bugs Fixed
diff --git a/sdk/keyvault/keyvault-keys/package.json b/sdk/keyvault/keyvault-keys/package.json
index 7f7381da92dc..610475bca866 100644
--- a/sdk/keyvault/keyvault-keys/package.json
+++ b/sdk/keyvault/keyvault-keys/package.json
@@ -2,7 +2,7 @@
"name": "@azure/keyvault-keys",
"sdk-type": "client",
"author": "Microsoft Corporation",
- "version": "4.8.1",
+ "version": "4.9.0",
"license": "MIT",
"description": "Isomorphic client library for Azure KeyVault's keys.",
"homepage": "https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/keyvault/keyvault-keys/README.md",
@@ -111,7 +111,7 @@
"@azure/core-rest-pipeline": "^1.8.1",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.0.0",
- "@azure/keyvault-common": "^1.0.0",
+ "@azure/keyvault-common": "^2.0.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0"
},
diff --git a/sdk/keyvault/keyvault-keys/src/constants.ts b/sdk/keyvault/keyvault-keys/src/constants.ts
index 59040838a911..6d660debf3a8 100644
--- a/sdk/keyvault/keyvault-keys/src/constants.ts
+++ b/sdk/keyvault/keyvault-keys/src/constants.ts
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-export const SDK_VERSION: string = "4.8.1";
+export const SDK_VERSION: string = "4.9.0";
diff --git a/sdk/keyvault/keyvault-keys/src/cryptography/remoteCryptographyProvider.ts b/sdk/keyvault/keyvault-keys/src/cryptography/remoteCryptographyProvider.ts
index 23ece08aba6a..db7e641a96c8 100644
--- a/sdk/keyvault/keyvault-keys/src/cryptography/remoteCryptographyProvider.ts
+++ b/sdk/keyvault/keyvault-keys/src/cryptography/remoteCryptographyProvider.ts
@@ -2,7 +2,6 @@
// Licensed under the MIT License.
import { TokenCredential } from "@azure/core-auth";
-import { bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline";
import {
DecryptOptions,
@@ -34,7 +33,7 @@ import { getKeyFromKeyBundle } from "../transformations";
import { createHash } from "./crypto";
import { CryptographyProvider, CryptographyProviderOperation } from "./models";
import { logger } from "../log";
-import { createKeyVaultChallengeCallbacks } from "@azure/keyvault-common";
+import { keyVaultAuthenticationPolicy } from "@azure/keyvault-common";
import { tracingClient } from "../tracing";
/**
@@ -382,12 +381,6 @@ function getOrInitializeClient(
: libInfo,
};
- const authPolicy = bearerTokenAuthenticationPolicy({
- credential,
- scopes: [], // Scopes are going to be defined by the challenge callbacks.
- challengeCallbacks: createKeyVaultChallengeCallbacks(options),
- });
-
const internalPipelineOptions = {
...options,
loggingOptions: {
@@ -404,7 +397,12 @@ function getOrInitializeClient(
options.serviceVersion || LATEST_API_VERSION,
internalPipelineOptions,
);
- client.pipeline.addPolicy(authPolicy);
+
+ // The authentication policy must come after the deserialization policy since the deserialization policy
+ // converts 401 responses to an Error, and we don't want to deal with that.
+ client.pipeline.addPolicy(keyVaultAuthenticationPolicy(credential, options), {
+ afterPolicies: ["deserializationPolicy"],
+ });
return client;
}
diff --git a/sdk/keyvault/keyvault-keys/src/generated/keyVaultClient.ts b/sdk/keyvault/keyvault-keys/src/generated/keyVaultClient.ts
index 5b616babff38..886842f9ac0c 100644
--- a/sdk/keyvault/keyvault-keys/src/generated/keyVaultClient.ts
+++ b/sdk/keyvault/keyvault-keys/src/generated/keyVaultClient.ts
@@ -98,7 +98,7 @@ export class KeyVaultClient extends coreHttpCompat.ExtendedServiceClient {
requestContentType: "application/json; charset=utf-8"
};
- const packageDetails = `azsdk-js-keyvault-keys/4.8.1`;
+ const packageDetails = `azsdk-js-keyvault-keys/4.9.0`;
const userAgentPrefix =
options.userAgentOptions && options.userAgentOptions.userAgentPrefix
? `${options.userAgentOptions.userAgentPrefix} ${packageDetails}`
diff --git a/sdk/keyvault/keyvault-keys/src/index.ts b/sdk/keyvault/keyvault-keys/src/index.ts
index 82449bf42554..3a6aaba78d72 100644
--- a/sdk/keyvault/keyvault-keys/src/index.ts
+++ b/sdk/keyvault/keyvault-keys/src/index.ts
@@ -2,8 +2,6 @@
// Licensed under the MIT License.
///
-import { bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline";
-
import { TokenCredential } from "@azure/core-auth";
import { logger } from "./log";
@@ -19,7 +17,7 @@ import {
} from "./generated/models";
import { KeyVaultClient } from "./generated/keyVaultClient";
import { SDK_VERSION } from "./constants";
-import { createKeyVaultChallengeCallbacks } from "@azure/keyvault-common";
+import { keyVaultAuthenticationPolicy } from "@azure/keyvault-common";
import { DeleteKeyPoller } from "./lro/delete/poller";
import { RecoverDeletedKeyPoller } from "./lro/recover/poller";
@@ -260,12 +258,6 @@ export class KeyClient {
: libInfo,
};
- const authPolicy = bearerTokenAuthenticationPolicy({
- credential,
- scopes: [], // Scopes are going to be defined by the challenge callbacks.
- challengeCallbacks: createKeyVaultChallengeCallbacks(pipelineOptions),
- });
-
const internalPipelineOptions = {
...pipelineOptions,
loggingOptions: {
@@ -283,7 +275,12 @@ export class KeyClient {
pipelineOptions.serviceVersion || LATEST_API_VERSION,
internalPipelineOptions,
);
- this.client.pipeline.addPolicy(authPolicy);
+
+ // The authentication policy must come after the deserialization policy since the deserialization policy
+ // converts 401 responses to an Error, and we don't want to deal with that.
+ this.client.pipeline.addPolicy(keyVaultAuthenticationPolicy(credential, pipelineOptions), {
+ afterPolicies: ["deserializationPolicy"],
+ });
}
/**
diff --git a/sdk/keyvault/keyvault-keys/swagger/README.md b/sdk/keyvault/keyvault-keys/swagger/README.md
index 77120b51efa5..225c8d8a8522 100644
--- a/sdk/keyvault/keyvault-keys/swagger/README.md
+++ b/sdk/keyvault/keyvault-keys/swagger/README.md
@@ -15,7 +15,7 @@ output-folder: ../
source-code-folder-path: ./src/generated
disable-async-iterators: true
api-version-parameter: choice
-package-version: 4.8.1
+package-version: 4.9.0
use-extension:
"@autorest/typescript": "6.0.0-beta.19"
```
diff --git a/sdk/keyvault/keyvault-secrets/CHANGELOG.md b/sdk/keyvault/keyvault-secrets/CHANGELOG.md
index 94971bb66a5c..8b5e39936207 100644
--- a/sdk/keyvault/keyvault-secrets/CHANGELOG.md
+++ b/sdk/keyvault/keyvault-secrets/CHANGELOG.md
@@ -1,9 +1,11 @@
# Release History
-## 4.8.1 (Unreleased)
+## 4.9.0 (Unreleased)
### Features Added
+- Add support for Continuous Access Evaluation (CAE). [#31140](https://github.com/Azure/azure-sdk-for-js/pull/31140)
+
### Breaking Changes
### Bugs Fixed
diff --git a/sdk/keyvault/keyvault-secrets/package.json b/sdk/keyvault/keyvault-secrets/package.json
index 381fe6d419b0..34dda6076189 100644
--- a/sdk/keyvault/keyvault-secrets/package.json
+++ b/sdk/keyvault/keyvault-secrets/package.json
@@ -2,7 +2,7 @@
"name": "@azure/keyvault-secrets",
"sdk-type": "client",
"author": "Microsoft Corporation",
- "version": "4.8.1",
+ "version": "4.9.0",
"license": "MIT",
"description": "Isomorphic client library for Azure KeyVault's secrets.",
"homepage": "https://github.com/Azure/azure-sdk-for-js/blob/main/sdk/keyvault/keyvault-secrets/README.md",
@@ -102,7 +102,7 @@
"@azure/core-rest-pipeline": "^1.8.0",
"@azure/core-tracing": "^1.0.0",
"@azure/core-util": "^1.0.0",
- "@azure/keyvault-common": "^1.0.0",
+ "@azure/keyvault-common": "^2.0.0",
"@azure/logger": "^1.0.0",
"tslib": "^2.2.0"
},
diff --git a/sdk/keyvault/keyvault-secrets/src/constants.ts b/sdk/keyvault/keyvault-secrets/src/constants.ts
index 59040838a911..6d660debf3a8 100644
--- a/sdk/keyvault/keyvault-secrets/src/constants.ts
+++ b/sdk/keyvault/keyvault-secrets/src/constants.ts
@@ -1,4 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.
-export const SDK_VERSION: string = "4.8.1";
+export const SDK_VERSION: string = "4.9.0";
diff --git a/sdk/keyvault/keyvault-secrets/src/generated/keyVaultClient.ts b/sdk/keyvault/keyvault-secrets/src/generated/keyVaultClient.ts
index 780dbb27376f..2d89a052c0bf 100644
--- a/sdk/keyvault/keyvault-secrets/src/generated/keyVaultClient.ts
+++ b/sdk/keyvault/keyvault-secrets/src/generated/keyVaultClient.ts
@@ -70,7 +70,7 @@ export class KeyVaultClient extends coreHttpCompat.ExtendedServiceClient {
requestContentType: "application/json; charset=utf-8"
};
- const packageDetails = `azsdk-js-keyvault-secrets/4.8.1`;
+ const packageDetails = `azsdk-js-keyvault-secrets/4.9.0`;
const userAgentPrefix =
options.userAgentOptions && options.userAgentOptions.userAgentPrefix
? `${options.userAgentOptions.userAgentPrefix} ${packageDetails}`
diff --git a/sdk/keyvault/keyvault-secrets/src/index.ts b/sdk/keyvault/keyvault-secrets/src/index.ts
index 7739e64f20ac..b458bda27c2f 100644
--- a/sdk/keyvault/keyvault-secrets/src/index.ts
+++ b/sdk/keyvault/keyvault-secrets/src/index.ts
@@ -4,8 +4,6 @@
import { TokenCredential } from "@azure/core-auth";
-import { bearerTokenAuthenticationPolicy } from "@azure/core-rest-pipeline";
-
import { logger } from "./log.js";
import { PageSettings, PagedAsyncIterableIterator } from "@azure/core-paging";
@@ -18,7 +16,7 @@ import {
SecretBundle,
} from "./generated/models/index.js";
import { KeyVaultClient } from "./generated/keyVaultClient.js";
-import { createKeyVaultChallengeCallbacks } from "@azure/keyvault-common";
+import { keyVaultAuthenticationPolicy } from "@azure/keyvault-common";
import { DeleteSecretPoller } from "./lro/delete/poller.js";
import { RecoverDeletedSecretPoller } from "./lro/recover/poller.js";
@@ -119,12 +117,6 @@ export class SecretClient {
) {
this.vaultUrl = vaultUrl;
- const authPolicy = bearerTokenAuthenticationPolicy({
- credential,
- scopes: [],
- challengeCallbacks: createKeyVaultChallengeCallbacks(pipelineOptions),
- });
-
const internalPipelineOptions = {
...pipelineOptions,
loggingOptions: {
@@ -141,7 +133,12 @@ export class SecretClient {
pipelineOptions.serviceVersion || LATEST_API_VERSION,
internalPipelineOptions,
);
- this.client.pipeline.addPolicy(authPolicy);
+
+ // The authentication policy must come after the deserialization policy since the deserialization policy
+ // converts 401 responses to an Error, and we don't want to deal with that.
+ this.client.pipeline.addPolicy(keyVaultAuthenticationPolicy(credential, pipelineOptions), {
+ afterPolicies: ["deserializationPolicy"],
+ });
}
/**
diff --git a/sdk/keyvault/keyvault-secrets/swagger/README.md b/sdk/keyvault/keyvault-secrets/swagger/README.md
index c08b518a860e..f1aa9b78be22 100644
--- a/sdk/keyvault/keyvault-secrets/swagger/README.md
+++ b/sdk/keyvault/keyvault-secrets/swagger/README.md
@@ -20,5 +20,5 @@ input-file: https://raw.githubusercontent.com/Azure/azure-rest-api-specs/7452e1c
output-folder: ../
source-code-folder-path: ./src/generated
hide-clients: true
-package-version: 4.8.1
+package-version: 4.9.0
```