Skip to content

Commit

Permalink
[keyvault] CAE support
Browse files Browse the repository at this point in the history
  • Loading branch information
timovv committed Sep 17, 2024
1 parent 738c910 commit 143fddf
Show file tree
Hide file tree
Showing 6 changed files with 80 additions and 3 deletions.
2 changes: 2 additions & 0 deletions sdk/keyvault/keyvault-common/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@

### Features Added

- Add support for Continuous Access Evaluation (CAE).

### Breaking Changes

### Bugs Fixed
Expand Down
1 change: 1 addition & 0 deletions sdk/keyvault/keyvault-common/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import {
import { WWWAuthenticate, parseWWWAuthenticateHeader } from "./parseWWWAuthenticate.js";

import { GetTokenOptions } from "@azure/core-auth";
import { logger } from "./logger.js";

/**
* @internal
Expand All @@ -35,6 +36,7 @@ type ChallengeState =
| {
status: "complete";
scopes: string[];
claims?: string;
};

/**
Expand Down Expand Up @@ -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}`);
}
Expand Down Expand Up @@ -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,
});

Expand Down
6 changes: 6 additions & 0 deletions sdk/keyvault/keyvault-common/src/logger.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

import { createClientLogger } from "@azure/logger";

export const logger = createClientLogger("keyvault-common");
16 changes: 14 additions & 2 deletions sdk/keyvault/keyvault-common/src/parseWWWAuthenticate.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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)[] = [
Expand All @@ -37,6 +47,8 @@ const validWWWAuthenticateProperties: readonly (keyof WWWAuthenticate)[] = [
"resource",
"scope",
"tenantId",
"claims",
"error",
] as const;

/**
Expand All @@ -52,10 +64,10 @@ export function parseWWWAuthenticateHeader(headerValue: string): WWWAuthenticate
const parsed = headerValue.split(pairDelimiter).reduce<WWWAuthenticate>((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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down

0 comments on commit 143fddf

Please sign in to comment.