@azure/msal-browser
Version:
Microsoft Authentication Library for js
501 lines (466 loc) • 14.7 kB
text/typescript
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthenticationScheme,
Authority,
AuthorizeProtocol,
ClientConfigurationErrorCodes,
CommonAuthorizationUrlRequest,
createClientConfigurationError,
invokeAsync,
IPerformanceClient,
Logger,
PerformanceEvents,
PopTokenGenerator,
ProtocolMode,
RequestParameterBuilder,
OAuthResponseType,
Constants,
CommonAuthorizationCodeRequest,
AuthorizationCodeClient,
ProtocolUtils,
ThrottlingUtils,
AuthorizeResponse,
ResponseHandler,
TimeUtils,
AuthorizationCodePayload,
ServerAuthorizationTokenResponse,
} from "@azure/msal-common/browser";
import { BrowserConfiguration } from "../config/Configuration.js";
import { ApiId, BrowserConstants } from "../utils/BrowserConstants.js";
import { version } from "../packageMetadata.js";
import { CryptoOps } from "../crypto/CryptoOps.js";
import {
BrowserAuthErrorCodes,
createBrowserAuthError,
} from "../error/BrowserAuthError.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { InteractionHandler } from "../interaction_handler/InteractionHandler.js";
import { BrowserCacheManager } from "../cache/BrowserCacheManager.js";
import { NativeInteractionClient } from "../interaction_client/NativeInteractionClient.js";
import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js";
import { EventHandler } from "../event/EventHandler.js";
import { decryptEarResponse } from "../crypto/BrowserCrypto.js";
/**
* Returns map of parameters that are applicable to all calls to /authorize whether using PKCE or EAR
* @param config
* @param authority
* @param request
* @param logger
* @param performanceClient
* @returns
*/
async function getStandardParameters(
config: BrowserConfiguration,
authority: Authority,
request: CommonAuthorizationUrlRequest,
logger: Logger,
performanceClient: IPerformanceClient
): Promise<Map<string, string>> {
const parameters = AuthorizeProtocol.getStandardAuthorizeRequestParameters(
{ ...config.auth, authority: authority },
request,
logger,
performanceClient
);
RequestParameterBuilder.addLibraryInfo(parameters, {
sku: BrowserConstants.MSAL_SKU,
version: version,
os: "",
cpu: "",
});
if (config.auth.protocolMode !== ProtocolMode.OIDC) {
RequestParameterBuilder.addApplicationTelemetry(
parameters,
config.telemetry.application
);
}
if (request.platformBroker) {
// signal ests that this is a WAM call
RequestParameterBuilder.addNativeBroker(parameters);
// pass the req_cnf for POP
if (request.authenticationScheme === AuthenticationScheme.POP) {
const cryptoOps = new CryptoOps(logger, performanceClient);
const popTokenGenerator = new PopTokenGenerator(cryptoOps);
// req_cnf is always sent as a string for SPAs
let reqCnfData;
if (!request.popKid) {
const generatedReqCnfData = await invokeAsync(
popTokenGenerator.generateCnf.bind(popTokenGenerator),
PerformanceEvents.PopTokenGenerateCnf,
logger,
performanceClient,
request.correlationId
)(request, logger);
reqCnfData = generatedReqCnfData.reqCnfString;
} else {
reqCnfData = cryptoOps.encodeKid(request.popKid);
}
RequestParameterBuilder.addPopToken(parameters, reqCnfData);
}
}
RequestParameterBuilder.instrumentBrokerParams(
parameters,
request.correlationId,
performanceClient
);
return parameters;
}
/**
* Gets the full /authorize URL with request parameters when using Auth Code + PKCE
* @param config
* @param authority
* @param request
* @param logger
* @param performanceClient
* @returns
*/
export async function getAuthCodeRequestUrl(
config: BrowserConfiguration,
authority: Authority,
request: CommonAuthorizationUrlRequest,
logger: Logger,
performanceClient: IPerformanceClient
): Promise<string> {
if (!request.codeChallenge) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.pkceParamsMissing
);
}
const parameters = await invokeAsync(
getStandardParameters,
PerformanceEvents.GetStandardParams,
logger,
performanceClient,
request.correlationId
)(config, authority, request, logger, performanceClient);
RequestParameterBuilder.addResponseType(parameters, OAuthResponseType.CODE);
RequestParameterBuilder.addCodeChallengeParams(
parameters,
request.codeChallenge,
Constants.S256_CODE_CHALLENGE_METHOD
);
RequestParameterBuilder.addExtraQueryParameters(
parameters,
request.extraQueryParameters || {}
);
return AuthorizeProtocol.getAuthorizeUrl(
authority,
parameters,
config.auth.encodeExtraQueryParams,
request.extraQueryParameters
);
}
/**
* Gets the form that will be posted to /authorize with request parameters when using EAR
*/
export async function getEARForm(
frame: Document,
config: BrowserConfiguration,
authority: Authority,
request: CommonAuthorizationUrlRequest,
logger: Logger,
performanceClient: IPerformanceClient
): Promise<HTMLFormElement> {
if (!request.earJwk) {
throw createBrowserAuthError(BrowserAuthErrorCodes.earJwkEmpty);
}
const parameters = await getStandardParameters(
config,
authority,
request,
logger,
performanceClient
);
RequestParameterBuilder.addResponseType(
parameters,
OAuthResponseType.IDTOKEN_TOKEN_REFRESHTOKEN
);
RequestParameterBuilder.addEARParameters(parameters, request.earJwk);
const queryParams = new Map<string, string>();
RequestParameterBuilder.addExtraQueryParameters(
queryParams,
request.extraQueryParameters || {}
);
const url = AuthorizeProtocol.getAuthorizeUrl(
authority,
queryParams,
config.auth.encodeExtraQueryParams,
request.extraQueryParameters
);
return createForm(frame, url, parameters);
}
/**
* Creates form element in the provided document with auth parameters in the post body
* @param frame
* @param authorizeUrl
* @param parameters
* @returns
*/
function createForm(
frame: Document,
authorizeUrl: string,
parameters: Map<string, string>
): HTMLFormElement {
const form = frame.createElement("form");
form.method = "post";
form.action = authorizeUrl;
parameters.forEach((value: string, key: string) => {
const param = frame.createElement("input");
param.hidden = true;
param.name = key;
param.value = value;
form.appendChild(param);
});
frame.body.appendChild(form);
return form;
}
/**
* Response handler when server returns accountId on the /authorize request
* @param request
* @param accountId
* @param apiId
* @param config
* @param browserStorage
* @param nativeStorage
* @param eventHandler
* @param logger
* @param performanceClient
* @param nativeMessageHandler
* @returns
*/
export async function handleResponsePlatformBroker(
request: CommonAuthorizationUrlRequest,
accountId: string,
apiId: ApiId,
config: BrowserConfiguration,
browserStorage: BrowserCacheManager,
nativeStorage: BrowserCacheManager,
eventHandler: EventHandler,
logger: Logger,
performanceClient: IPerformanceClient,
nativeMessageHandler?: NativeMessageHandler
): Promise<AuthenticationResult> {
if (!nativeMessageHandler) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.nativeConnectionNotEstablished
);
}
const browserCrypto = new CryptoOps(logger, performanceClient);
const nativeInteractionClient = new NativeInteractionClient(
config,
browserStorage,
browserCrypto,
logger,
eventHandler,
config.system.navigationClient,
apiId,
performanceClient,
nativeMessageHandler,
accountId,
nativeStorage,
request.correlationId
);
const { userRequestState } = ProtocolUtils.parseRequestState(
browserCrypto,
request.state
);
return invokeAsync(
nativeInteractionClient.acquireToken.bind(nativeInteractionClient),
PerformanceEvents.NativeInteractionClientAcquireToken,
logger,
performanceClient,
request.correlationId
)({
...request,
state: userRequestState,
prompt: undefined, // Server should handle the prompt, ideally native broker can do this part silently
});
}
/**
* Response handler when server returns code on the /authorize request
* @param request
* @param response
* @param codeVerifier
* @param authClient
* @param browserStorage
* @param logger
* @param performanceClient
* @returns
*/
export async function handleResponseCode(
request: CommonAuthorizationUrlRequest,
response: AuthorizeResponse,
codeVerifier: string,
apiId: ApiId,
config: BrowserConfiguration,
authClient: AuthorizationCodeClient,
browserStorage: BrowserCacheManager,
nativeStorage: BrowserCacheManager,
eventHandler: EventHandler,
logger: Logger,
performanceClient: IPerformanceClient,
nativeMessageHandler?: NativeMessageHandler
): Promise<AuthenticationResult> {
// Remove throttle if it exists
ThrottlingUtils.removeThrottle(
browserStorage,
config.auth.clientId,
request
);
if (response.accountId) {
return invokeAsync(
handleResponsePlatformBroker,
PerformanceEvents.HandleResponsePlatformBroker,
logger,
performanceClient,
request.correlationId
)(
request,
response.accountId,
apiId,
config,
browserStorage,
nativeStorage,
eventHandler,
logger,
performanceClient,
nativeMessageHandler
);
}
const authCodeRequest: CommonAuthorizationCodeRequest = {
...request,
code: response.code || "",
codeVerifier: codeVerifier,
};
// Create popup interaction handler.
const interactionHandler = new InteractionHandler(
authClient,
browserStorage,
authCodeRequest,
logger,
performanceClient
);
// Handle response from hash string.
const result = await invokeAsync(
interactionHandler.handleCodeResponse.bind(interactionHandler),
PerformanceEvents.HandleCodeResponse,
logger,
performanceClient,
request.correlationId
)(response, request);
return result;
}
/**
* Response handler when server returns ear_jwe on the /authorize request
* @param request
* @param response
* @param apiId
* @param config
* @param authority
* @param browserStorage
* @param nativeStorage
* @param eventHandler
* @param logger
* @param performanceClient
* @param nativeMessageHandler
* @returns
*/
export async function handleResponseEAR(
request: CommonAuthorizationUrlRequest,
response: AuthorizeResponse,
apiId: ApiId,
config: BrowserConfiguration,
authority: Authority,
browserStorage: BrowserCacheManager,
nativeStorage: BrowserCacheManager,
eventHandler: EventHandler,
logger: Logger,
performanceClient: IPerformanceClient,
nativeMessageHandler?: NativeMessageHandler
): Promise<AuthenticationResult> {
// Remove throttle if it exists
ThrottlingUtils.removeThrottle(
browserStorage,
config.auth.clientId,
request
);
// Validate state & check response for errors
AuthorizeProtocol.validateAuthorizationResponse(response, request.state);
if (!response.ear_jwe) {
throw createBrowserAuthError(BrowserAuthErrorCodes.earJweEmpty);
}
if (!request.earJwk) {
throw createBrowserAuthError(BrowserAuthErrorCodes.earJwkEmpty);
}
const decryptedData = JSON.parse(
await invokeAsync(
decryptEarResponse,
PerformanceEvents.DecryptEarResponse,
logger,
performanceClient,
request.correlationId
)(request.earJwk, response.ear_jwe)
) as AuthorizeResponse & ServerAuthorizationTokenResponse;
if (decryptedData.accountId) {
return invokeAsync(
handleResponsePlatformBroker,
PerformanceEvents.HandleResponsePlatformBroker,
logger,
performanceClient,
request.correlationId
)(
request,
decryptedData.accountId,
apiId,
config,
browserStorage,
nativeStorage,
eventHandler,
logger,
performanceClient,
nativeMessageHandler
);
}
const responseHandler = new ResponseHandler(
config.auth.clientId,
browserStorage,
new CryptoOps(logger, performanceClient),
logger,
null,
null,
performanceClient
);
// Validate response. This function throws a server error if an error is returned by the server.
responseHandler.validateTokenResponse(decryptedData);
// Temporary until response handler is refactored to be more flow agnostic.
const additionalData: AuthorizationCodePayload = {
code: "",
state: request.state,
nonce: request.nonce,
client_info: decryptedData.client_info,
cloud_graph_host_name: decryptedData.cloud_graph_host_name,
cloud_instance_host_name: decryptedData.cloud_instance_host_name,
cloud_instance_name: decryptedData.cloud_instance_name,
msgraph_host: decryptedData.msgraph_host,
};
return (await invokeAsync(
responseHandler.handleServerTokenResponse.bind(responseHandler),
PerformanceEvents.HandleServerTokenResponse,
logger,
performanceClient,
request.correlationId
)(
decryptedData,
authority,
TimeUtils.nowSeconds(),
request,
additionalData,
undefined,
undefined,
undefined,
undefined
)) as AuthenticationResult;
}