Skip to content
Merged
Show file tree
Hide file tree
Changes from 9 commits
Commits
Show all changes
61 commits
Select commit Hold shift + click to select a range
9795395
first draft with tests
sadasant Feb 19, 2021
10f90df
changelog entry
sadasant Feb 20, 2021
f37ce5a
claims on core-auth
sadasant Feb 20, 2021
1f682a6
improvements
sadasant Mar 1, 2021
85d52c1
comments and exporting a missing type
sadasant Mar 1, 2021
8fcc45b
added parseCAEChallenges test
sadasant Mar 1, 2021
b1df5f9
moved this test to be node only
sadasant Mar 1, 2021
2cce476
Merge branch 'master' into core-https/CAE-support
sadasant Mar 1, 2021
8fa9ec1
fixing bad link in the CHANGELOG
sadasant Mar 1, 2021
ebbed8a
cleanups after talk with Jeff
sadasant Mar 2, 2021
272d240
claims to challengeClaims
sadasant Mar 2, 2021
fa0646c
formatting
sadasant Mar 2, 2021
a8a7a50
back to claims after Scott feedback
sadasant Mar 2, 2021
b0a8337
claims changelog entry
sadasant Mar 2, 2021
663d7eb
renamed the authentication context, reduced the number of requests on…
sadasant Mar 3, 2021
435776e
processing challenges even if we already have a token
sadasant Mar 3, 2021
480ac2c
removing getToken from the possible parameters
sadasant Mar 3, 2021
670ce25
Merge remote-tracking branch 'upstream/master' into core-https/CAE-su…
jeremymeng Mar 24, 2021
d6b32a2
Fix merging issues
jeremymeng Mar 24, 2021
87d4b55
Pass the whole tracingOptions to GetTokenOptions
jeremymeng Mar 24, 2021
dda4039
Fix linting errors
jeremymeng Mar 25, 2021
ee68f6c
Fix test failures on NodeJS v8 and v10
jeremymeng Mar 25, 2021
0a09732
Update to make it work for container registry
jeremymeng Mar 30, 2021
83720b2
wip
sadasant Mar 31, 2021
a68a673
tests passing
sadasant Mar 31, 2021
5e0a716
API review cleanup
sadasant Mar 31, 2021
10ca6e5
Alignment with .Net
sadasant Mar 31, 2021
c9e6c47
Merge pull request #21 from sadasant/core-https/CAE-support-proposal
sadasant Apr 1, 2021
9f31ecf
Removed console.log
sadasant Apr 1, 2021
a789c76
Merge remote-tracking branch 'Azure/master' into core-https/CAE-support
sadasant Apr 1, 2021
633dfe9
fixes after merging the token cycler
sadasant Apr 1, 2021
b300a51
lint fix and small changelog improvement
sadasant Apr 1, 2021
b44184a
Merge remote-tracking branch 'Azure/master' into core-https/CAE-support
sadasant Apr 1, 2021
b387796
Bug fix: error without challenge should be re-thrown
sadasant Apr 2, 2021
5a4d88b
sendRequest to shouldSendRequest, thanks to Deya
sadasant Apr 2, 2021
d260f63
Move CAE support into a new policy
jeremymeng Apr 7, 2021
9fe1712
Merge remote-tracking branch 'upstream/master' into core-https/CAE-su…
jeremymeng Apr 7, 2021
e4ec342
Restore original bearerTokenAuthenticationPolicy
jeremymeng Apr 7, 2021
ab1c2e4
Refactoring
jeremymeng Apr 7, 2021
892ee7c
Rename challenge callbacks
jeremymeng Apr 7, 2021
d54547c
Pass full response to authorizeRequestOnChallenge()
jeremymeng Apr 7, 2021
9da2dd6
Expose WWW-Authenticate challenge parsing support
jeremymeng Apr 7, 2021
6631d38
Keep properties of object implementing `challengeCallbacks`
jeremymeng Apr 8, 2021
9a3e758
Merge remote-tracking branch 'upstream/master' into core-https/CAE-su…
jeremymeng Apr 8, 2021
c1c1213
- Remove unnecessary condition
jeremymeng Apr 8, 2021
9c97a64
Make `scopes` to have type `string[]`
jeremymeng Apr 9, 2021
dbf2e1f
- Make default on-challenge callback to just return false
jeremymeng Apr 13, 2021
06e6854
Move www-authenticate parsing support to core-util
jeremymeng Apr 13, 2021
e28bb35
Fix lint error
jeremymeng Apr 13, 2021
373589d
- Remove default authorizeRequestOnChallenge call back
jeremymeng Apr 13, 2021
1f2d6a3
Split ChallengeCallbackOptions into two for the two callbacks
jeremymeng Apr 15, 2021
fa118af
Remove single string scope
jeremymeng Apr 15, 2021
ce29fbf
Update version and CHANGELOG text
jeremymeng Apr 19, 2021
e78d193
Merge remote-tracking branch 'upstream/master' into core-https/CAE-su…
jeremymeng Apr 19, 2021
5ff8119
Conform to versioning convention
jeremymeng Apr 19, 2021
4b14121
Merge remote-tracking branch 'upstream/master' into core-https/CAE-su…
jeremymeng Apr 19, 2021
0c2392a
Remove `claims` change as architects are re-visiting this
jeremymeng Apr 23, 2021
5314922
Undo formatting change
jeremymeng Apr 23, 2021
e9b8a8d
Fix test
jeremymeng Apr 23, 2021
0d89d9d
Address CR feedback
jeremymeng Apr 26, 2021
5b64e51
Type `cachedToken` as `AccessToken | null` for consistency
jeremymeng Apr 26, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions sdk/core/core-auth/review/core-auth.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export class AzureSASCredential implements SASCredential {
// @public
export interface GetTokenOptions {
abortSignal?: AbortSignalLike;
claims?: string;
Comment thread
sadasant marked this conversation as resolved.
Outdated

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should claims be a Record<string, any> or just string?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

pending architects' discussion.

requestOptions?: {
timeout?: number;
};
Expand Down
4 changes: 4 additions & 0 deletions sdk/core/core-auth/src/tokenCredential.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,10 @@ export interface TokenCredential {
* Defines options for TokenCredential.getToken.
*/
export interface GetTokenOptions {
/**
* Challenge claims
*/
claims?: string;
/**
* The signal which can be used to abort requests.
*/
Expand Down
1 change: 1 addition & 0 deletions sdk/core/core-https/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

- Changed from exposing `DefaultHttpsClient` as a class directly, to providing `createDefaultHttpsClient()` to instantiate the appropriate runtime class.
- Fix an issue when passing in proxy hosts. [PR 13911](https://github.com/Azure/azure-sdk-for-js/pull/13911)
- Added a `challengeAuthenticationPolicy` with features to support [CAE](https://docs.microsoft.com/azure/active-directory/conditional-access/concept-continuous-access-evaluation).

## 1.0.0-beta.1 (2021-02-04)

Expand Down
21 changes: 21 additions & 0 deletions sdk/core/core-https/review/core-https.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,12 @@ export interface AddPipelineOptions {
phase?: PipelinePhase;
}

// @public
export interface AuthenticationContext {
claims?: string;
scopes?: string | string[];
Comment thread
sadasant marked this conversation as resolved.
Outdated
}

// @public
export function bearerTokenAuthenticationPolicy(options: BearerTokenAuthenticationPolicyOptions): PipelinePolicy;
Comment thread
sadasant marked this conversation as resolved.

Expand All @@ -25,10 +31,25 @@ export const bearerTokenAuthenticationPolicyName = "bearerTokenAuthenticationPol

// @public
export interface BearerTokenAuthenticationPolicyOptions {
authenticationContext?: AuthenticationContext;
credential: TokenCredential;
scopes: string | string[];
}

// @public
export function challengeAuthenticationPolicy(options: ChallengeAuthenticationPolicyOptions): PipelinePolicy;

// @public
export const challengeAuthenticationPolicyName = "challengeAuthenticationPolicy";

// @public
export interface ChallengeAuthenticationPolicyOptions {
authenticationContext: AuthenticationContext;
getChallenge?(response: PipelineResponse): string | undefined;
prepareRequest?(request: PipelineRequest): Promise<void>;
processChallenge(challenge: string, context: AuthenticationContext): Promise<boolean>;
}

// @public
export function createDefaultHttpsClient(): HttpsClient;

Expand Down
8 changes: 7 additions & 1 deletion sdk/core/core-https/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,8 @@ export {
ProxySettings,
RawHttpHeaders,
TransferProgressEvent,
RequestBodyType
RequestBodyType,
AuthenticationContext
} from "./interfaces";
export {
AddPolicyOptions as AddPipelineOptions,
Expand Down Expand Up @@ -67,4 +68,9 @@ export {
BearerTokenAuthenticationPolicyOptions,
bearerTokenAuthenticationPolicyName
} from "./policies/bearerTokenAuthenticationPolicy";
export {
challengeAuthenticationPolicy,
ChallengeAuthenticationPolicyOptions,
challengeAuthenticationPolicyName
} from "./policies/challengeAuthenticationPolicy";
export { ndJsonPolicy, ndJsonPolicyName } from "./policies/ndJsonPolicy";
14 changes: 14 additions & 0 deletions sdk/core/core-https/src/interfaces.ts
Original file line number Diff line number Diff line change
Expand Up @@ -274,3 +274,17 @@ export type FormDataValue = string | Blob;
* A simple object that provides form data, as if from a browser form.
*/
export type FormDataMap = { [key: string]: FormDataValue | FormDataValue[] };

/**
* Allows the discovery of authentication properties.
*/
export interface AuthenticationContext {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should this live in core-auth maybe?

@sadasant sadasant Mar 3, 2021

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is really specific to the challenge methods in bearerTokenAuthenticationPolicy. We could also name it like BearerTokenChallengeResult. We could also inline it like this: processChallenge(challenge: string): Promise<{ scopes?: string[], claims?: string } | undefined>; to avoid having to have a new interface. 🤔

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll go with BearerTokenChallengeResult for now, but please feel free to point to another direction!

/**
* Scopes to overwrite during the get token request.
*/
scopes?: string | string[];
/**
* Claims that can be used by the credential's getToken request.
*/
claims?: string;
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { PipelineResponse, PipelineRequest, SendRequest } from "../interfaces";
import {
PipelineResponse,
PipelineRequest,
SendRequest,
AuthenticationContext
} from "../interfaces";
import { PipelinePolicy } from "../pipeline";
import { TokenCredential, GetTokenOptions } from "@azure/core-auth";
import { AccessTokenCache, ExpiringAccessTokenCache } from "../accessTokenCache";
Expand All @@ -23,6 +28,10 @@ export interface BearerTokenAuthenticationPolicyOptions {
* The scopes for which the bearer token applies.
*/
scopes: string | string[];
/**
* Allows the dynamic discovery of authentication properties.
*/
authenticationContext?: AuthenticationContext;
}

/**
Expand All @@ -34,10 +43,14 @@ export function bearerTokenAuthenticationPolicy(
): PipelinePolicy {
const { credential, scopes } = options;
const tokenCache: AccessTokenCache = new ExpiringAccessTokenCache();
const authenticationContext: AuthenticationContext = options.authenticationContext || {};

async function getToken(tokenOptions: GetTokenOptions): Promise<string | undefined> {
let accessToken = tokenCache.getCachedToken();
if (accessToken === undefined) {
accessToken = (await credential.getToken(scopes, tokenOptions)) || undefined;
accessToken =
(await credential.getToken(authenticationContext.scopes || scopes, tokenOptions)) ||
undefined;
tokenCache.setCachedToken(accessToken);
}

Expand All @@ -50,8 +63,10 @@ export function bearerTokenAuthenticationPolicy(
abortSignal: request.abortSignal,
tracingOptions: {
spanOptions: request.spanOptions
}
},
claims: authenticationContext.claims
});

request.headers.set("Authorization", `Bearer ${token}`);
return next(request);
}
Expand Down
86 changes: 86 additions & 0 deletions sdk/core/core-https/src/policies/challengeAuthenticationPolicy.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import {
PipelineResponse,
PipelineRequest,
SendRequest,
AuthenticationContext
} from "../interfaces";
import { PipelinePolicy } from "../pipeline";

/**
* The programmatic identifier of the challengeAuthenticationPolicy.
*/
export const challengeAuthenticationPolicyName = "challengeAuthenticationPolicy";

/**
* Options to configure the challengeAuthenticationPolicy
*/
export interface ChallengeAuthenticationPolicyOptions {
/**
* Authentication context that can bind together the challengeAuthenticationPolicy with other policies.
* The second policy should also receive an authenticationContext in their options.
* By default, the only other policy that supports this is the bearerTokenAuthenticationPolicy.
*/
authenticationContext: AuthenticationContext;
/**
* Allows for the customization of the next request before its sent.
*/
prepareRequest?(request: PipelineRequest): Promise<void>;
/**
* Defines how to get the challenge from the PipelineResponse.
* By default we will retrieve the challenge only if the response status code was 401,
* and if the response contained the header "WWW-Authenticate" with a non-empty value.
*/
getChallenge?(response: PipelineResponse): string | undefined;
/**
* Updates the authentication context based on the challenge.
*/
processChallenge(challenge: string, context: AuthenticationContext): Promise<boolean>;
}

/**
* By default we will retrieve the challenge only if the response status code was 401,
* and if the response contained the header "WWW-Authenticate" with a non-empty value.
*/
function defaultGetChallenge(response: PipelineResponse): string | undefined {
const challenges = response.headers.get("WWW-Authenticate");
if (response.status === 401 && challenges) {
return challenges;
}
return;
}

/**
* Allows processing authentication challenges.
*/
export function challengeAuthenticationPolicy(
Comment thread
sadasant marked this conversation as resolved.
Outdated
options: ChallengeAuthenticationPolicyOptions
): PipelinePolicy {
const {
prepareRequest,
getChallenge = defaultGetChallenge,
processChallenge,
authenticationContext
} = options;

return {
name: challengeAuthenticationPolicyName,
async sendRequest(request: PipelineRequest, next: SendRequest): Promise<PipelineResponse> {
if (prepareRequest) {
await prepareRequest(request);
}
const response = await next(request);
const challenge = getChallenge(response);
if (
challenge &&
processChallenge &&
(await processChallenge(challenge, authenticationContext))
Comment thread
sadasant marked this conversation as resolved.
Outdated
) {
return next(request);
}
return response;
}
};
}
38 changes: 38 additions & 0 deletions sdk/core/core-https/src/util/parseCAEChallenges.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
* parseCAEChallenges Parses multiple challenges into an array of objects.
* Allows users to specify the challenge type through the TChallenge type parameter.
*/
export function parseCAEChallenges<TChallenge>(challenges: string): TChallenge[] {
if (!challenges) return [{} as TChallenge];

// Parses a `key="value"` string into an object with { key: "key", value: "value" }
const parseKeyValue = (keyValue: string): { key: string; value: string } =>
(keyValue.match(/(?<key>\w+(?==))="(?<value>[^"]*)"/) as any)?.groups || {};
Comment thread
sadasant marked this conversation as resolved.
Outdated

// Receives an array of `key="value"` strings
// And produces an object with properties based on those keys and values.
const groupKeyValues = (keyValues: string[]): TChallenge =>
keyValues.reduce((parsedChallenge, keyValue) => {
const { key, value } = parseKeyValue(keyValue);
if (!key) {
return parsedChallenge;
}
return {
...parsedChallenge,
[key]: value || ""
};
}, {}) as TChallenge;

// Splits a string challenge composed of key="value" elements separated by comma
// into an array of `key="value"` strings.
const separateKeyValues = (challenge: string): string[] =>
`${challenge}, `.match(/(\w+="[^"]*"(?=, ))/g) || [];
Comment thread
sadasant marked this conversation as resolved.
Outdated

// Each set of challenges will be separated by "Bearer ".
const bearerSeparated = challenges.split("Bearer").filter((x) => x);

return bearerSeparated.map((challenge) => groupKeyValues(separateKeyValues(challenge)));
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,8 @@ describe("BearerTokenAuthenticationPolicy", function() {
assert(
fakeGetToken.calledWith(tokenScopes, {
abortSignal: undefined,
tracingOptions: { spanOptions: undefined }
tracingOptions: { spanOptions: undefined },
claims: undefined
}),
"fakeGetToken called incorrectly."
);
Expand Down
Loading