Skip to content

Commit

Permalink
Merge a1f4884 into f851df6
Browse files Browse the repository at this point in the history
  • Loading branch information
jmckennon committed Sep 3, 2020
2 parents f851df6 + a1f4884 commit 9a0138c
Show file tree
Hide file tree
Showing 25 changed files with 798 additions and 33 deletions.
@@ -0,0 +1,8 @@
{
"type": "minor",
"comment": "Added client-side throttling to enhance server stability (#1907)",
"packageName": "@azure/msal-browser",
"email": "jamckenn@microsoft.com",
"dependentChangeType": "patch",
"date": "2020-08-11T19:04:58.604Z"
}
@@ -0,0 +1,8 @@
{
"type": "minor",
"comment": "Added client-side throttling to enhance server stability (#1907)",
"packageName": "@azure/msal-common",
"email": "jamckenn@microsoft.com",
"dependentChangeType": "patch",
"date": "2020-08-11T19:05:23.196Z"
}
7 changes: 5 additions & 2 deletions lib/msal-browser/src/app/ClientApplication.ts
Expand Up @@ -5,7 +5,7 @@

import { CryptoOps } from "../crypto/CryptoOps";
import { BrowserStorage } from "../cache/BrowserStorage";
import { Authority, TrustedAuthority, StringUtils, CacheSchemaType, UrlString, ServerAuthorizationCodeResponse, AuthorizationCodeRequest, AuthorizationUrlRequest, AuthorizationCodeClient, PromptValue, SilentFlowRequest, ServerError, InteractionRequiredAuthError, EndSessionRequest, AccountInfo, AuthorityFactory, ServerTelemetryManager, SilentFlowClient, ClientConfiguration, BaseAuthRequest, ServerTelemetryRequest, PersistentCacheKeys, IdToken, ProtocolUtils, ResponseMode, Constants, INetworkModule, AuthenticationResult, Logger } from "@azure/msal-common";
import { Authority, TrustedAuthority, StringUtils, CacheSchemaType, UrlString, ServerAuthorizationCodeResponse, AuthorizationCodeRequest, AuthorizationUrlRequest, AuthorizationCodeClient, PromptValue, SilentFlowRequest, ServerError, InteractionRequiredAuthError, EndSessionRequest, AccountInfo, AuthorityFactory, ServerTelemetryManager, SilentFlowClient, ClientConfiguration, BaseAuthRequest, ServerTelemetryRequest, PersistentCacheKeys, IdToken, ProtocolUtils, ResponseMode, Constants, INetworkModule, AuthenticationResult, Logger, ThrottlingUtils } from "@azure/msal-common";
import { buildConfiguration, Configuration } from "../config/Configuration";
import { TemporaryCacheKeys, InteractionType, ApiId, BrowserConstants, DEFAULT_REQUEST } from "../utils/BrowserConstants";
import { BrowserUtils } from "../utils/BrowserUtils";
Expand Down Expand Up @@ -194,7 +194,7 @@ export abstract class ClientApplication {
const currentAuthority = this.browserStorage.getCachedAuthority();
const authClient = await this.createAuthCodeClient(serverTelemetryManager, currentAuthority);
const interactionHandler = new RedirectHandler(authClient, this.browserStorage);
return await interactionHandler.handleCodeResponse(responseHash, this.browserCrypto);
return await interactionHandler.handleCodeResponse(responseHash, this.browserCrypto, this.config.auth.clientId);
} catch (e) {
serverTelemetryManager.cacheFailedRequest(e);
this.browserStorage.cleanRequest();
Expand Down Expand Up @@ -290,6 +290,9 @@ export abstract class ClientApplication {
// Monitor the window for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds.
const hash = await interactionHandler.monitorPopupForHash(popupWindow, this.config.system.windowHashTimeout);

// Remove throttle if it exists
ThrottlingUtils.removeThrottle(this.browserStorage, this.config.auth.clientId, authCodeRequest.authority, authCodeRequest.scopes);

// Handle response from hash string.
return await interactionHandler.handleCodeResponse(hash);
} catch (e) {
Expand Down
6 changes: 5 additions & 1 deletion lib/msal-browser/src/cache/BrowserStorage.ts
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Constants, PersistentCacheKeys, StringUtils, AuthorizationCodeRequest, ICrypto, CacheSchemaType, AccountEntity, IdTokenEntity, CredentialType, AccessTokenEntity, RefreshTokenEntity, AppMetadataEntity, CacheManager, CredentialEntity, ServerTelemetryCacheValue } from "@azure/msal-common";
import { Constants, PersistentCacheKeys, StringUtils, AuthorizationCodeRequest, ICrypto, CacheSchemaType, AccountEntity, IdTokenEntity, CredentialType, AccessTokenEntity, RefreshTokenEntity, AppMetadataEntity, CacheManager, CredentialEntity, ServerTelemetryCacheValue, ThrottlingEntity } from "@azure/msal-common";
import { CacheOptions } from "../config/Configuration";
import { BrowserAuthError } from "../error/BrowserAuthError";
import { BrowserConfigurationAuthError } from "../error/BrowserConfigurationAuthError";
Expand Down Expand Up @@ -114,6 +114,7 @@ export class BrowserStorage extends CacheManager {
case CacheSchemaType.ACCOUNT:
case CacheSchemaType.CREDENTIAL:
case CacheSchemaType.APP_METADATA:
case CacheSchemaType.THROTTLING:
this.windowStorage.setItem(key, JSON.stringify(value));
break;
case CacheSchemaType.TEMPORARY: {
Expand Down Expand Up @@ -169,6 +170,9 @@ export class BrowserStorage extends CacheManager {
case CacheSchemaType.APP_METADATA: {
return (JSON.parse(value) as AppMetadataEntity);
}
case CacheSchemaType.THROTTLING: {
return (JSON.parse(value) as ThrottlingEntity);
}
case CacheSchemaType.TEMPORARY: {
const itemCookie = this.getItemCookie(key);
if (this.cacheConfig.storeAuthStateInCookie) {
Expand Down
9 changes: 7 additions & 2 deletions lib/msal-browser/src/interaction_handler/RedirectHandler.ts
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { StringUtils, AuthorizationCodeRequest, ICrypto, CacheSchemaType, AuthenticationResult } from "@azure/msal-common";
import { StringUtils, AuthorizationCodeRequest, ICrypto, CacheSchemaType, AuthenticationResult, ThrottlingUtils } from "@azure/msal-common";
import { InteractionHandler } from "./InteractionHandler";
import { BrowserAuthError } from "../error/BrowserAuthError";
import { BrowserConstants, TemporaryCacheKeys } from "../utils/BrowserConstants";
Expand Down Expand Up @@ -46,7 +46,7 @@ export class RedirectHandler extends InteractionHandler {
* Handle authorization code response in the window.
* @param hash
*/
async handleCodeResponse(locationHash: string, browserCrypto?: ICrypto): Promise<AuthenticationResult> {
async handleCodeResponse(locationHash: string, browserCrypto?: ICrypto, clientId?: string): Promise<AuthenticationResult> {
// Check that location hash isn't empty.
if (StringUtils.isEmpty(locationHash)) {
throw BrowserAuthError.createEmptyHashError(locationHash);
Expand All @@ -65,6 +65,11 @@ export class RedirectHandler extends InteractionHandler {
this.authCodeRequest = this.browserStorage.getCachedRequest(requestState, browserCrypto);
this.authCodeRequest.code = authCode;

// Remove throttle if it exists
if (clientId) {
ThrottlingUtils.removeThrottle(this.browserStorage, clientId, this.authCodeRequest.authority, this.authCodeRequest.scopes);
}

// Acquire token with retrieved code.
const tokenResponse = await this.authModule.acquireToken(this.authCodeRequest, cachedNonce, requestState);

Expand Down
2 changes: 1 addition & 1 deletion lib/msal-browser/test/cache/BrowserStorage.spec.ts
Expand Up @@ -495,6 +495,6 @@ describe("BrowserStorage() tests", () => {
// Perform test
const tokenRequest = browserStorage.getCachedRequest(RANDOM_TEST_GUID, browserCrypto);
expect(tokenRequest.authority).to.be.eq(alternateAuthority);
});
});
});
});
35 changes: 35 additions & 0 deletions lib/msal-common/src/cache/entities/ThrottlingEntity.ts
@@ -0,0 +1,35 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ThrottlingConstants } from "../../utils/Constants";

export class ThrottlingEntity {
// Unix-time value representing the expiration of the throttle
throttleTime: number;
// Information provided by the server
error?: string;
errorCodes?: Array<string>;
errorMessage?: string;
subError?: string;

/**
* validates if a given cache entry is "Throttling", parses <key,value>
* @param key
* @param entity
*/
static isThrottlingEntity(key: string, entity?: object): boolean {

let validateKey: boolean = false;
if (key) {
validateKey = key.indexOf(ThrottlingConstants.THROTTLING_PREFIX) === 0;
}

let validateEntity: boolean = true;
if (entity) {
validateEntity = entity.hasOwnProperty("throttleTime");
}

return validateKey && validateEntity;
}
}
3 changes: 2 additions & 1 deletion lib/msal-common/src/cache/utils/CacheTypes.ts
Expand Up @@ -9,6 +9,7 @@ import { AccessTokenEntity } from "../entities/AccessTokenEntity";
import { RefreshTokenEntity } from "../entities/RefreshTokenEntity";
import { AppMetadataEntity } from "../entities/AppMetadataEntity";
import { ServerTelemetryEntity } from "../entities/ServerTelemetryEntity";
import { ThrottlingEntity } from "../entities/ThrottlingEntity";

export type AccountCache = Record<string, AccountEntity>;
export type IdTokenCache = Record<string, IdTokenEntity>;
Expand All @@ -21,7 +22,7 @@ export type CredentialCache = {
refreshTokens: RefreshTokenCache;
};

export type ValidCacheType = AccountEntity | IdTokenEntity | AccessTokenEntity | RefreshTokenEntity | AppMetadataEntity | ServerTelemetryEntity | string;
export type ValidCacheType = AccountEntity | IdTokenEntity | AccessTokenEntity | RefreshTokenEntity | AppMetadataEntity | ServerTelemetryEntity | ThrottlingEntity | string;

/**
* Account: <home_account_id>-<environment>-<realm*>
Expand Down
9 changes: 8 additions & 1 deletion lib/msal-common/src/client/AuthorizationCodeClient.ts
Expand Up @@ -22,6 +22,7 @@ import { ServerAuthorizationCodeResponse } from "../response/ServerAuthorization
import { AccountEntity } from "../cache/entities/AccountEntity";
import { EndSessionRequest } from "../request/EndSessionRequest";
import { ClientConfigurationError } from "../error/ClientConfigurationError";
import { RequestThumbprint } from "../network/RequestThumbprint";

/**
* Oauth2.0 Authorization Code client
Expand Down Expand Up @@ -130,10 +131,16 @@ export class AuthorizationCodeClient extends BaseClient {
* @param request
*/
private async executeTokenRequest(authority: Authority, request: AuthorizationCodeRequest): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
const thumbprint: RequestThumbprint = {
clientId: this.config.authOptions.clientId,
authority: authority.canonicalAuthority,
scopes: request.scopes
};

const requestBody = this.createTokenRequestBody(request);
const headers: Record<string, string> = this.createDefaultTokenRequestHeaders();

return this.executePostToTokenEndpoint(authority.tokenEndpoint, requestBody, headers);
return this.executePostToTokenEndpoint(authority.tokenEndpoint, requestBody, headers, thumbprint);
}

/**
Expand Down
24 changes: 16 additions & 8 deletions lib/msal-common/src/client/BaseClient.ts
Expand Up @@ -5,15 +5,16 @@

import { ClientConfiguration, buildClientConfiguration } from "../config/ClientConfiguration";
import { INetworkModule } from "../network/INetworkModule";
import { NetworkManager, NetworkResponse } from "../network/NetworkManager";
import { ICrypto } from "../crypto/ICrypto";
import { Authority } from "../authority/Authority";
import { Logger } from "../logger/Logger";
import { AADServerParamKeys, Constants, HeaderNames } from "../utils/Constants";
import { NetworkResponse } from "../network/NetworkManager";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse";
import { TrustedAuthority } from "../authority/TrustedAuthority";
import { CacheManager } from "../cache/CacheManager";
import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager";
import { RequestThumbprint } from "../network/RequestThumbprint";

/**
* Base application class which will construct requests to send to and handle responses from the Microsoft STS using the authorization code flow.
Expand All @@ -37,6 +38,9 @@ export abstract class BaseClient {
// Server Telemetry Manager
protected serverTelemetryManager: ServerTelemetryManager;

// Network Manager
protected networkManager: NetworkManager;

// Default authority object
protected authority: Authority;

Expand All @@ -56,6 +60,9 @@ export abstract class BaseClient {
// Set the network interface
this.networkClient = this.config.networkInterface;

// Set the NetworkManager
this.networkManager = new NetworkManager(this.networkClient, this.cacheManager);

// Set TelemetryManager
this.serverTelemetryManager = this.config.serverTelemetryManager;

Expand All @@ -70,6 +77,7 @@ export abstract class BaseClient {
protected createDefaultTokenRequestHeaders(): Record<string, string> {
const headers = this.createDefaultLibraryHeaders();
headers[HeaderNames.CONTENT_TYPE] = Constants.URL_FORM_CONTENT_TYPE;
headers[HeaderNames.X_MS_LIB_CAPABILITY] = HeaderNames.X_MS_LIB_CAPABILITY_VALUE;

if (this.serverTelemetryManager) {
headers[HeaderNames.X_CLIENT_CURR_TELEM] = this.serverTelemetryManager.generateCurrentRequestHeaderValue();
Expand Down Expand Up @@ -99,14 +107,14 @@ export abstract class BaseClient {
* @param tokenEndpoint
* @param queryString
* @param headers
* @param thumbprint
*/
protected async executePostToTokenEndpoint(tokenEndpoint: string, queryString: string, headers: Record<string, string>): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
const response = await this.networkClient.sendPostRequestAsync<
ServerAuthorizationTokenResponse
>(tokenEndpoint, {
body: queryString,
headers: headers,
});
protected async executePostToTokenEndpoint(tokenEndpoint: string, queryString: string, headers: Record<string, string>, thumbprint: RequestThumbprint): Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {
const response = await this.networkManager.sendPostRequest<ServerAuthorizationTokenResponse>(
thumbprint,
tokenEndpoint,
{ body: queryString, headers: headers }
);

if (this.config.serverTelemetryManager && response.status < 500 && response.status !== 429) {
// Telemetry data successfully logged by server, clear Telemetry cache
Expand Down
8 changes: 7 additions & 1 deletion lib/msal-common/src/client/ClientCredentialClient.ts
Expand Up @@ -17,6 +17,7 @@ import { CredentialType } from "../utils/Constants";
import { AccessTokenEntity } from "../cache/entities/AccessTokenEntity";
import { TimeUtils } from "../utils/TimeUtils";
import { StringUtils } from "../utils/StringUtils";
import { RequestThumbprint } from "../network/RequestThumbprint";

/**
* OAuth2.0 client credential grant
Expand Down Expand Up @@ -81,8 +82,13 @@ export class ClientCredentialClient extends BaseClient {

const requestBody = this.createTokenRequestBody(request);
const headers: Record<string, string> = this.createDefaultTokenRequestHeaders();
const thumbprint: RequestThumbprint = {
clientId: this.config.authOptions.clientId,
authority: request.authority,
scopes: request.scopes
};

const response = await this.executePostToTokenEndpoint(authority.tokenEndpoint, requestBody, headers);
const response = await this.executePostToTokenEndpoint(authority.tokenEndpoint, requestBody, headers, thumbprint);

const responseHandler = new ResponseHandler(
this.config.authOptions.clientId,
Expand Down
23 changes: 18 additions & 5 deletions lib/msal-common/src/client/DeviceCodeClient.ts
Expand Up @@ -16,6 +16,7 @@ import { ScopeSet } from "../request/ScopeSet";
import { ResponseHandler } from "../response/ResponseHandler";
import { AuthenticationResult } from "../response/AuthenticationResult";
import { StringUtils } from "../utils/StringUtils";
import { RequestThumbprint } from "../network/RequestThumbprint";

/**
* OAuth2.0 Device code client
Expand Down Expand Up @@ -61,11 +62,15 @@ export class DeviceCodeClient extends BaseClient {
* @param request
*/
private async getDeviceCode(request: DeviceCodeRequest): Promise<DeviceCodeResponse> {

const queryString = this.createQueryString(request);
const headers = this.createDefaultLibraryHeaders();
const thumbprint: RequestThumbprint = {
clientId: this.config.authOptions.clientId,
authority: request.authority,
scopes: request.scopes
};

return this.executePostRequestToDeviceCodeEndpoint(this.authority.deviceCodeEndpoint, queryString, headers);
return this.executePostRequestToDeviceCodeEndpoint(this.authority.deviceCodeEndpoint, queryString, headers, thumbprint);
}

/**
Expand All @@ -77,7 +82,8 @@ export class DeviceCodeClient extends BaseClient {
private async executePostRequestToDeviceCodeEndpoint(
deviceCodeEndpoint: string,
queryString: string,
headers: Record<string, string>): Promise<DeviceCodeResponse> {
headers: Record<string, string>,
thumbprint: RequestThumbprint): Promise<DeviceCodeResponse> {

const {
body: {
Expand All @@ -88,7 +94,8 @@ export class DeviceCodeClient extends BaseClient {
interval,
message
}
} = await this.networkClient.sendPostRequestAsync<ServerDeviceCodeResponse>(
} = await this.networkManager.sendPostRequest<ServerDeviceCodeResponse>(
thumbprint,
deviceCodeEndpoint,
{
body: queryString,
Expand Down Expand Up @@ -157,10 +164,16 @@ export class DeviceCodeClient extends BaseClient {
reject(ClientAuthError.createDeviceCodeExpiredError());

} else {
const thumbprint: RequestThumbprint = {
clientId: this.config.authOptions.clientId,
authority: request.authority,
scopes: request.scopes
};
const response = await this.executePostToTokenEndpoint(
this.authority.tokenEndpoint,
requestBody,
headers);
headers,
thumbprint);

if (response.body && response.body.error == Constants.AUTHORIZATION_PENDING) {
// user authorization is pending. Sleep for polling interval and try again
Expand Down
12 changes: 9 additions & 3 deletions lib/msal-common/src/client/RefreshTokenClient.ts
Expand Up @@ -6,14 +6,16 @@
import { ClientConfiguration } from "../config/ClientConfiguration";
import { BaseClient } from "./BaseClient";
import { RefreshTokenRequest } from "../request/RefreshTokenRequest";
import { Authority, NetworkResponse } from "..";
import { Authority } from "..";
import { ServerAuthorizationTokenResponse } from "../response/ServerAuthorizationTokenResponse";
import { RequestParameterBuilder } from "../request/RequestParameterBuilder";
import { ScopeSet } from "../request/ScopeSet";
import { GrantType } from "../utils/Constants";
import { ResponseHandler } from "../response/ResponseHandler";
import { AuthenticationResult } from "../response/AuthenticationResult";
import { StringUtils } from "../utils/StringUtils";
import { RequestThumbprint } from "../network/RequestThumbprint";
import { NetworkResponse } from "../network/NetworkManager";

/**
* OAuth2.0 refresh token client
Expand Down Expand Up @@ -45,11 +47,15 @@ export class RefreshTokenClient extends BaseClient {

private async executeTokenRequest(request: RefreshTokenRequest, authority: Authority)
: Promise<NetworkResponse<ServerAuthorizationTokenResponse>> {

const requestBody = this.createTokenRequestBody(request);
const headers: Record<string, string> = this.createDefaultTokenRequestHeaders();
const thumbprint: RequestThumbprint = {
clientId: this.config.authOptions.clientId,
authority: authority.canonicalAuthority,
scopes: request.scopes
};

return this.executePostToTokenEndpoint(authority.tokenEndpoint, requestBody, headers);
return this.executePostToTokenEndpoint(authority.tokenEndpoint, requestBody, headers, thumbprint);
}

private createTokenRequestBody(request: RefreshTokenRequest): string {
Expand Down

0 comments on commit 9a0138c

Please sign in to comment.