Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
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;
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 {
/**
* Additional claims to be included in the token. See {@link https://openid.net/specs/openid-connect-core-1_0-final.html#ClaimsParameter} for more information on format and content.
*/
claims?: string;
/**
* The signal which can be used to abort requests.
*/
Expand Down
42 changes: 41 additions & 1 deletion sdk/core/core-http/review/core-http.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,8 @@ export class AccessTokenRefresher {
constructor(credential: TokenCredential, scopes: string | string[], requiredMillisecondsBeforeNewRefresh?: number);
isReady(): boolean;
refresh(options: GetTokenOptions): Promise<AccessToken | undefined>;
}
setScopes(scopes: string | string[]): void;
}

// @public
export interface ApiKeyCredentialOptions {
Expand Down Expand Up @@ -96,9 +97,45 @@ export class BasicAuthenticationCredentials implements ServiceClientCredentials
userName: string;
}

// @public
export class BearerTokenAuthenticationPolicy extends BaseRequestPolicy {
constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptions, tokenCache: AccessTokenCache, tokenRefresher: AccessTokenRefresher);
sendRequest(webResource: WebResourceLike): Promise<HttpOperationResponse>;
}

// @public
export function bearerTokenAuthenticationPolicy(credential: TokenCredential, scopes: string | string[]): RequestPolicyFactory;

// Warning: (ae-forgotten-export) The symbol "BaseChallengePolicy" needs to be exported by the entry point coreHttp.d.ts
//
// @public
export class BearerTokenChallengeAuthenticationPolicy<TChallenge> extends BaseChallengePolicy {
constructor(nextPolicy: RequestPolicy, options: RequestPolicyOptions, tokenCache: AccessTokenCache, tokenRefresher: AccessTokenRefresher, challengeCache: ChallengeCache<TChallenge>);
// (undocumented)
protected challengeCache: ChallengeCache<TChallenge>;
// (undocumented)
protected claims?: string;
protected getChallenge(response: HttpOperationResponse): string | undefined;
protected getToken(options: GetTokenOptions): Promise<string | undefined>;
protected loadToken(webResource: WebResource, accessToken?: string): Promise<void>;
// (undocumented)
protected scope?: string;
sendRequest(webResource: WebResourceLike): Promise<HttpOperationResponse>;
// (undocumented)
protected tokenCache: AccessTokenCache;
// (undocumented)
protected tokenRefresher: AccessTokenRefresher;
}

// @public
export class ChallengeCache<TChallenge> {
// (undocumented)
challenge?: TChallenge;
equalTo(other: TChallenge | undefined): boolean;
// (undocumented)
setCachedChallenge(challenge: TChallenge): void;
}

// @public (undocumented)
export interface CompositeMapper extends BaseMapper {
// (undocumented)
Expand Down Expand Up @@ -520,6 +557,9 @@ export interface ParameterValue {
value: any;
}

// @public
export function parseCAEChallenges<TChallenge>(challenges: string): TChallenge[];

// @public
export function parseXML(str: string, opts?: SerializerOptions): Promise<any>;

Expand Down
4 changes: 4 additions & 0 deletions sdk/core/core-http/src/CAE/baseChallenge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

export type ChallengeWithClaims = Record<"claims", string>;
30 changes: 30 additions & 0 deletions sdk/core/core-http/src/CAE/challengeCache.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

/**
* Compares challenges
*/
export function compareChallenges<TChallenge>(a: TChallenge, b: TChallenge): boolean {
return a && b && JSON.stringify(a).toLowerCase() === JSON.stringify(b).toLowerCase();
}

/**
* Helps keep a copy of any previous authentication challenges,
* so that we can compare on any further request.
*/
export class ChallengeCache<TChallenge> {
public challenge?: TChallenge;

public setCachedChallenge(challenge: TChallenge): void {
this.challenge = challenge;
}

/**
* Checks that this AuthenticationChallenge is equal to another one given.
* Only compares the scope.
* @param other - The other KeyVaultAuthenticationChallenge
*/
public equalTo(other: TChallenge | undefined): boolean {
return other ? compareChallenges(this.challenge, other) : false;
}
}
38 changes: 38 additions & 0 deletions sdk/core/core-http/src/CAE/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 || {};

// 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) || [];

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

return bearerSeparated.map((challenge) => groupKeyValues(separateKeyValues(challenge)));
}
4 changes: 4 additions & 0 deletions sdk/core/core-http/src/coreHttp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,11 @@ export { PipelineOptions, InternalPipelineOptions } from "./pipelineOptions";
export { QueryCollectionFormat } from "./queryCollectionFormat";
export { Constants } from "./util/constants";
export { bearerTokenAuthenticationPolicy } from "./policies/bearerTokenAuthenticationPolicy";
export { parseCAEChallenges } from "./CAE/parseCAEChallenges";
export { ChallengeCache } from "./CAE/challengeCache";
export { LogPolicyOptions, logPolicy } from "./policies/logPolicy";
export { BearerTokenAuthenticationPolicy } from "./policies/bearerTokenAuthenticationPolicy";
export { BearerTokenChallengeAuthenticationPolicy } from "./policies/bearerTokenChallengeAuthenticationPolicy";
export {
BaseRequestPolicy,
RequestPolicy,
Expand Down
7 changes: 7 additions & 0 deletions sdk/core/core-http/src/credentials/accessTokenRefresher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,11 @@ export class AccessTokenRefresher {

return this.promise;
}

/**
* Allows setting the scope.
*/
public setScopes(scopes: string | string[]): void {
this.scopes = scopes;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,155 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT license.

import { GetTokenOptions } from "@azure/core-auth";
import { BaseRequestPolicy, RequestPolicy, RequestPolicyOptions } from "../policies/requestPolicy";
import { Constants } from "../util/constants";
import { HttpOperationResponse } from "../httpOperationResponse";
import { HttpHeaders } from "../httpHeaders";
import { WebResource, WebResourceLike } from "../webResource";
import { AccessTokenCache } from "../credentials/accessTokenCache";
import { AccessTokenRefresher } from "../credentials/accessTokenRefresher";
import { ChallengeCache } from "../CAE/challengeCache";

/**
* Represents a policy that handles challenges.
*/
export abstract class BaseChallengePolicy extends BaseRequestPolicy {
/**
* Allows configuration on the request before its sent.
*/
protected prepareRequest?(webResource: WebResourceLike): Promise<void>;

/**
* Tries to retrieve the challenge.
*/
protected getChallenge?(response: HttpOperationResponse): string | undefined;

/**
* Authorizes request according to an authentication challenge.
*/
protected processChallenge?(_webResource: WebResourceLike, challenge?: string): Promise<boolean>;
}

/**
*
* Provides a RequestPolicy that can request a token from a TokenCredential
* implementation and then apply it to the Authorization header of a request
* as a Bearer token.
*
*/
export class BearerTokenChallengeAuthenticationPolicy<TChallenge> extends BaseChallengePolicy {
protected scope?: string;
protected claims?: string;

/**
* Creates a new BearerTokenChallengeAuthenticationPolicy object.
*
* @param nextPolicy - The next RequestPolicy in the request pipeline.
* @param options - Options for this RequestPolicy.
* @param scopes - The scopes for which the bearer token applies.
* @param tokenCache - The cache for the most recent AccessToken returned from the TokenCredential.
* @param tokenRefresher - The AccessToken refresher.
* @param challengeCache - The cache for the challenge.
*/
constructor(
nextPolicy: RequestPolicy,
options: RequestPolicyOptions,
protected tokenCache: AccessTokenCache,
protected tokenRefresher: AccessTokenRefresher,
protected challengeCache: ChallengeCache<TChallenge>
) {
super(nextPolicy, options);
}

/**
* Gets or updates the token from the token cache into the headers of the received web resource.
*/
protected async loadToken(webResource: WebResource, accessToken?: string): Promise<void> {
if (!accessToken) {
const getTokenOptions: GetTokenOptions = {
abortSignal: webResource.abortSignal,
tracingOptions: {
spanOptions: webResource.spanOptions
}
};
if (this.claims) {
getTokenOptions.claims = this.claims;
}
accessToken = await this.getToken(getTokenOptions);
}

if (accessToken) {
webResource.headers.set(Constants.HeaderConstants.AUTHORIZATION, `Bearer ${accessToken}`);
}
}

/**
* Tries to retrieve the challenge.
*/
protected getChallenge(response: HttpOperationResponse): string | undefined {
const challenges = response.headers.get("WWW-Authenticate");
if (response.status === 401 && challenges) {
return challenges;
}
return;
}

/**
* Applies the Bearer token to the request through the Authorization header.
*/
public async sendRequest(webResource: WebResourceLike): Promise<HttpOperationResponse> {
if (!webResource.headers) webResource.headers = new HttpHeaders();

if (this.prepareRequest) {
await this.prepareRequest(webResource);
} else {
await this.loadToken(webResource);
}

const response = await this._nextPolicy.sendRequest(webResource);

if (
this.processChallenge &&
(await this.processChallenge(webResource, this.getChallenge(response)))
) {
return this._nextPolicy.sendRequest(webResource);
}

return response;
}

/**
* Attempts a token update if any other time related conditionals have been reached based on the tokenRefresher class.
*/
private async updateTokenIfNeeded(options: GetTokenOptions): Promise<void> {
if (this.tokenRefresher.isReady()) {
const accessToken = await this.tokenRefresher.refresh(options);
this.tokenCache.setCachedToken(accessToken);
}
}

/**
* Tries to retrieve the token from the cache, otherwise tries to refresh the token.
*/
protected async getToken(options: GetTokenOptions): Promise<string | undefined> {
let accessToken = this.tokenCache.getCachedToken();
if (accessToken === undefined) {
// Updating the scope based on the local one
if (this.scope) {
this.tokenRefresher.setScopes(this.scope);
}
// Waiting for the next refresh only if the cache is unable to retrieve the access token,
// which means that it has expired, or it has never been set.
accessToken = await this.tokenRefresher.refresh(options);
this.tokenCache.setCachedToken(accessToken);
} else {
// If we still have a cached access token,
// And any other time related conditionals have been reached based on the tokenRefresher class,
// then attempt to refresh without waiting.
this.updateTokenIfNeeded(options);
}

return accessToken ? accessToken.token : undefined;
}
}
10 changes: 9 additions & 1 deletion sdk/core/core-http/src/util/base64.browser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export function encodeString(value: string): string {

/**
* Encodes a byte array in base64 format.
* @param value - The Uint8Aray to encode
* @param value - The Uint8Array to encode
*/
export function encodeByteArray(value: Uint8Array): string {
let str = "";
Expand All @@ -33,3 +33,11 @@ export function decodeString(value: string): Uint8Array {
}
return arr;
}

/**
* Converts a uint8Array to a string.
*/
export function uint8ArrayToString(ab: Uint8Array): string {
const decoder = new TextDecoder("utf-8");
return decoder.decode(ab);
}
7 changes: 7 additions & 0 deletions sdk/core/core-http/src/util/base64.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,3 +27,10 @@ export function encodeByteArray(value: Uint8Array): string {
export function decodeString(value: string): Uint8Array {
return Buffer.from(value, "base64");
}

/**
* Converts a uint8Array to a string.
*/
export function uint8ArrayToString(ab: Uint8Array): string {
return Buffer.from(ab).toString("utf-8");
}
Loading