diff --git a/sdk/keyvault/keyvault-common/CHANGELOG.md b/sdk/keyvault/keyvault-common/CHANGELOG.md index 6487b537e5ac..790064388d41 100644 --- a/sdk/keyvault/keyvault-common/CHANGELOG.md +++ b/sdk/keyvault/keyvault-common/CHANGELOG.md @@ -4,6 +4,8 @@ ### Features Added +- Add support for Continuous Access Evaluation (CAE). + ### Breaking Changes ### Bugs Fixed diff --git a/sdk/keyvault/keyvault-common/package.json b/sdk/keyvault/keyvault-common/package.json index bb1863edcc76..33b2c1dbb330 100644 --- a/sdk/keyvault/keyvault-common/package.json +++ b/sdk/keyvault/keyvault-common/package.json @@ -58,6 +58,7 @@ "@azure/core-tracing": "^1.0.0", "@azure/core-auth": "^1.3.0", "@azure/abort-controller": "^2.0.0", + "@azure/logger": "^1.1.5", "tslib": "^2.2.0" }, "devDependencies": { diff --git a/sdk/keyvault/keyvault-common/src/challengeBasedAuthenticationPolicy.ts b/sdk/keyvault/keyvault-common/src/challengeBasedAuthenticationPolicy.ts index 54f8eba7d07c..35a059032b4e 100644 --- a/sdk/keyvault/keyvault-common/src/challengeBasedAuthenticationPolicy.ts +++ b/sdk/keyvault/keyvault-common/src/challengeBasedAuthenticationPolicy.ts @@ -11,6 +11,7 @@ import { import { WWWAuthenticate, parseWWWAuthenticateHeader } from "./parseWWWAuthenticate.js"; import { GetTokenOptions } from "@azure/core-auth"; +import { logger } from "./logger.js"; /** * @internal @@ -35,6 +36,7 @@ type ChallengeState = | { status: "complete"; scopes: string[]; + claims?: string; }; /** @@ -112,7 +114,11 @@ 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: challengeState.claims !== undefined, + claims: challengeState.claims, + }); if (token) { request.headers.set("authorization", `Bearer ${token.token}`); } @@ -154,8 +160,19 @@ export function createKeyVaultChallengeCallbacks( verifyChallengeResource(scope, request); } + const { error, claims: base64EncodedClaims } = parsedChallenge; + let claims: string | undefined = undefined; + if (error) { + logger.verbose("Received error in WWW-Authenticate header:", error); + if (error === "insufficient_claims" && base64EncodedClaims) { + claims = atob(base64EncodedClaims); + } + } + const accessToken = await getAccessToken([scope], { ...getTokenOptions, + enableCae: claims !== undefined, + claims, tenantId: parsedChallenge.tenantId, }); 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/test/internal/challengeAuthenticationCallbacks.spec.ts b/sdk/keyvault/keyvault-common/test/internal/challengeAuthenticationCallbacks.spec.ts index 1145edba04aa..ebe5556207f4 100644 --- a/sdk/keyvault/keyvault-common/test/internal/challengeAuthenticationCallbacks.spec.ts +++ b/sdk/keyvault/keyvault-common/test/internal/challengeAuthenticationCallbacks.spec.ts @@ -89,6 +89,45 @@ describe("Challenge based authentication tests", function () { expect(options.request.headers.get("authorization")).toBeUndefined(); }); + + it("Handles insufficient claims", async () => { + const options: AuthorizeRequestOptions = { + getAccessToken: () => { + return Promise.resolve({ token: "access_token", expiresOnTimestamp: 1000 }); + }, + request: createPipelineRequest({ + url: "https://foo.bar", + headers: createHttpHeaders({}), + }), + scopes: [], + }; + + let called = false; + + await challengeCallbacks.authorizeRequestOnChallenge!({ + getAccessToken: (scopes, getAccessTokenOptions) => { + expect(scopes).to.deep.equal(["https://vault.azure.net/.default"]); + expect(getAccessTokenOptions.claims).to.equal(`{"claim":"fooo"}`); + expect(getAccessTokenOptions.enableCae).toBeTruthy(); + called = true; + return Promise.resolve({ token: "successful_token", expiresOnTimestamp: 999999999 }); + }, + request, + response: { + headers: createHttpHeaders({ + "WWW-Authenticate": `Bearer resource="https://vault.azure.net" error="insufficient_claims" claims="eyJjbGFpbSI6ImZvb28ifQ=="`, + }), + request, + status: 401, + }, + scopes: [], + }); + + await challengeCallbacks.authorizeRequest!(options); + + expect(options.request.headers.get("authorization")).toEqual("Bearer access_token"); + expect(called).toBeTruthy(); + }); }); describe("authorizeRequestOnChallenge", () => {