Initial commit

This commit is contained in:
2025-03-07 19:22:02 +01:00
commit 4a98255d83
55743 changed files with 5280367 additions and 0 deletions
+247
View File
@@ -0,0 +1,247 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AccountFilter,
AccountInfo,
Logger,
PerformanceCallbackFunction,
} from "@azure/msal-common/browser";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { SilentRequest } from "../request/SilentRequest.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import { EndSessionRequest } from "../request/EndSessionRequest.js";
import {
BrowserConfigurationAuthErrorCodes,
createBrowserConfigurationAuthError,
} from "../error/BrowserConfigurationAuthError.js";
import { WrapperSKU } from "../utils/BrowserConstants.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import { EndSessionPopupRequest } from "../request/EndSessionPopupRequest.js";
import { ITokenCache } from "../cache/ITokenCache.js";
import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js";
import { BrowserConfiguration } from "../config/Configuration.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { EventCallbackFunction } from "../event/EventMessage.js";
import { ClearCacheRequest } from "../request/ClearCacheRequest.js";
import { InitializeApplicationRequest } from "../request/InitializeApplicationRequest.js";
import { EventType } from "../event/EventType.js";
export interface IPublicClientApplication {
// TODO: Make request mandatory in the next major version?
initialize(request?: InitializeApplicationRequest): Promise<void>;
acquireTokenPopup(request: PopupRequest): Promise<AuthenticationResult>;
acquireTokenRedirect(request: RedirectRequest): Promise<void>;
acquireTokenSilent(
silentRequest: SilentRequest
): Promise<AuthenticationResult>;
acquireTokenByCode(
request: AuthorizationCodeRequest
): Promise<AuthenticationResult>;
addEventCallback(
callback: EventCallbackFunction,
eventTypes?: Array<EventType>
): string | null;
removeEventCallback(callbackId: string): void;
addPerformanceCallback(callback: PerformanceCallbackFunction): string;
removePerformanceCallback(callbackId: string): boolean;
enableAccountStorageEvents(): void;
disableAccountStorageEvents(): void;
getAccount(accountFilter: AccountFilter): AccountInfo | null;
getAccountByHomeId(homeAccountId: string): AccountInfo | null;
getAccountByLocalId(localId: string): AccountInfo | null;
getAccountByUsername(userName: string): AccountInfo | null;
getAllAccounts(): AccountInfo[];
handleRedirectPromise(hash?: string): Promise<AuthenticationResult | null>;
loginPopup(request?: PopupRequest): Promise<AuthenticationResult>;
loginRedirect(request?: RedirectRequest): Promise<void>;
logout(logoutRequest?: EndSessionRequest): Promise<void>;
logoutRedirect(logoutRequest?: EndSessionRequest): Promise<void>;
logoutPopup(logoutRequest?: EndSessionPopupRequest): Promise<void>;
ssoSilent(request: SsoSilentRequest): Promise<AuthenticationResult>;
getTokenCache(): ITokenCache;
getLogger(): Logger;
setLogger(logger: Logger): void;
setActiveAccount(account: AccountInfo | null): void;
getActiveAccount(): AccountInfo | null;
initializeWrapperLibrary(sku: WrapperSKU, version: string): void;
setNavigationClient(navigationClient: INavigationClient): void;
/** @internal */
getConfiguration(): BrowserConfiguration;
hydrateCache(
result: AuthenticationResult,
request:
| SilentRequest
| SsoSilentRequest
| RedirectRequest
| PopupRequest
): Promise<void>;
clearCache(logoutRequest?: ClearCacheRequest): Promise<void>;
}
export const stubbedPublicClientApplication: IPublicClientApplication = {
initialize: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
acquireTokenPopup: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
acquireTokenRedirect: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
acquireTokenSilent: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
acquireTokenByCode: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
getAllAccounts: () => {
return [];
},
getAccount: () => {
return null;
},
getAccountByHomeId: () => {
return null;
},
getAccountByUsername: () => {
return null;
},
getAccountByLocalId: () => {
return null;
},
handleRedirectPromise: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
loginPopup: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
loginRedirect: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
logout: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
logoutRedirect: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
logoutPopup: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
ssoSilent: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
addEventCallback: () => {
return null;
},
removeEventCallback: () => {
return;
},
addPerformanceCallback: () => {
return "";
},
removePerformanceCallback: () => {
return false;
},
enableAccountStorageEvents: () => {
return;
},
disableAccountStorageEvents: () => {
return;
},
getTokenCache: () => {
throw createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
);
},
getLogger: () => {
throw createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
);
},
setLogger: () => {
return;
},
setActiveAccount: () => {
return;
},
getActiveAccount: () => {
return null;
},
initializeWrapperLibrary: () => {
return;
},
setNavigationClient: () => {
return;
},
getConfiguration: () => {
throw createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
);
},
hydrateCache: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
clearCache: () => {
return Promise.reject(
createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled
)
);
},
};
+473
View File
@@ -0,0 +1,473 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ITokenCache } from "../cache/ITokenCache.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { SilentRequest } from "../request/SilentRequest.js";
import { WrapperSKU } from "../utils/BrowserConstants.js";
import { IPublicClientApplication } from "./IPublicClientApplication.js";
import { IController } from "../controllers/IController.js";
import {
PerformanceCallbackFunction,
AccountInfo,
AccountFilter,
Logger,
} from "@azure/msal-common/browser";
import { EndSessionRequest } from "../request/EndSessionRequest.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import * as ControllerFactory from "../controllers/ControllerFactory.js";
import { StandardController } from "../controllers/StandardController.js";
import {
BrowserConfiguration,
Configuration,
} from "../config/Configuration.js";
import { StandardOperatingContext } from "../operatingcontext/StandardOperatingContext.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { EventCallbackFunction } from "../event/EventMessage.js";
import { ClearCacheRequest } from "../request/ClearCacheRequest.js";
import { EndSessionPopupRequest } from "../request/EndSessionPopupRequest.js";
import { NestedAppAuthController } from "../controllers/NestedAppAuthController.js";
import { NestedAppOperatingContext } from "../operatingcontext/NestedAppOperatingContext.js";
import { InitializeApplicationRequest } from "../request/InitializeApplicationRequest.js";
import { EventType } from "../event/EventType.js";
/**
* The PublicClientApplication class is the object exposed by the library to perform authentication and authorization functions in Single Page Applications
* to obtain JWT tokens as described in the OAuth 2.0 Authorization Code Flow with PKCE specification.
*/
export class PublicClientApplication implements IPublicClientApplication {
protected controller: IController;
/**
* Creates StandardController and passes it to the PublicClientApplication
*
* @param configuration {Configuration}
*/
public static async createPublicClientApplication(
configuration: Configuration
): Promise<IPublicClientApplication> {
const controller = await ControllerFactory.createV3Controller(
configuration
);
const pca = new PublicClientApplication(configuration, controller);
return pca;
}
/**
* @constructor
* Constructor for the PublicClientApplication used to instantiate the PublicClientApplication object
*
* Important attributes in the Configuration object for auth are:
* - clientID: the application ID of your application. You can obtain one by registering your application with our Application registration portal : https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredAppsPreview
* - authority: the authority URL for your application.
* - redirect_uri: the uri of your application registered in the portal.
*
* In Azure AD, authority is a URL indicating the Azure active directory that MSAL uses to obtain tokens.
* It is of the form https://login.microsoftonline.com/{Enter_the_Tenant_Info_Here}
* If your application supports Accounts in one organizational directory, replace "Enter_the_Tenant_Info_Here" value with the Tenant Id or Tenant name (for example, contoso.microsoft.com).
* If your application supports Accounts in any organizational directory, replace "Enter_the_Tenant_Info_Here" value with organizations.
* If your application supports Accounts in any organizational directory and personal Microsoft accounts, replace "Enter_the_Tenant_Info_Here" value with common.
* To restrict support to Personal Microsoft accounts only, replace "Enter_the_Tenant_Info_Here" value with consumers.
*
* In Azure B2C, authority is of the form https://{instance}/tfp/{tenant}/{policyName}/
* Full B2C functionality will be available in this library in future versions.
*
* @param configuration Object for the MSAL PublicClientApplication instance
* @param IController Optional parameter to explictly set the controller. (Will be removed when we remove public constructor)
*/
public constructor(configuration: Configuration, controller?: IController) {
this.controller =
controller ||
new StandardController(new StandardOperatingContext(configuration));
}
/**
* Initializer function to perform async startup tasks such as connecting to WAM extension
* @param request {?InitializeApplicationRequest}
*/
async initialize(request?: InitializeApplicationRequest): Promise<void> {
return this.controller.initialize(request);
}
/**
* Use when you want to obtain an access_token for your API via opening a popup window in the user's browser
*
* @param request
*
* @returns A promise that is fulfilled when this function has completed, or rejected if an error was raised.
*/
async acquireTokenPopup(
request: PopupRequest
): Promise<AuthenticationResult> {
return this.controller.acquireTokenPopup(request);
}
/**
* Use when you want to obtain an access_token for your API by redirecting the user's browser window to the authorization endpoint. This function redirects
* the page, so any code that follows this function will not execute.
*
* IMPORTANT: It is NOT recommended to have code that is dependent on the resolution of the Promise. This function will navigate away from the current
* browser window. It currently returns a Promise in order to reflect the asynchronous nature of the code running in this function.
*
* @param request
*/
acquireTokenRedirect(request: RedirectRequest): Promise<void> {
return this.controller.acquireTokenRedirect(request);
}
/**
* Silently acquire an access token for a given set of scopes. Returns currently processing promise if parallel requests are made.
*
* @param {@link (SilentRequest:type)}
* @returns {Promise.<AuthenticationResult>} - a promise that is fulfilled when this function has completed, or rejected if an error was raised. Returns the {@link AuthenticationResult} object
*/
acquireTokenSilent(
silentRequest: SilentRequest
): Promise<AuthenticationResult> {
return this.controller.acquireTokenSilent(silentRequest);
}
/**
* This function redeems an authorization code (passed as code) from the eSTS token endpoint.
* This authorization code should be acquired server-side using a confidential client to acquire a spa_code.
* This API is not indended for normal authorization code acquisition and redemption.
*
* Redemption of this authorization code will not require PKCE, as it was acquired by a confidential client.
*
* @param request {@link AuthorizationCodeRequest}
* @returns A promise that is fulfilled when this function has completed, or rejected if an error was raised.
*/
acquireTokenByCode(
request: AuthorizationCodeRequest
): Promise<AuthenticationResult> {
return this.controller.acquireTokenByCode(request);
}
/**
* Adds event callbacks to array
* @param callback
* @param eventTypes
*/
addEventCallback(
callback: EventCallbackFunction,
eventTypes?: Array<EventType>
): string | null {
return this.controller.addEventCallback(callback, eventTypes);
}
/**
* Removes callback with provided id from callback array
* @param callbackId
*/
removeEventCallback(callbackId: string): void {
return this.controller.removeEventCallback(callbackId);
}
/**
* Registers a callback to receive performance events.
*
* @param {PerformanceCallbackFunction} callback
* @returns {string}
*/
addPerformanceCallback(callback: PerformanceCallbackFunction): string {
return this.controller.addPerformanceCallback(callback);
}
/**
* Removes a callback registered with addPerformanceCallback.
*
* @param {string} callbackId
* @returns {boolean}
*/
removePerformanceCallback(callbackId: string): boolean {
return this.controller.removePerformanceCallback(callbackId);
}
/**
* Adds event listener that emits an event when a user account is added or removed from localstorage in a different browser tab or window
*/
enableAccountStorageEvents(): void {
this.controller.enableAccountStorageEvents();
}
/**
* Removes event listener that emits an event when a user account is added or removed from localstorage in a different browser tab or window
*/
disableAccountStorageEvents(): void {
this.controller.disableAccountStorageEvents();
}
/**
* Returns the first account found in the cache that matches the account filter passed in.
* @param accountFilter
* @returns The first account found in the cache matching the provided filter or null if no account could be found.
*/
getAccount(accountFilter: AccountFilter): AccountInfo | null {
return this.controller.getAccount(accountFilter);
}
/**
* Returns the signed in account matching homeAccountId.
* (the account object is created at the time of successful login)
* or null when no matching account is found
* @param homeAccountId
* @returns The account object stored in MSAL
* @deprecated - Use getAccount instead
*/
getAccountByHomeId(homeAccountId: string): AccountInfo | null {
return this.controller.getAccountByHomeId(homeAccountId);
}
/**
* Returns the signed in account matching localAccountId.
* (the account object is created at the time of successful login)
* or null when no matching account is found
* @param localAccountId
* @returns The account object stored in MSAL
* @deprecated - Use getAccount instead
*/
getAccountByLocalId(localId: string): AccountInfo | null {
return this.controller.getAccountByLocalId(localId);
}
/**
* Returns the signed in account matching username.
* (the account object is created at the time of successful login)
* or null when no matching account is found.
* This API is provided for convenience but getAccountById should be used for best reliability
* @param userName
* @returns The account object stored in MSAL
* @deprecated - Use getAccount instead
*/
getAccountByUsername(userName: string): AccountInfo | null {
return this.controller.getAccountByUsername(userName);
}
/**
* Returns all the accounts in the cache that match the optional filter. If no filter is provided, all accounts are returned.
* @param accountFilter - (Optional) filter to narrow down the accounts returned
* @returns Array of AccountInfo objects in cache
*/
getAllAccounts(accountFilter?: AccountFilter): AccountInfo[] {
return this.controller.getAllAccounts(accountFilter);
}
/**
* Event handler function which allows users to fire events after the PublicClientApplication object
* has loaded during redirect flows. This should be invoked on all page loads involved in redirect
* auth flows.
* @param hash Hash to process. Defaults to the current value of window.location.hash. Only needs to be provided explicitly if the response to be handled is not contained in the current value.
* @returns Token response or null. If the return value is null, then no auth redirect was detected.
*/
handleRedirectPromise(
hash?: string | undefined
): Promise<AuthenticationResult | null> {
return this.controller.handleRedirectPromise(hash);
}
/**
* Use when initiating the login process via opening a popup window in the user's browser
*
* @param request
*
* @returns A promise that is fulfilled when this function has completed, or rejected if an error was raised.
*/
loginPopup(
request?: PopupRequest | undefined
): Promise<AuthenticationResult> {
return this.controller.loginPopup(request);
}
/**
* Use when initiating the login process by redirecting the user's browser to the authorization endpoint. This function redirects the page, so
* any code that follows this function will not execute.
*
* IMPORTANT: It is NOT recommended to have code that is dependent on the resolution of the Promise. This function will navigate away from the current
* browser window. It currently returns a Promise in order to reflect the asynchronous nature of the code running in this function.
*
* @param request
*/
loginRedirect(request?: RedirectRequest | undefined): Promise<void> {
return this.controller.loginRedirect(request);
}
/**
* Deprecated logout function. Use logoutRedirect or logoutPopup instead
* @param logoutRequest
* @deprecated
*/
logout(logoutRequest?: EndSessionRequest): Promise<void> {
return this.controller.logout(logoutRequest);
}
/**
* Use to log out the current user, and redirect the user to the postLogoutRedirectUri.
* Default behaviour is to redirect the user to `window.location.href`.
* @param logoutRequest
*/
logoutRedirect(logoutRequest?: EndSessionRequest): Promise<void> {
return this.controller.logoutRedirect(logoutRequest);
}
/**
* Clears local cache for the current user then opens a popup window prompting the user to sign-out of the server
* @param logoutRequest
*/
logoutPopup(logoutRequest?: EndSessionPopupRequest): Promise<void> {
return this.controller.logoutPopup(logoutRequest);
}
/**
* This function uses a hidden iframe to fetch an authorization code from the eSTS. There are cases where this may not work:
* - Any browser using a form of Intelligent Tracking Prevention
* - If there is not an established session with the service
*
* In these cases, the request must be done inside a popup or full frame redirect.
*
* For the cases where interaction is required, you cannot send a request with prompt=none.
*
* If your refresh token has expired, you can use this function to fetch a new set of tokens silently as long as
* you session on the server still exists.
* @param request {@link SsoSilentRequest}
*
* @returns A promise that is fulfilled when this function has completed, or rejected if an error was raised.
*/
ssoSilent(request: SsoSilentRequest): Promise<AuthenticationResult> {
return this.controller.ssoSilent(request);
}
/**
* Gets the token cache for the application.
*/
getTokenCache(): ITokenCache {
return this.controller.getTokenCache();
}
/**
* Returns the logger instance
*/
getLogger(): Logger {
return this.controller.getLogger();
}
/**
* Replaces the default logger set in configurations with new Logger with new configurations
* @param logger Logger instance
*/
setLogger(logger: Logger): void {
this.controller.setLogger(logger);
}
/**
* Sets the account to use as the active account. If no account is passed to the acquireToken APIs, then MSAL will use this active account.
* @param account
*/
setActiveAccount(account: AccountInfo | null): void {
this.controller.setActiveAccount(account);
}
/**
* Gets the currently active account
*/
getActiveAccount(): AccountInfo | null {
return this.controller.getActiveAccount();
}
/**
* Called by wrapper libraries (Angular & React) to set SKU and Version passed down to telemetry, logger, etc.
* @param sku
* @param version
*/
initializeWrapperLibrary(sku: WrapperSKU, version: string): void {
return this.controller.initializeWrapperLibrary(sku, version);
}
/**
* Sets navigation client
* @param navigationClient
*/
setNavigationClient(navigationClient: INavigationClient): void {
this.controller.setNavigationClient(navigationClient);
}
/**
* Returns the configuration object
* @internal
*/
getConfiguration(): BrowserConfiguration {
return this.controller.getConfiguration();
}
/**
* Hydrates cache with the tokens and account in the AuthenticationResult object
* @param result
* @param request - The request object that was used to obtain the AuthenticationResult
* @returns
*/
async hydrateCache(
result: AuthenticationResult,
request:
| SilentRequest
| SsoSilentRequest
| RedirectRequest
| PopupRequest
): Promise<void> {
return this.controller.hydrateCache(result, request);
}
/**
* Clears tokens and account from the browser cache.
* @param logoutRequest
*/
clearCache(logoutRequest?: ClearCacheRequest): Promise<void> {
return this.controller.clearCache(logoutRequest);
}
}
/**
* creates NestedAppAuthController and passes it to the PublicClientApplication,
* falls back to StandardController if NestedAppAuthController is not available
*
* @param configuration
* @returns IPublicClientApplication
*
*/
export async function createNestablePublicClientApplication(
configuration: Configuration
): Promise<IPublicClientApplication> {
const nestedAppAuth = new NestedAppOperatingContext(configuration);
await nestedAppAuth.initialize();
if (nestedAppAuth.isAvailable()) {
const controller = new NestedAppAuthController(nestedAppAuth);
const nestablePCA = new PublicClientApplication(
configuration,
controller
);
await nestablePCA.initialize();
return nestablePCA;
}
return createStandardPublicClientApplication(configuration);
}
/**
* creates PublicClientApplication using StandardController
*
* @param configuration
* @returns IPublicClientApplication
*
*/
export async function createStandardPublicClientApplication(
configuration: Configuration
): Promise<IPublicClientApplication> {
const pca = new PublicClientApplication(configuration);
await pca.initialize();
return pca;
}
+454
View File
@@ -0,0 +1,454 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ITokenCache } from "../cache/ITokenCache.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { SilentRequest } from "../request/SilentRequest.js";
import { WrapperSKU } from "../utils/BrowserConstants.js";
import { IPublicClientApplication } from "./IPublicClientApplication.js";
import { IController } from "../controllers/IController.js";
import {
PerformanceCallbackFunction,
AccountInfo,
AccountFilter,
Logger,
} from "@azure/msal-common/browser";
import { EndSessionRequest } from "../request/EndSessionRequest.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import * as ControllerFactory from "../controllers/ControllerFactory.js";
import {
BrowserConfiguration,
Configuration,
} from "../config/Configuration.js";
import { EventCallbackFunction } from "../event/EventMessage.js";
import { ClearCacheRequest } from "../request/ClearCacheRequest.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { UnknownOperatingContextController } from "../controllers/UnknownOperatingContextController.js";
import { UnknownOperatingContext } from "../operatingcontext/UnknownOperatingContext.js";
import { EventType } from "../event/EventType.js";
/**
* PublicClientNext is an early look at the planned implementation of PublicClientApplication in the next major version of MSAL.js.
* It contains support for multiple API implementations based on the runtime environment that it is running in.
*
* The goals of these changes are to provide a clean separation of behavior between different operating contexts (Nested App Auth, Platform Brokers, Plain old Browser, etc.)
* while still providing a consistent API surface for developers.
*
* Please use PublicClientApplication for any prod/real-world scenarios.
* Note: PublicClientNext is experimental and subject to breaking changes without following semver
*
*/
export class PublicClientNext implements IPublicClientApplication {
/*
* Definite assignment assertion used below
* https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-7.html#definite-assignment-assertions
*/
protected controller!: IController;
protected configuration: Configuration;
public static async createPublicClientApplication(
configuration: Configuration
): Promise<IPublicClientApplication> {
const controller = await ControllerFactory.createController(
configuration
);
let pca;
if (controller !== null) {
pca = new PublicClientNext(configuration, controller);
} else {
pca = new PublicClientNext(configuration);
}
return pca;
}
/**
* @constructor
* Constructor for the PublicClientNext used to instantiate the PublicClientNext object
*
* Important attributes in the Configuration object for auth are:
* - clientID: the application ID of your application. You can obtain one by registering your application with our Application registration portal : https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredAppsPreview
* - authority: the authority URL for your application.
* - redirect_uri: the uri of your application registered in the portal.
*
* In Azure AD, authority is a URL indicating the Azure active directory that MSAL uses to obtain tokens.
* It is of the form https://login.microsoftonline.com/{Enter_the_Tenant_Info_Here}
* If your application supports Accounts in one organizational directory, replace "Enter_the_Tenant_Info_Here" value with the Tenant Id or Tenant name (for example, contoso.microsoft.com).
* If your application supports Accounts in any organizational directory, replace "Enter_the_Tenant_Info_Here" value with organizations.
* If your application supports Accounts in any organizational directory and personal Microsoft accounts, replace "Enter_the_Tenant_Info_Here" value with common.
* To restrict support to Personal Microsoft accounts only, replace "Enter_the_Tenant_Info_Here" value with consumers.
*
* In Azure B2C, authority is of the form https://{instance}/tfp/{tenant}/{policyName}/
* Full B2C functionality will be available in this library in future versions.
*
* @param configuration Object for the MSAL PublicClientApplication instance
* @param IController Optional parameter to explictly set the controller. (Will be removed when we remove public constructor)
*/
private constructor(
configuration: Configuration,
controller?: IController
) {
this.configuration = configuration;
if (controller) {
this.controller = controller;
} else {
const operatingContext = new UnknownOperatingContext(configuration);
this.controller = new UnknownOperatingContextController(
operatingContext
);
}
}
/**
* Initializer function to perform async startup tasks such as connecting to WAM extension
*/
async initialize(): Promise<void> {
if (this.controller instanceof UnknownOperatingContextController) {
const result = await ControllerFactory.createController(
this.configuration
);
if (result !== null) {
this.controller = result;
}
return this.controller.initialize();
}
return Promise.resolve();
}
/**
* Use when you want to obtain an access_token for your API via opening a popup window in the user's browser
*
* @param request
*
* @returns A promise that is fulfilled when this function has completed, or rejected if an error was raised.
*/
async acquireTokenPopup(
request: PopupRequest
): Promise<AuthenticationResult> {
return this.controller.acquireTokenPopup(request);
}
/**
* Use when you want to obtain an access_token for your API by redirecting the user's browser window to the authorization endpoint. This function redirects
* the page, so any code that follows this function will not execute.
*
* IMPORTANT: It is NOT recommended to have code that is dependent on the resolution of the Promise. This function will navigate away from the current
* browser window. It currently returns a Promise in order to reflect the asynchronous nature of the code running in this function.
*
* @param request
*/
acquireTokenRedirect(request: RedirectRequest): Promise<void> {
return this.controller.acquireTokenRedirect(request);
}
/**
* Silently acquire an access token for a given set of scopes. Returns currently processing promise if parallel requests are made.
*
* @param {@link (SilentRequest:type)}
* @returns {Promise.<AuthenticationResult>} - a promise that is fulfilled when this function has completed, or rejected if an error was raised. Returns the {@link AuthenticationResult} object
*/
acquireTokenSilent(
silentRequest: SilentRequest
): Promise<AuthenticationResult> {
return this.controller.acquireTokenSilent(silentRequest);
}
/**
* This function redeems an authorization code (passed as code) from the eSTS token endpoint.
* This authorization code should be acquired server-side using a confidential client to acquire a spa_code.
* This API is not indended for normal authorization code acquisition and redemption.
*
* Redemption of this authorization code will not require PKCE, as it was acquired by a confidential client.
*
* @param request {@link AuthorizationCodeRequest}
* @returns A promise that is fulfilled when this function has completed, or rejected if an error was raised.
*/
acquireTokenByCode(
request: AuthorizationCodeRequest
): Promise<AuthenticationResult> {
return this.controller.acquireTokenByCode(request);
}
/**
* Adds event callbacks to array
* @param callback
*/
addEventCallback(
callback: EventCallbackFunction,
eventTypes?: Array<EventType>
): string | null {
return this.controller.addEventCallback(callback, eventTypes);
}
/**
* Removes callback with provided id from callback array
* @param callbackId
*/
removeEventCallback(callbackId: string): void {
return this.controller.removeEventCallback(callbackId);
}
/**
* Registers a callback to receive performance events.
*
* @param {PerformanceCallbackFunction} callback
* @returns {string}
*/
addPerformanceCallback(callback: PerformanceCallbackFunction): string {
return this.controller.addPerformanceCallback(callback);
}
/**
* Removes a callback registered with addPerformanceCallback.
*
* @param {string} callbackId
* @returns {boolean}
*/
removePerformanceCallback(callbackId: string): boolean {
return this.controller.removePerformanceCallback(callbackId);
}
/**
* Adds event listener that emits an event when a user account is added or removed from localstorage in a different browser tab or window
*/
enableAccountStorageEvents(): void {
this.controller.enableAccountStorageEvents();
}
/**
* Removes event listener that emits an event when a user account is added or removed from localstorage in a different browser tab or window
*/
disableAccountStorageEvents(): void {
this.controller.disableAccountStorageEvents();
}
/**
* Returns the first account found in the cache that matches the account filter passed in.
* @param accountFilter
* @returns The first account found in the cache matching the provided filter or null if no account could be found.
*/
getAccount(accountFilter: AccountFilter): AccountInfo | null {
return this.controller.getAccount(accountFilter);
}
/**
* Returns the signed in account matching homeAccountId.
* (the account object is created at the time of successful login)
* or null when no matching account is found
* @param homeAccountId
* @returns The account object stored in MSAL
* @deprecated - Use getAccount instead
*/
getAccountByHomeId(homeAccountId: string): AccountInfo | null {
return this.controller.getAccountByHomeId(homeAccountId);
}
/**
* Returns the signed in account matching localAccountId.
* (the account object is created at the time of successful login)
* or null when no matching account is found
* @param localAccountId
* @returns The account object stored in MSAL
* @deprecated - Use getAccount instead
*/
getAccountByLocalId(localId: string): AccountInfo | null {
return this.controller.getAccountByLocalId(localId);
}
/**
* Returns the signed in account matching username.
* (the account object is created at the time of successful login)
* or null when no matching account is found.
* This API is provided for convenience but getAccountById should be used for best reliability
* @param userName
* @returns The account object stored in MSAL
* @deprecated - Use getAccount instead
*/
getAccountByUsername(userName: string): AccountInfo | null {
return this.controller.getAccountByUsername(userName);
}
/**
* Returns all the accounts in the cache that match the optional filter. If no filter is provided, all accounts are returned.
* @param accountFilter - (Optional) filter to narrow down the accounts returned
* @returns Array of AccountInfo objects in cache
*/
getAllAccounts(accountFilter?: AccountFilter): AccountInfo[] {
return this.controller.getAllAccounts(accountFilter);
}
/**
* Event handler function which allows users to fire events after the PublicClientApplication object
* has loaded during redirect flows. This should be invoked on all page loads involved in redirect
* auth flows.
* @param hash Hash to process. Defaults to the current value of window.location.hash. Only needs to be provided explicitly if the response to be handled is not contained in the current value.
* @returns Token response or null. If the return value is null, then no auth redirect was detected.
*/
handleRedirectPromise(
hash?: string | undefined
): Promise<AuthenticationResult | null> {
return this.controller.handleRedirectPromise(hash);
}
/**
* Use when initiating the login process via opening a popup window in the user's browser
*
* @param request
*
* @returns A promise that is fulfilled when this function has completed, or rejected if an error was raised.
*/
loginPopup(
request?: PopupRequest | undefined
): Promise<AuthenticationResult> {
return this.controller.loginPopup(request);
}
/**
* Use when initiating the login process by redirecting the user's browser to the authorization endpoint. This function redirects the page, so
* any code that follows this function will not execute.
*
* IMPORTANT: It is NOT recommended to have code that is dependent on the resolution of the Promise. This function will navigate away from the current
* browser window. It currently returns a Promise in order to reflect the asynchronous nature of the code running in this function.
*
* @param request
*/
loginRedirect(request?: RedirectRequest | undefined): Promise<void> {
return this.controller.loginRedirect(request);
}
/**
* Deprecated logout function. Use logoutRedirect or logoutPopup instead
* @param logoutRequest
* @deprecated
*/
logout(logoutRequest?: EndSessionRequest): Promise<void> {
return this.controller.logout(logoutRequest);
}
/**
* Use to log out the current user, and redirect the user to the postLogoutRedirectUri.
* Default behaviour is to redirect the user to `window.location.href`.
* @param logoutRequest
*/
logoutRedirect(logoutRequest?: EndSessionRequest): Promise<void> {
return this.controller.logoutRedirect(logoutRequest);
}
/**
* Clears local cache for the current user then opens a popup window prompting the user to sign-out of the server
* @param logoutRequest
*/
logoutPopup(logoutRequest?: EndSessionRequest): Promise<void> {
return this.controller.logoutPopup(logoutRequest);
}
/**
* This function uses a hidden iframe to fetch an authorization code from the eSTS. There are cases where this may not work:
* - Any browser using a form of Intelligent Tracking Prevention
* - If there is not an established session with the service
*
* In these cases, the request must be done inside a popup or full frame redirect.
*
* For the cases where interaction is required, you cannot send a request with prompt=none.
*
* If your refresh token has expired, you can use this function to fetch a new set of tokens silently as long as
* you session on the server still exists.
* @param request {@link SsoSilentRequest}
*
* @returns A promise that is fulfilled when this function has completed, or rejected if an error was raised.
*/
ssoSilent(request: SsoSilentRequest): Promise<AuthenticationResult> {
return this.controller.ssoSilent(request);
}
/**
* Gets the token cache for the application.
*/
getTokenCache(): ITokenCache {
return this.controller.getTokenCache();
}
/**
* Returns the logger instance
*/
getLogger(): Logger {
return this.controller.getLogger();
}
/**
* Replaces the default logger set in configurations with new Logger with new configurations
* @param logger Logger instance
*/
setLogger(logger: Logger): void {
this.controller.setLogger(logger);
}
/**
* Sets the account to use as the active account. If no account is passed to the acquireToken APIs, then MSAL will use this active account.
* @param account
*/
setActiveAccount(account: AccountInfo | null): void {
this.controller.setActiveAccount(account);
}
/**
* Gets the currently active account
*/
getActiveAccount(): AccountInfo | null {
return this.controller.getActiveAccount();
}
/**
* Called by wrapper libraries (Angular & React) to set SKU and Version passed down to telemetry, logger, etc.
* @param sku
* @param version
*/
initializeWrapperLibrary(sku: WrapperSKU, version: string): void {
return this.controller.initializeWrapperLibrary(sku, version);
}
/**
* Sets navigation client
* @param navigationClient
*/
setNavigationClient(navigationClient: INavigationClient): void {
this.controller.setNavigationClient(navigationClient);
}
/**
* Returns the configuration object
* @internal
*/
getConfiguration(): BrowserConfiguration {
return this.controller.getConfiguration();
}
/**
* Hydrates cache with the tokens and account in the AuthenticationResult object
* @param result
* @param request - The request object that was used to obtain the AuthenticationResult
* @returns
*/
async hydrateCache(
result: AuthenticationResult,
request:
| SilentRequest
| SsoSilentRequest
| RedirectRequest
| PopupRequest
): Promise<void> {
return this.controller.hydrateCache(result, request);
}
/**
* Clears tokens and account from the browser cache.
* @param logoutRequest
*/
clearCache(logoutRequest?: ClearCacheRequest): Promise<void> {
return this.controller.clearCache(logoutRequest);
}
}
@@ -0,0 +1,416 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
NativeConstants,
NativeExtensionMethod,
} from "../../utils/BrowserConstants.js";
import {
Logger,
AuthError,
createAuthError,
AuthErrorCodes,
AuthenticationScheme,
InProgressPerformanceEvent,
PerformanceEvents,
IPerformanceClient,
} from "@azure/msal-common/browser";
import {
NativeExtensionRequest,
NativeExtensionRequestBody,
} from "./NativeRequest.js";
import { createNativeAuthError } from "../../error/NativeAuthError.js";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../../error/BrowserAuthError.js";
import { BrowserConfiguration } from "../../config/Configuration.js";
import { createNewGuid } from "../../crypto/BrowserCrypto.js";
type ResponseResolvers<T> = {
resolve: (value: T | PromiseLike<T>) => void;
reject: (
value: AuthError | Error | PromiseLike<Error> | PromiseLike<AuthError>
) => void;
};
export class NativeMessageHandler {
private extensionId: string | undefined;
private extensionVersion: string | undefined;
private logger: Logger;
private readonly handshakeTimeoutMs: number;
private timeoutId: number | undefined;
private resolvers: Map<string, ResponseResolvers<object>>;
private handshakeResolvers: Map<string, ResponseResolvers<void>>;
private messageChannel: MessageChannel;
private readonly windowListener: (event: MessageEvent) => void;
private readonly performanceClient: IPerformanceClient;
private readonly handshakeEvent: InProgressPerformanceEvent;
constructor(
logger: Logger,
handshakeTimeoutMs: number,
performanceClient: IPerformanceClient,
extensionId?: string
) {
this.logger = logger;
this.handshakeTimeoutMs = handshakeTimeoutMs;
this.extensionId = extensionId;
this.resolvers = new Map(); // Used for non-handshake messages
this.handshakeResolvers = new Map(); // Used for handshake messages
this.messageChannel = new MessageChannel();
this.windowListener = this.onWindowMessage.bind(this); // Window event callback doesn't have access to 'this' unless it's bound
this.performanceClient = performanceClient;
this.handshakeEvent = performanceClient.startMeasurement(
PerformanceEvents.NativeMessageHandlerHandshake
);
}
/**
* Sends a given message to the extension and resolves with the extension response
* @param body
*/
async sendMessage(body: NativeExtensionRequestBody): Promise<object> {
this.logger.trace("NativeMessageHandler - sendMessage called.");
const req: NativeExtensionRequest = {
channel: NativeConstants.CHANNEL_ID,
extensionId: this.extensionId,
responseId: createNewGuid(),
body: body,
};
this.logger.trace(
"NativeMessageHandler - Sending request to browser extension"
);
this.logger.tracePii(
`NativeMessageHandler - Sending request to browser extension: ${JSON.stringify(
req
)}`
);
this.messageChannel.port1.postMessage(req);
return new Promise((resolve, reject) => {
this.resolvers.set(req.responseId, { resolve, reject });
});
}
/**
* Returns an instance of the MessageHandler that has successfully established a connection with an extension
* @param {Logger} logger
* @param {number} handshakeTimeoutMs
* @param {IPerformanceClient} performanceClient
* @param {ICrypto} crypto
*/
static async createProvider(
logger: Logger,
handshakeTimeoutMs: number,
performanceClient: IPerformanceClient
): Promise<NativeMessageHandler> {
logger.trace("NativeMessageHandler - createProvider called.");
try {
const preferredProvider = new NativeMessageHandler(
logger,
handshakeTimeoutMs,
performanceClient,
NativeConstants.PREFERRED_EXTENSION_ID
);
await preferredProvider.sendHandshakeRequest();
return preferredProvider;
} catch (e) {
// If preferred extension fails for whatever reason, fallback to using any installed extension
const backupProvider = new NativeMessageHandler(
logger,
handshakeTimeoutMs,
performanceClient
);
await backupProvider.sendHandshakeRequest();
return backupProvider;
}
}
/**
* Send handshake request helper.
*/
private async sendHandshakeRequest(): Promise<void> {
this.logger.trace(
"NativeMessageHandler - sendHandshakeRequest called."
);
// Register this event listener before sending handshake
window.addEventListener("message", this.windowListener, false); // false is important, because content script message processing should work first
const req: NativeExtensionRequest = {
channel: NativeConstants.CHANNEL_ID,
extensionId: this.extensionId,
responseId: createNewGuid(),
body: {
method: NativeExtensionMethod.HandshakeRequest,
},
};
this.handshakeEvent.add({
extensionId: this.extensionId,
extensionHandshakeTimeoutMs: this.handshakeTimeoutMs,
});
this.messageChannel.port1.onmessage = (event) => {
this.onChannelMessage(event);
};
window.postMessage(req, window.origin, [this.messageChannel.port2]);
return new Promise((resolve, reject) => {
this.handshakeResolvers.set(req.responseId, { resolve, reject });
this.timeoutId = window.setTimeout(() => {
/*
* Throw an error if neither HandshakeResponse nor original Handshake request are received in a reasonable timeframe.
* This typically suggests an event handler stopped propagation of the Handshake request but did not respond to it on the MessageChannel port
*/
window.removeEventListener(
"message",
this.windowListener,
false
);
this.messageChannel.port1.close();
this.messageChannel.port2.close();
this.handshakeEvent.end({
extensionHandshakeTimedOut: true,
success: false,
});
reject(
createBrowserAuthError(
BrowserAuthErrorCodes.nativeHandshakeTimeout
)
);
this.handshakeResolvers.delete(req.responseId);
}, this.handshakeTimeoutMs); // Use a reasonable timeout in milliseconds here
});
}
/**
* Invoked when a message is posted to the window. If a handshake request is received it means the extension is not installed.
* @param event
*/
private onWindowMessage(event: MessageEvent): void {
this.logger.trace("NativeMessageHandler - onWindowMessage called");
// We only accept messages from ourselves
if (event.source !== window) {
return;
}
const request = event.data;
if (
!request.channel ||
request.channel !== NativeConstants.CHANNEL_ID
) {
return;
}
if (request.extensionId && request.extensionId !== this.extensionId) {
return;
}
if (request.body.method === NativeExtensionMethod.HandshakeRequest) {
const handshakeResolver = this.handshakeResolvers.get(
request.responseId
);
/*
* Filter out responses with no matched resolvers sooner to keep channel ports open while waiting for
* the proper response.
*/
if (!handshakeResolver) {
this.logger.trace(
`NativeMessageHandler.onWindowMessage - resolver can't be found for request ${request.responseId}`
);
return;
}
// If we receive this message back it means no extension intercepted the request, meaning no extension supporting handshake protocol is installed
this.logger.verbose(
request.extensionId
? `Extension with id: ${request.extensionId} not installed`
: "No extension installed"
);
clearTimeout(this.timeoutId);
this.messageChannel.port1.close();
this.messageChannel.port2.close();
window.removeEventListener("message", this.windowListener, false);
this.handshakeEvent.end({
success: false,
extensionInstalled: false,
});
handshakeResolver.reject(
createBrowserAuthError(
BrowserAuthErrorCodes.nativeExtensionNotInstalled
)
);
}
}
/**
* Invoked when a message is received from the extension on the MessageChannel port
* @param event
*/
private onChannelMessage(event: MessageEvent): void {
this.logger.trace("NativeMessageHandler - onChannelMessage called.");
const request = event.data;
const resolver = this.resolvers.get(request.responseId);
const handshakeResolver = this.handshakeResolvers.get(
request.responseId
);
try {
const method = request.body.method;
if (method === NativeExtensionMethod.Response) {
if (!resolver) {
return;
}
const response = request.body.response;
this.logger.trace(
"NativeMessageHandler - Received response from browser extension"
);
this.logger.tracePii(
`NativeMessageHandler - Received response from browser extension: ${JSON.stringify(
response
)}`
);
if (response.status !== "Success") {
resolver.reject(
createNativeAuthError(
response.code,
response.description,
response.ext
)
);
} else if (response.result) {
if (
response.result["code"] &&
response.result["description"]
) {
resolver.reject(
createNativeAuthError(
response.result["code"],
response.result["description"],
response.result["ext"]
)
);
} else {
resolver.resolve(response.result);
}
} else {
throw createAuthError(
AuthErrorCodes.unexpectedError,
"Event does not contain result."
);
}
this.resolvers.delete(request.responseId);
} else if (method === NativeExtensionMethod.HandshakeResponse) {
if (!handshakeResolver) {
this.logger.trace(
`NativeMessageHandler.onChannelMessage - resolver can't be found for request ${request.responseId}`
);
return;
}
clearTimeout(this.timeoutId); // Clear setTimeout
window.removeEventListener(
"message",
this.windowListener,
false
); // Remove 'No extension' listener
this.extensionId = request.extensionId;
this.extensionVersion = request.body.version;
this.logger.verbose(
`NativeMessageHandler - Received HandshakeResponse from extension: ${this.extensionId}`
);
this.handshakeEvent.end({
extensionInstalled: true,
success: true,
});
handshakeResolver.resolve();
this.handshakeResolvers.delete(request.responseId);
}
// Do nothing if method is not Response or HandshakeResponse
} catch (err) {
this.logger.error("Error parsing response from WAM Extension");
this.logger.errorPii(
`Error parsing response from WAM Extension: ${err as string}`
);
this.logger.errorPii(`Unable to parse ${event}`);
if (resolver) {
resolver.reject(err as AuthError);
} else if (handshakeResolver) {
handshakeResolver.reject(err as AuthError);
}
}
}
/**
* Returns the Id for the browser extension this handler is communicating with
* @returns
*/
getExtensionId(): string | undefined {
return this.extensionId;
}
/**
* Returns the version for the browser extension this handler is communicating with
* @returns
*/
getExtensionVersion(): string | undefined {
return this.extensionVersion;
}
/**
* Returns boolean indicating whether or not the request should attempt to use native broker
* @param logger
* @param config
* @param nativeExtensionProvider
* @param authenticationScheme
*/
static isPlatformBrokerAvailable(
config: BrowserConfiguration,
logger: Logger,
nativeExtensionProvider?: NativeMessageHandler,
authenticationScheme?: AuthenticationScheme
): boolean {
logger.trace("isPlatformBrokerAvailable called");
if (!config.system.allowPlatformBroker) {
logger.trace(
"isPlatformBrokerAvailable: allowPlatformBroker is not enabled, returning false"
);
// Developer disabled WAM
return false;
}
if (!nativeExtensionProvider) {
logger.trace(
"isPlatformBrokerAvailable: Platform extension provider is not initialized, returning false"
);
// Extension is not available
return false;
}
if (authenticationScheme) {
switch (authenticationScheme) {
case AuthenticationScheme.BEARER:
case AuthenticationScheme.POP:
logger.trace(
"isPlatformBrokerAvailable: authenticationScheme is supported, returning true"
);
return true;
default:
logger.trace(
"isPlatformBrokerAvailable: authenticationScheme is not supported, returning false"
);
return false;
}
}
return true;
}
}
@@ -0,0 +1,54 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { NativeExtensionMethod } from "../../utils/BrowserConstants.js";
import { StoreInCache, StringDict } from "@azure/msal-common/browser";
/**
* Token request which native broker will use to acquire tokens
*/
export type NativeTokenRequest = {
accountId: string; // WAM specific account id used for identification of WAM account. This can be any broker-id eventually
clientId: string;
authority: string;
redirectUri: string;
scope: string;
correlationId: string;
windowTitleSubstring: string; // The name of the document title. This helps the native prompt properly "parent" to the window making the request
prompt?: string;
nonce?: string;
claims?: string;
state?: string;
reqCnf?: string;
keyId?: string;
tokenType?: string;
shrClaims?: string;
shrNonce?: string;
resourceRequestMethod?: string;
resourceRequestUri?: string;
extendedExpiryToken?: boolean;
extraParameters?: StringDict;
storeInCache?: StoreInCache; // Object of booleans indicating whether to store tokens in the cache or not (default is true)
signPopToken?: boolean; // Set to true only if token request deos not contain a PoP keyId
embeddedClientId?: string;
};
/**
* Request which will be forwarded to native broker by the browser extension
*/
export type NativeExtensionRequestBody = {
method: NativeExtensionMethod;
request?: NativeTokenRequest;
};
/**
* Browser extension request
*/
export type NativeExtensionRequest = {
channel: string;
responseId: string;
extensionId?: string;
body: NativeExtensionRequestBody;
};
@@ -0,0 +1,56 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Account properties returned by Native Platform e.g. WAM
*/
export type NativeAccountInfo = {
id: string;
properties: object;
userName: string;
};
/**
* Token response returned by Native Platform
*/
export type NativeResponse = {
access_token: string;
account: NativeAccountInfo;
client_info: string;
expires_in: number;
id_token: string;
properties: NativeResponseProperties;
scope: string;
state: string;
shr?: string;
extendedLifetimeToken?: boolean;
};
/**
* Properties returned under "properties" of the NativeResponse
*/
export type NativeResponseProperties = {
MATS?: string;
};
/**
* The native token broker can optionally include additional information about operations it performs. If that data is returned, MSAL.js will include the following properties in the telemetry it collects.
*/
export type MATS = {
is_cached?: number;
broker_version?: string;
account_join_on_start?: string;
account_join_on_end?: string;
device_join?: string;
prompt_behavior?: string;
api_error_code?: number;
ui_visible?: boolean;
silent_code?: number;
silent_bi_sub_code?: number;
silent_message?: string;
silent_status?: number;
http_status?: number;
http_event_count?: number;
};
@@ -0,0 +1,13 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
// Status Codes that can be thrown by WAM
export const USER_INTERACTION_REQUIRED = "USER_INTERACTION_REQUIRED";
export const USER_CANCEL = "USER_CANCEL";
export const NO_NETWORK = "NO_NETWORK";
export const TRANSIENT_ERROR = "TRANSIENT_ERROR";
export const PERSISTENT_ERROR = "PERSISTENT_ERROR";
export const DISABLED = "DISABLED";
export const ACCOUNT_UNAVAILABLE = "ACCOUNT_UNAVAILABLE";
+184
View File
@@ -0,0 +1,184 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountInfo, AccountFilter, Logger } from "@azure/msal-common/browser";
import { BrowserCacheManager } from "./BrowserCacheManager.js";
/**
* Returns all the accounts in the cache that match the optional filter. If no filter is provided, all accounts are returned.
* @param accountFilter - (Optional) filter to narrow down the accounts returned
* @returns Array of AccountInfo objects in cache
*/
export function getAllAccounts(
logger: Logger,
browserStorage: BrowserCacheManager,
isInBrowser: boolean,
accountFilter?: AccountFilter
): AccountInfo[] {
logger.verbose("getAllAccounts called");
return isInBrowser ? browserStorage.getAllAccounts(accountFilter) : [];
}
/**
* Returns the first account found in the cache that matches the account filter passed in.
* @param accountFilter
* @returns The first account found in the cache matching the provided filter or null if no account could be found.
*/
export function getAccount(
accountFilter: AccountFilter,
logger: Logger,
browserStorage: BrowserCacheManager
): AccountInfo | null {
logger.trace("getAccount called");
if (Object.keys(accountFilter).length === 0) {
logger.warning("getAccount: No accountFilter provided");
return null;
}
const account: AccountInfo | null =
browserStorage.getAccountInfoFilteredBy(accountFilter);
if (account) {
logger.verbose(
"getAccount: Account matching provided filter found, returning"
);
return account;
} else {
logger.verbose("getAccount: No matching account found, returning null");
return null;
}
}
/**
* Returns the signed in account matching username.
* (the account object is created at the time of successful login)
* or null when no matching account is found.
* This API is provided for convenience but getAccountById should be used for best reliability
* @param username
* @returns The account object stored in MSAL
*/
export function getAccountByUsername(
username: string,
logger: Logger,
browserStorage: BrowserCacheManager
): AccountInfo | null {
logger.trace("getAccountByUsername called");
if (!username) {
logger.warning("getAccountByUsername: No username provided");
return null;
}
const account = browserStorage.getAccountInfoFilteredBy({
username,
});
if (account) {
logger.verbose(
"getAccountByUsername: Account matching username found, returning"
);
logger.verbosePii(
`getAccountByUsername: Returning signed-in accounts matching username: ${username}`
);
return account;
} else {
logger.verbose(
"getAccountByUsername: No matching account found, returning null"
);
return null;
}
}
/**
* Returns the signed in account matching homeAccountId.
* (the account object is created at the time of successful login)
* or null when no matching account is found
* @param homeAccountId
* @returns The account object stored in MSAL
*/
export function getAccountByHomeId(
homeAccountId: string,
logger: Logger,
browserStorage: BrowserCacheManager
): AccountInfo | null {
logger.trace("getAccountByHomeId called");
if (!homeAccountId) {
logger.warning("getAccountByHomeId: No homeAccountId provided");
return null;
}
const account = browserStorage.getAccountInfoFilteredBy({
homeAccountId,
});
if (account) {
logger.verbose(
"getAccountByHomeId: Account matching homeAccountId found, returning"
);
logger.verbosePii(
`getAccountByHomeId: Returning signed-in accounts matching homeAccountId: ${homeAccountId}`
);
return account;
} else {
logger.verbose(
"getAccountByHomeId: No matching account found, returning null"
);
return null;
}
}
/**
* Returns the signed in account matching localAccountId.
* (the account object is created at the time of successful login)
* or null when no matching account is found
* @param localAccountId
* @returns The account object stored in MSAL
*/
export function getAccountByLocalId(
localAccountId: string,
logger: Logger,
browserStorage: BrowserCacheManager
): AccountInfo | null {
logger.trace("getAccountByLocalId called");
if (!localAccountId) {
logger.warning("getAccountByLocalId: No localAccountId provided");
return null;
}
const account = browserStorage.getAccountInfoFilteredBy({
localAccountId,
});
if (account) {
logger.verbose(
"getAccountByLocalId: Account matching localAccountId found, returning"
);
logger.verbosePii(
`getAccountByLocalId: Returning signed-in accounts matching localAccountId: ${localAccountId}`
);
return account;
} else {
logger.verbose(
"getAccountByLocalId: No matching account found, returning null"
);
return null;
}
}
/**
* Sets the account to use as the active account. If no account is passed to the acquireToken APIs, then MSAL will use this active account.
* @param account
*/
export function setActiveAccount(
account: AccountInfo | null,
browserStorage: BrowserCacheManager
): void {
browserStorage.setActiveAccount(account);
}
/**
* Gets the currently active account
*/
export function getActiveAccount(
browserStorage: BrowserCacheManager
): AccountInfo | null {
return browserStorage.getActiveAccount();
}
+156
View File
@@ -0,0 +1,156 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Logger } from "@azure/msal-common/browser";
import {
BrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { DatabaseStorage } from "./DatabaseStorage.js";
import { IAsyncStorage } from "./IAsyncStorage.js";
import { MemoryStorage } from "./MemoryStorage.js";
/**
* This class allows MSAL to store artifacts asynchronously using the DatabaseStorage IndexedDB wrapper,
* backed up with the more volatile MemoryStorage object for cases in which IndexedDB may be unavailable.
*/
export class AsyncMemoryStorage<T> implements IAsyncStorage<T> {
private inMemoryCache: MemoryStorage<T>;
private indexedDBCache: DatabaseStorage<T>;
private logger: Logger;
constructor(logger: Logger) {
this.inMemoryCache = new MemoryStorage<T>();
this.indexedDBCache = new DatabaseStorage<T>();
this.logger = logger;
}
private handleDatabaseAccessError(error: unknown): void {
if (
error instanceof BrowserAuthError &&
error.errorCode === BrowserAuthErrorCodes.databaseUnavailable
) {
this.logger.error(
"Could not access persistent storage. This may be caused by browser privacy features which block persistent storage in third-party contexts."
);
} else {
throw error;
}
}
/**
* Get the item matching the given key. Tries in-memory cache first, then in the asynchronous
* storage object if item isn't found in-memory.
* @param key
*/
async getItem(key: string): Promise<T | null> {
const item = this.inMemoryCache.getItem(key);
if (!item) {
try {
this.logger.verbose(
"Queried item not found in in-memory cache, now querying persistent storage."
);
return await this.indexedDBCache.getItem(key);
} catch (e) {
this.handleDatabaseAccessError(e);
}
}
return item;
}
/**
* Sets the item in the in-memory cache and then tries to set it in the asynchronous
* storage object with the given key.
* @param key
* @param value
*/
async setItem(key: string, value: T): Promise<void> {
this.inMemoryCache.setItem(key, value);
try {
await this.indexedDBCache.setItem(key, value);
} catch (e) {
this.handleDatabaseAccessError(e);
}
}
/**
* Removes the item matching the key from the in-memory cache, then tries to remove it from the asynchronous storage object.
* @param key
*/
async removeItem(key: string): Promise<void> {
this.inMemoryCache.removeItem(key);
try {
await this.indexedDBCache.removeItem(key);
} catch (e) {
this.handleDatabaseAccessError(e);
}
}
/**
* Get all the keys from the in-memory cache as an iterable array of strings. If no keys are found, query the keys in the
* asynchronous storage object.
*/
async getKeys(): Promise<string[]> {
const cacheKeys = this.inMemoryCache.getKeys();
if (cacheKeys.length === 0) {
try {
this.logger.verbose(
"In-memory cache is empty, now querying persistent storage."
);
return await this.indexedDBCache.getKeys();
} catch (e) {
this.handleDatabaseAccessError(e);
}
}
return cacheKeys;
}
/**
* Returns true or false if the given key is present in the cache.
* @param key
*/
async containsKey(key: string): Promise<boolean> {
const containsKey = this.inMemoryCache.containsKey(key);
if (!containsKey) {
try {
this.logger.verbose(
"Key not found in in-memory cache, now querying persistent storage."
);
return await this.indexedDBCache.containsKey(key);
} catch (e) {
this.handleDatabaseAccessError(e);
}
}
return containsKey;
}
/**
* Clears in-memory Map
*/
clearInMemory(): void {
// InMemory cache is a Map instance, clear is straightforward
this.logger.verbose(`Deleting in-memory keystore`);
this.inMemoryCache.clear();
this.logger.verbose(`In-memory keystore deleted`);
}
/**
* Tries to delete the IndexedDB database
* @returns
*/
async clearPersistent(): Promise<boolean> {
try {
this.logger.verbose("Deleting persistent keystore");
const dbDeleted = await this.indexedDBCache.deleteDatabase();
if (dbDeleted) {
this.logger.verbose("Persistent keystore deleted");
}
return dbDeleted;
} catch (e) {
this.handleDatabaseAccessError(e);
return false;
}
}
}
File diff suppressed because it is too large Load Diff
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TokenKeys } from "@azure/msal-common/browser";
import { StaticCacheKeys } from "../utils/BrowserConstants.js";
import { IWindowStorage } from "./IWindowStorage.js";
/**
* Returns a list of cache keys for all known accounts
* @param storage
* @returns
*/
export function getAccountKeys(storage: IWindowStorage<string>): Array<string> {
const accountKeys = storage.getItem(StaticCacheKeys.ACCOUNT_KEYS);
if (accountKeys) {
return JSON.parse(accountKeys);
}
return [];
}
/**
* Returns a list of cache keys for all known tokens
* @param clientId
* @param storage
* @returns
*/
export function getTokenKeys(
clientId: string,
storage: IWindowStorage<string>
): TokenKeys {
const item = storage.getItem(`${StaticCacheKeys.TOKEN_KEYS}.${clientId}`);
if (item) {
const tokenKeys = JSON.parse(item);
if (
tokenKeys &&
tokenKeys.hasOwnProperty("idToken") &&
tokenKeys.hasOwnProperty("accessToken") &&
tokenKeys.hasOwnProperty("refreshToken")
) {
return tokenKeys as TokenKeys;
}
}
return {
idToken: [],
accessToken: [],
refreshToken: [],
};
}
+107
View File
@@ -0,0 +1,107 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ClientAuthErrorCodes,
createClientAuthError,
} from "@azure/msal-common/browser";
import { IWindowStorage } from "./IWindowStorage.js";
// Cookie life calculation (hours * minutes * seconds * ms)
const COOKIE_LIFE_MULTIPLIER = 24 * 60 * 60 * 1000;
export const SameSiteOptions = {
Lax: "Lax",
None: "None",
} as const;
export type SameSiteOptions =
(typeof SameSiteOptions)[keyof typeof SameSiteOptions];
export class CookieStorage implements IWindowStorage<string> {
initialize(): Promise<void> {
return Promise.resolve();
}
getItem(key: string): string | null {
const name = `${encodeURIComponent(key)}`;
const cookieList = document.cookie.split(";");
for (let i = 0; i < cookieList.length; i++) {
const cookie = cookieList[i];
const [key, ...rest] = decodeURIComponent(cookie).trim().split("=");
const value = rest.join("=");
if (key === name) {
return value;
}
}
return "";
}
getUserData(): string | null {
throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented);
}
setItem(
key: string,
value: string,
cookieLifeDays?: number,
secure: boolean = true,
sameSite: SameSiteOptions = SameSiteOptions.Lax
): void {
let cookieStr = `${encodeURIComponent(key)}=${encodeURIComponent(
value
)};path=/;SameSite=${sameSite};`;
if (cookieLifeDays) {
const expireTime = getCookieExpirationTime(cookieLifeDays);
cookieStr += `expires=${expireTime};`;
}
if (secure || sameSite === SameSiteOptions.None) {
// SameSite None requires Secure flag
cookieStr += "Secure;";
}
document.cookie = cookieStr;
}
async setUserData(): Promise<void> {
return Promise.reject(
createClientAuthError(ClientAuthErrorCodes.methodNotImplemented)
);
}
removeItem(key: string): void {
// Setting expiration to -1 removes it
this.setItem(key, "", -1);
}
getKeys(): string[] {
const cookieList = document.cookie.split(";");
const keys: Array<string> = [];
cookieList.forEach((cookie) => {
const cookieParts = decodeURIComponent(cookie).trim().split("=");
keys.push(cookieParts[0]);
});
return keys;
}
containsKey(key: string): boolean {
return this.getKeys().includes(key);
}
}
/**
* Get cookie expiration time
* @param cookieLifeDays
*/
export function getCookieExpirationTime(cookieLifeDays: number): string {
const today = new Date();
const expr = new Date(
today.getTime() + cookieLifeDays * COOKIE_LIFE_MULTIPLIER
);
return expr.toUTCString();
}
+301
View File
@@ -0,0 +1,301 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import {
DB_NAME,
DB_TABLE_NAME,
DB_VERSION,
} from "../utils/BrowserConstants.js";
import { IAsyncStorage } from "./IAsyncStorage.js";
interface IDBOpenDBRequestEvent extends Event {
target: IDBOpenDBRequest & EventTarget;
}
interface IDBOpenOnUpgradeNeededEvent extends IDBVersionChangeEvent {
target: IDBOpenDBRequest & EventTarget;
}
interface IDBRequestEvent extends Event {
target: IDBRequest & EventTarget;
}
/**
* Storage wrapper for IndexedDB storage in browsers: https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API
*/
export class DatabaseStorage<T> implements IAsyncStorage<T> {
private db: IDBDatabase | undefined;
private dbName: string;
private tableName: string;
private version: number;
private dbOpen: boolean;
constructor() {
this.dbName = DB_NAME;
this.version = DB_VERSION;
this.tableName = DB_TABLE_NAME;
this.dbOpen = false;
}
/**
* Opens IndexedDB instance.
*/
async open(): Promise<void> {
return new Promise((resolve, reject) => {
const openDB = window.indexedDB.open(this.dbName, this.version);
openDB.addEventListener(
"upgradeneeded",
(e: IDBVersionChangeEvent) => {
const event = e as IDBOpenOnUpgradeNeededEvent;
event.target.result.createObjectStore(this.tableName);
}
);
openDB.addEventListener("success", (e: Event) => {
const event = e as IDBOpenDBRequestEvent;
this.db = event.target.result;
this.dbOpen = true;
resolve();
});
openDB.addEventListener("error", () =>
reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseUnavailable
)
)
);
});
}
/**
* Closes the connection to IndexedDB database when all pending transactions
* complete.
*/
closeConnection(): void {
const db = this.db;
if (db && this.dbOpen) {
db.close();
this.dbOpen = false;
}
}
/**
* Opens database if it's not already open
*/
private async validateDbIsOpen(): Promise<void> {
if (!this.dbOpen) {
return this.open();
}
}
/**
* Retrieves item from IndexedDB instance.
* @param key
*/
async getItem(key: string): Promise<T | null> {
await this.validateDbIsOpen();
return new Promise<T>((resolve, reject) => {
// TODO: Add timeouts?
if (!this.db) {
return reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseNotOpen
)
);
}
const transaction = this.db.transaction(
[this.tableName],
"readonly"
);
const objectStore = transaction.objectStore(this.tableName);
const dbGet = objectStore.get(key);
dbGet.addEventListener("success", (e: Event) => {
const event = e as IDBRequestEvent;
this.closeConnection();
resolve(event.target.result);
});
dbGet.addEventListener("error", (e: Event) => {
this.closeConnection();
reject(e);
});
});
}
/**
* Adds item to IndexedDB under given key
* @param key
* @param payload
*/
async setItem(key: string, payload: T): Promise<void> {
await this.validateDbIsOpen();
return new Promise<void>((resolve: Function, reject: Function) => {
// TODO: Add timeouts?
if (!this.db) {
return reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseNotOpen
)
);
}
const transaction = this.db.transaction(
[this.tableName],
"readwrite"
);
const objectStore = transaction.objectStore(this.tableName);
const dbPut = objectStore.put(payload, key);
dbPut.addEventListener("success", () => {
this.closeConnection();
resolve();
});
dbPut.addEventListener("error", (e) => {
this.closeConnection();
reject(e);
});
});
}
/**
* Removes item from IndexedDB under given key
* @param key
*/
async removeItem(key: string): Promise<void> {
await this.validateDbIsOpen();
return new Promise<void>((resolve: Function, reject: Function) => {
if (!this.db) {
return reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseNotOpen
)
);
}
const transaction = this.db.transaction(
[this.tableName],
"readwrite"
);
const objectStore = transaction.objectStore(this.tableName);
const dbDelete = objectStore.delete(key);
dbDelete.addEventListener("success", () => {
this.closeConnection();
resolve();
});
dbDelete.addEventListener("error", (e) => {
this.closeConnection();
reject(e);
});
});
}
/**
* Get all the keys from the storage object as an iterable array of strings.
*/
async getKeys(): Promise<string[]> {
await this.validateDbIsOpen();
return new Promise<string[]>((resolve: Function, reject: Function) => {
if (!this.db) {
return reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseNotOpen
)
);
}
const transaction = this.db.transaction(
[this.tableName],
"readonly"
);
const objectStore = transaction.objectStore(this.tableName);
const dbGetKeys = objectStore.getAllKeys();
dbGetKeys.addEventListener("success", (e: Event) => {
const event = e as IDBRequestEvent;
this.closeConnection();
resolve(event.target.result);
});
dbGetKeys.addEventListener("error", (e: Event) => {
this.closeConnection();
reject(e);
});
});
}
/**
*
* Checks whether there is an object under the search key in the object store
*/
async containsKey(key: string): Promise<boolean> {
await this.validateDbIsOpen();
return new Promise<boolean>((resolve: Function, reject: Function) => {
if (!this.db) {
return reject(
createBrowserAuthError(
BrowserAuthErrorCodes.databaseNotOpen
)
);
}
const transaction = this.db.transaction(
[this.tableName],
"readonly"
);
const objectStore = transaction.objectStore(this.tableName);
const dbContainsKey = objectStore.count(key);
dbContainsKey.addEventListener("success", (e: Event) => {
const event = e as IDBRequestEvent;
this.closeConnection();
resolve(event.target.result === 1);
});
dbContainsKey.addEventListener("error", (e: Event) => {
this.closeConnection();
reject(e);
});
});
}
/**
* Deletes the MSAL database. The database is deleted rather than cleared to make it possible
* for client applications to downgrade to a previous MSAL version without worrying about forward compatibility issues
* with IndexedDB database versions.
*/
async deleteDatabase(): Promise<boolean> {
// Check if database being deleted exists
if (this.db && this.dbOpen) {
this.closeConnection();
}
return new Promise<boolean>((resolve: Function, reject: Function) => {
const deleteDbRequest = window.indexedDB.deleteDatabase(DB_NAME);
const id = setTimeout(() => reject(false), 200); // Reject if events aren't raised within 200ms
deleteDbRequest.addEventListener("success", () => {
clearTimeout(id);
return resolve(true);
});
deleteDbRequest.addEventListener("blocked", () => {
clearTimeout(id);
return resolve(true);
});
deleteDbRequest.addEventListener("error", () => {
clearTimeout(id);
return reject(false);
});
});
}
}
+36
View File
@@ -0,0 +1,36 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export interface IAsyncStorage<T> {
/**
* Get the item from the asynchronous storage object matching the given key.
* @param key
*/
getItem(key: string): Promise<T | null>;
/**
* Sets the item in the asynchronous storage object with the given key.
* @param key
* @param value
*/
setItem(key: string, value: T): Promise<void>;
/**
* Removes the item in the asynchronous storage object matching the given key.
* @param key
*/
removeItem(key: string): Promise<void>;
/**
* Get all the keys from the asynchronous storage object as an iterable array of strings.
*/
getKeys(): Promise<string[]>;
/**
* Returns true or false if the given key is present in the cache.
* @param key
*/
containsKey(key: string): Promise<boolean>;
}
+21
View File
@@ -0,0 +1,21 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ExternalTokenResponse } from "@azure/msal-common/browser";
import { SilentRequest } from "../request/SilentRequest.js";
import { LoadTokenOptions } from "./TokenCache.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
export interface ITokenCache {
/**
* API to side-load tokens to MSAL cache
* @returns `AuthenticationResult` for the response that was loaded.
*/
loadExternalTokens(
request: SilentRequest,
response: ExternalTokenResponse,
options: LoadTokenOptions
): Promise<AuthenticationResult>;
}
+50
View File
@@ -0,0 +1,50 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export interface IWindowStorage<T> {
/**
* Async initializer
*/
initialize(correlationId: string): Promise<void>;
/**
* Get the item from the window storage object matching the given key.
* @param key
*/
getItem(key: string): T | null;
/**
* Getter for sensitive data that may contain PII.
*/
getUserData(key: string): T | null;
/**
* Sets the item in the window storage object with the given key.
* @param key
* @param value
*/
setItem(key: string, value: T): void;
/**
* Setter for sensitive data that may contain PII.
*/
setUserData(key: string, value: T, correlationId: string): Promise<void>;
/**
* Removes the item in the window storage object matching the given key.
* @param key
*/
removeItem(key: string): void;
/**
* Get all the keys from the window storage object as an iterable array of strings.
*/
getKeys(): string[];
/**
* Returns true or false if the given key is present in the cache.
* @param key
*/
containsKey(key: string): boolean;
}
+431
View File
@@ -0,0 +1,431 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
Constants,
TokenKeys,
IPerformanceClient,
invokeAsync,
PerformanceEvents,
Logger,
invoke,
} from "@azure/msal-common/browser";
import {
createNewGuid,
decrypt,
encrypt,
generateBaseKey,
generateHKDF,
} from "../crypto/BrowserCrypto.js";
import { base64DecToArr } from "../encode/Base64Decode.js";
import { urlEncodeArr } from "../encode/Base64Encode.js";
import {
BrowserAuthErrorCodes,
createBrowserAuthError,
} from "../error/BrowserAuthError.js";
import {
BrowserConfigurationAuthErrorCodes,
createBrowserConfigurationAuthError,
} from "../error/BrowserConfigurationAuthError.js";
import { CookieStorage, SameSiteOptions } from "./CookieStorage.js";
import { IWindowStorage } from "./IWindowStorage.js";
import { MemoryStorage } from "./MemoryStorage.js";
import { getAccountKeys, getTokenKeys } from "./CacheHelpers.js";
import { StaticCacheKeys } from "../utils/BrowserConstants.js";
const ENCRYPTION_KEY = "msal.cache.encryption";
const BROADCAST_CHANNEL_NAME = "msal.broadcast.cache";
type EncryptionCookie = {
id: string;
key: CryptoKey;
};
type EncryptedData = {
id: string;
nonce: string;
data: string;
};
export class LocalStorage implements IWindowStorage<string> {
private clientId: string;
private initialized: boolean;
private memoryStorage: MemoryStorage<string>;
private performanceClient: IPerformanceClient;
private logger: Logger;
private encryptionCookie?: EncryptionCookie;
private broadcast: BroadcastChannel;
constructor(
clientId: string,
logger: Logger,
performanceClient: IPerformanceClient
) {
if (!window.localStorage) {
throw createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.storageNotSupported
);
}
this.memoryStorage = new MemoryStorage<string>();
this.initialized = false;
this.clientId = clientId;
this.logger = logger;
this.performanceClient = performanceClient;
this.broadcast = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
}
async initialize(correlationId: string): Promise<void> {
this.initialized = true;
const cookies = new CookieStorage();
const cookieString = cookies.getItem(ENCRYPTION_KEY);
let parsedCookie = { key: "", id: "" };
if (cookieString) {
try {
parsedCookie = JSON.parse(cookieString);
} catch (e) {}
}
if (parsedCookie.key && parsedCookie.id) {
// Encryption key already exists, import
const baseKey = invoke(
base64DecToArr,
PerformanceEvents.Base64Decode,
this.logger,
this.performanceClient,
correlationId
)(parsedCookie.key);
this.encryptionCookie = {
id: parsedCookie.id,
key: await invokeAsync(
generateHKDF,
PerformanceEvents.GenerateHKDF,
this.logger,
this.performanceClient,
correlationId
)(baseKey),
};
await invokeAsync(
this.importExistingCache.bind(this),
PerformanceEvents.ImportExistingCache,
this.logger,
this.performanceClient,
correlationId
)(correlationId);
} else {
// Encryption key doesn't exist or is invalid, generate a new one and clear existing cache
this.clear();
const id = createNewGuid();
const baseKey = await invokeAsync(
generateBaseKey,
PerformanceEvents.GenerateBaseKey,
this.logger,
this.performanceClient,
correlationId
)();
const keyStr = invoke(
urlEncodeArr,
PerformanceEvents.UrlEncodeArr,
this.logger,
this.performanceClient,
correlationId
)(new Uint8Array(baseKey));
this.encryptionCookie = {
id: id,
key: await invokeAsync(
generateHKDF,
PerformanceEvents.GenerateHKDF,
this.logger,
this.performanceClient,
correlationId
)(baseKey),
};
const cookieData = {
id: id,
key: keyStr,
};
cookies.setItem(
ENCRYPTION_KEY,
JSON.stringify(cookieData),
0, // Expiration - 0 means cookie will be cleared at the end of the browser session
true, // Secure flag
SameSiteOptions.None // SameSite must be None to support iframed apps
);
}
// Register listener for cache updates in other tabs
this.broadcast.addEventListener("message", this.updateCache.bind(this));
}
getItem(key: string): string | null {
return window.localStorage.getItem(key);
}
getUserData(key: string): string | null {
if (!this.initialized) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.uninitializedPublicClientApplication
);
}
return this.memoryStorage.getItem(key);
}
setItem(key: string, value: string): void {
window.localStorage.setItem(key, value);
}
async setUserData(
key: string,
value: string,
correlationId: string
): Promise<void> {
if (!this.initialized || !this.encryptionCookie) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.uninitializedPublicClientApplication
);
}
const { data, nonce } = await invokeAsync(
encrypt,
PerformanceEvents.Encrypt,
this.logger,
this.performanceClient,
correlationId
)(this.encryptionCookie.key, value, this.getContext(key));
const encryptedData: EncryptedData = {
id: this.encryptionCookie.id,
nonce: nonce,
data: data,
};
this.memoryStorage.setItem(key, value);
this.setItem(key, JSON.stringify(encryptedData));
// Notify other frames to update their in-memory cache
this.broadcast.postMessage({
key: key,
value: value,
context: this.getContext(key),
});
}
removeItem(key: string): void {
if (this.memoryStorage.containsKey(key)) {
this.memoryStorage.removeItem(key);
this.broadcast.postMessage({
key: key,
value: null,
context: this.getContext(key),
});
}
window.localStorage.removeItem(key);
}
getKeys(): string[] {
return Object.keys(window.localStorage);
}
containsKey(key: string): boolean {
return window.localStorage.hasOwnProperty(key);
}
/**
* Removes all known MSAL keys from the cache
*/
clear(): void {
// Removes all remaining MSAL cache items
this.memoryStorage.clear();
const accountKeys = getAccountKeys(this);
accountKeys.forEach((key) => this.removeItem(key));
const tokenKeys = getTokenKeys(this.clientId, this);
tokenKeys.idToken.forEach((key) => this.removeItem(key));
tokenKeys.accessToken.forEach((key) => this.removeItem(key));
tokenKeys.refreshToken.forEach((key) => this.removeItem(key));
// Clean up anything left
this.getKeys().forEach((cacheKey: string) => {
if (
cacheKey.startsWith(Constants.CACHE_PREFIX) ||
cacheKey.indexOf(this.clientId) !== -1
) {
this.removeItem(cacheKey);
}
});
}
/**
* Helper to decrypt all known MSAL keys in localStorage and save them to inMemory storage
* @returns
*/
private async importExistingCache(correlationId: string): Promise<void> {
if (!this.encryptionCookie) {
return;
}
let accountKeys = getAccountKeys(this);
accountKeys = await this.importArray(accountKeys, correlationId);
// Write valid account keys back to map
this.setItem(StaticCacheKeys.ACCOUNT_KEYS, JSON.stringify(accountKeys));
const tokenKeys: TokenKeys = getTokenKeys(this.clientId, this);
tokenKeys.idToken = await this.importArray(
tokenKeys.idToken,
correlationId
);
tokenKeys.accessToken = await this.importArray(
tokenKeys.accessToken,
correlationId
);
tokenKeys.refreshToken = await this.importArray(
tokenKeys.refreshToken,
correlationId
);
// Write valid token keys back to map
this.setItem(
`${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}`,
JSON.stringify(tokenKeys)
);
}
/**
* Helper to decrypt and save cache entries
* @param key
* @returns
*/
private async getItemFromEncryptedCache(
key: string,
correlationId: string
): Promise<string | null> {
if (!this.encryptionCookie) {
return null;
}
const rawCache = this.getItem(key);
if (!rawCache) {
return null;
}
let encObj: EncryptedData;
try {
encObj = JSON.parse(rawCache);
} catch (e) {
// Not a valid encrypted object, remove
return null;
}
if (!encObj.id || !encObj.nonce || !encObj.data) {
// Data is not encrypted, likely from old version of MSAL. It must be removed because we don't know how old it is.
this.performanceClient.incrementFields(
{ unencryptedCacheCount: 1 },
correlationId
);
return null;
}
if (encObj.id !== this.encryptionCookie.id) {
// Data was encrypted with a different key. It must be removed because it is from a previous session.
this.performanceClient.incrementFields(
{ encryptedCacheExpiredCount: 1 },
correlationId
);
return null;
}
return invokeAsync(
decrypt,
PerformanceEvents.Decrypt,
this.logger,
this.performanceClient,
correlationId
)(
this.encryptionCookie.key,
encObj.nonce,
this.getContext(key),
encObj.data
);
}
/**
* Helper to decrypt and save an array of cache keys
* @param arr
* @returns Array of keys successfully imported
*/
private async importArray(
arr: Array<string>,
correlationId: string
): Promise<Array<string>> {
const importedArr: Array<string> = [];
const promiseArr: Array<Promise<void>> = [];
arr.forEach((key) => {
const promise = this.getItemFromEncryptedCache(
key,
correlationId
).then((value) => {
if (value) {
this.memoryStorage.setItem(key, value);
importedArr.push(key);
} else {
// If value is empty, unencrypted or expired remove
this.removeItem(key);
}
});
promiseArr.push(promise);
});
await Promise.all(promiseArr);
return importedArr;
}
/**
* Gets encryption context for a given cache entry. This is clientId for app specific entries, empty string for shared entries
* @param key
* @returns
*/
private getContext(key: string): string {
let context = "";
if (key.includes(this.clientId)) {
context = this.clientId; // Used to bind encryption key to this appId
}
return context;
}
private updateCache(event: MessageEvent): void {
this.logger.trace("Updating internal cache from broadcast event");
const perfMeasurement = this.performanceClient.startMeasurement(
PerformanceEvents.LocalStorageUpdated
);
perfMeasurement.add({ isBackground: true });
const { key, value, context } = event.data;
if (!key) {
this.logger.error("Broadcast event missing key");
perfMeasurement.end({ success: false, errorCode: "noKey" });
return;
}
if (context && context !== this.clientId) {
this.logger.trace(
`Ignoring broadcast event from clientId: ${context}`
);
perfMeasurement.end({
success: false,
errorCode: "contextMismatch",
});
return;
}
if (!value) {
this.memoryStorage.removeItem(key);
this.logger.verbose("Removed item from internal cache");
} else {
this.memoryStorage.setItem(key, value);
this.logger.verbose("Updated item in internal cache");
}
perfMeasurement.end({ success: true });
}
}
+54
View File
@@ -0,0 +1,54 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { IWindowStorage } from "./IWindowStorage.js";
export class MemoryStorage<T> implements IWindowStorage<T> {
private cache: Map<string, T>;
constructor() {
this.cache = new Map<string, T>();
}
async initialize(): Promise<void> {
// Memory storage does not require initialization
}
getItem(key: string): T | null {
return this.cache.get(key) || null;
}
getUserData(key: string): T | null {
return this.getItem(key);
}
setItem(key: string, value: T): void {
this.cache.set(key, value);
}
async setUserData(key: string, value: T): Promise<void> {
this.setItem(key, value);
}
removeItem(key: string): void {
this.cache.delete(key);
}
getKeys(): string[] {
const cacheKeys: string[] = [];
this.cache.forEach((value: T, key: string) => {
cacheKeys.push(key);
});
return cacheKeys;
}
containsKey(key: string): boolean {
return this.cache.has(key);
}
clear(): void {
this.cache.clear();
}
}
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
BrowserConfigurationAuthErrorCodes,
createBrowserConfigurationAuthError,
} from "../error/BrowserConfigurationAuthError.js";
import { IWindowStorage } from "./IWindowStorage.js";
export class SessionStorage implements IWindowStorage<string> {
constructor() {
if (!window.sessionStorage) {
throw createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.storageNotSupported
);
}
}
async initialize(): Promise<void> {
// Session storage does not require initialization
}
getItem(key: string): string | null {
return window.sessionStorage.getItem(key);
}
getUserData(key: string): string | null {
return this.getItem(key);
}
setItem(key: string, value: string): void {
window.sessionStorage.setItem(key, value);
}
async setUserData(key: string, value: string): Promise<void> {
this.setItem(key, value);
}
removeItem(key: string): void {
window.sessionStorage.removeItem(key);
}
getKeys(): string[] {
return Object.keys(window.sessionStorage);
}
containsKey(key: string): boolean {
return window.sessionStorage.hasOwnProperty(key);
}
}
+424
View File
@@ -0,0 +1,424 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AccessTokenEntity,
ICrypto,
IdTokenEntity,
Logger,
ScopeSet,
Authority,
AuthorityOptions,
ExternalTokenResponse,
AccountEntity,
AuthToken,
RefreshTokenEntity,
CacheRecord,
TokenClaims,
CacheHelpers,
buildAccountToCache,
} from "@azure/msal-common/browser";
import { BrowserConfiguration } from "../config/Configuration.js";
import { SilentRequest } from "../request/SilentRequest.js";
import { BrowserCacheManager } from "./BrowserCacheManager.js";
import { ITokenCache } from "./ITokenCache.js";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { base64Decode } from "../encode/Base64Decode.js";
import * as BrowserCrypto from "../crypto/BrowserCrypto.js";
export type LoadTokenOptions = {
clientInfo?: string;
expiresOn?: number;
extendedExpiresOn?: number;
};
/**
* Token cache manager
*/
export class TokenCache implements ITokenCache {
// Flag to indicate if in browser environment
public isBrowserEnvironment: boolean;
// Input configuration by developer/user
protected config: BrowserConfiguration;
// Browser cache storage
private storage: BrowserCacheManager;
// Logger
private logger: Logger;
// Crypto class
private cryptoObj: ICrypto;
constructor(
configuration: BrowserConfiguration,
storage: BrowserCacheManager,
logger: Logger,
cryptoObj: ICrypto
) {
this.isBrowserEnvironment = typeof window !== "undefined";
this.config = configuration;
this.storage = storage;
this.logger = logger;
this.cryptoObj = cryptoObj;
}
// Move getAllAccounts here and cache utility APIs
/**
* API to load tokens to msal-browser cache.
* @param request
* @param response
* @param options
* @returns `AuthenticationResult` for the response that was loaded.
*/
async loadExternalTokens(
request: SilentRequest,
response: ExternalTokenResponse,
options: LoadTokenOptions
): Promise<AuthenticationResult> {
if (!this.isBrowserEnvironment) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.nonBrowserEnvironment
);
}
const correlationId =
request.correlationId || BrowserCrypto.createNewGuid();
const idTokenClaims = response.id_token
? AuthToken.extractTokenClaims(response.id_token, base64Decode)
: undefined;
const authorityOptions: AuthorityOptions = {
protocolMode: this.config.auth.protocolMode,
knownAuthorities: this.config.auth.knownAuthorities,
cloudDiscoveryMetadata: this.config.auth.cloudDiscoveryMetadata,
authorityMetadata: this.config.auth.authorityMetadata,
skipAuthorityMetadataCache:
this.config.auth.skipAuthorityMetadataCache,
};
const authority = request.authority
? new Authority(
Authority.generateAuthority(
request.authority,
request.azureCloudOptions
),
this.config.system.networkClient,
this.storage,
authorityOptions,
this.logger,
request.correlationId || BrowserCrypto.createNewGuid()
)
: undefined;
const cacheRecordAccount: AccountEntity = await this.loadAccount(
request,
options.clientInfo || response.client_info || "",
correlationId,
idTokenClaims,
authority
);
const idToken = await this.loadIdToken(
response,
cacheRecordAccount.homeAccountId,
cacheRecordAccount.environment,
cacheRecordAccount.realm,
correlationId
);
const accessToken = await this.loadAccessToken(
request,
response,
cacheRecordAccount.homeAccountId,
cacheRecordAccount.environment,
cacheRecordAccount.realm,
options,
correlationId
);
const refreshToken = await this.loadRefreshToken(
response,
cacheRecordAccount.homeAccountId,
cacheRecordAccount.environment,
correlationId
);
return this.generateAuthenticationResult(
request,
{
account: cacheRecordAccount,
idToken,
accessToken,
refreshToken,
},
idTokenClaims,
authority
);
}
/**
* Helper function to load account to msal-browser cache
* @param idToken
* @param environment
* @param clientInfo
* @param authorityType
* @param requestHomeAccountId
* @returns `AccountEntity`
*/
private async loadAccount(
request: SilentRequest,
clientInfo: string,
correlationId: string,
idTokenClaims?: TokenClaims,
authority?: Authority
): Promise<AccountEntity> {
this.logger.verbose("TokenCache - loading account");
if (request.account) {
const accountEntity = AccountEntity.createFromAccountInfo(
request.account
);
await this.storage.setAccount(accountEntity, correlationId);
return accountEntity;
} else if (!authority || (!clientInfo && !idTokenClaims)) {
this.logger.error(
"TokenCache - if an account is not provided on the request, authority and either clientInfo or idToken must be provided instead."
);
throw createBrowserAuthError(
BrowserAuthErrorCodes.unableToLoadToken
);
}
const homeAccountId = AccountEntity.generateHomeAccountId(
clientInfo,
authority.authorityType,
this.logger,
this.cryptoObj,
idTokenClaims
);
const claimsTenantId = idTokenClaims?.tid;
const cachedAccount = buildAccountToCache(
this.storage,
authority,
homeAccountId,
base64Decode,
idTokenClaims,
clientInfo,
authority.hostnameAndPort,
claimsTenantId,
undefined, // authCodePayload
undefined, // nativeAccountId
this.logger
);
await this.storage.setAccount(cachedAccount, correlationId);
return cachedAccount;
}
/**
* Helper function to load id tokens to msal-browser cache
* @param idToken
* @param homeAccountId
* @param environment
* @param tenantId
* @returns `IdTokenEntity`
*/
private async loadIdToken(
response: ExternalTokenResponse,
homeAccountId: string,
environment: string,
tenantId: string,
correlationId: string
): Promise<IdTokenEntity | null> {
if (!response.id_token) {
this.logger.verbose("TokenCache - no id token found in response");
return null;
}
this.logger.verbose("TokenCache - loading id token");
const idTokenEntity = CacheHelpers.createIdTokenEntity(
homeAccountId,
environment,
response.id_token,
this.config.auth.clientId,
tenantId
);
await this.storage.setIdTokenCredential(idTokenEntity, correlationId);
return idTokenEntity;
}
/**
* Helper function to load access tokens to msal-browser cache
* @param request
* @param response
* @param homeAccountId
* @param environment
* @param tenantId
* @returns `AccessTokenEntity`
*/
private async loadAccessToken(
request: SilentRequest,
response: ExternalTokenResponse,
homeAccountId: string,
environment: string,
tenantId: string,
options: LoadTokenOptions,
correlationId: string
): Promise<AccessTokenEntity | null> {
if (!response.access_token) {
this.logger.verbose(
"TokenCache - no access token found in response"
);
return null;
} else if (!response.expires_in) {
this.logger.error(
"TokenCache - no expiration set on the access token. Cannot add it to the cache."
);
return null;
} else if (
!response.scope &&
(!request.scopes || !request.scopes.length)
) {
this.logger.error(
"TokenCache - scopes not specified in the request or response. Cannot add token to the cache."
);
return null;
}
this.logger.verbose("TokenCache - loading access token");
const scopes = response.scope
? ScopeSet.fromString(response.scope)
: new ScopeSet(request.scopes);
const expiresOn =
options.expiresOn ||
response.expires_in + new Date().getTime() / 1000;
const extendedExpiresOn =
options.extendedExpiresOn ||
(response.ext_expires_in || response.expires_in) +
new Date().getTime() / 1000;
const accessTokenEntity = CacheHelpers.createAccessTokenEntity(
homeAccountId,
environment,
response.access_token,
this.config.auth.clientId,
tenantId,
scopes.printScopes(),
expiresOn,
extendedExpiresOn,
base64Decode
);
await this.storage.setAccessTokenCredential(
accessTokenEntity,
correlationId
);
return accessTokenEntity;
}
/**
* Helper function to load refresh tokens to msal-browser cache
* @param request
* @param response
* @param homeAccountId
* @param environment
* @returns `RefreshTokenEntity`
*/
private async loadRefreshToken(
response: ExternalTokenResponse,
homeAccountId: string,
environment: string,
correlationId: string
): Promise<RefreshTokenEntity | null> {
if (!response.refresh_token) {
this.logger.verbose(
"TokenCache - no refresh token found in response"
);
return null;
}
this.logger.verbose("TokenCache - loading refresh token");
const refreshTokenEntity = CacheHelpers.createRefreshTokenEntity(
homeAccountId,
environment,
response.refresh_token,
this.config.auth.clientId,
response.foci,
undefined, // userAssertionHash
response.refresh_token_expires_in
);
await this.storage.setRefreshTokenCredential(
refreshTokenEntity,
correlationId
);
return refreshTokenEntity;
}
/**
* Helper function to generate an `AuthenticationResult` for the result.
* @param request
* @param idTokenObj
* @param cacheRecord
* @param authority
* @returns `AuthenticationResult`
*/
private generateAuthenticationResult(
request: SilentRequest,
cacheRecord: CacheRecord & { account: AccountEntity },
idTokenClaims?: TokenClaims,
authority?: Authority
): AuthenticationResult {
let accessToken: string = "";
let responseScopes: Array<string> = [];
let expiresOn: Date | null = null;
let extExpiresOn: Date | undefined;
if (cacheRecord?.accessToken) {
accessToken = cacheRecord.accessToken.secret;
responseScopes = ScopeSet.fromString(
cacheRecord.accessToken.target
).asArray();
expiresOn = new Date(
Number(cacheRecord.accessToken.expiresOn) * 1000
);
extExpiresOn = new Date(
Number(cacheRecord.accessToken.extendedExpiresOn) * 1000
);
}
const accountEntity = cacheRecord.account;
return {
authority: authority ? authority.canonicalAuthority : "",
uniqueId: cacheRecord.account.localAccountId,
tenantId: cacheRecord.account.realm,
scopes: responseScopes,
account: accountEntity.getAccountInfo(),
idToken: cacheRecord.idToken?.secret || "",
idTokenClaims: idTokenClaims || {},
accessToken: accessToken,
fromCache: true,
expiresOn: expiresOn,
correlationId: request.correlationId || "",
requestId: "",
extExpiresOn: extExpiresOn,
familyId: cacheRecord.refreshToken?.familyId || "",
tokenType: cacheRecord?.accessToken?.tokenType || "",
state: request.state || "",
cloudGraphHostName: accountEntity.cloudGraphHostName || "",
msGraphHost: accountEntity.msGraphHost || "",
fromNativeBroker: false,
};
}
}
+406
View File
@@ -0,0 +1,406 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
SystemOptions,
LoggerOptions,
INetworkModule,
DEFAULT_SYSTEM_OPTIONS,
Constants,
ProtocolMode,
OIDCOptions,
ServerResponseType,
LogLevel,
StubbedNetworkModule,
AzureCloudInstance,
AzureCloudOptions,
ApplicationTelemetry,
createClientConfigurationError,
ClientConfigurationErrorCodes,
IPerformanceClient,
StubPerformanceClient,
Logger,
} from "@azure/msal-common/browser";
import {
BrowserCacheLocation,
BrowserConstants,
} from "../utils/BrowserConstants.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import { NavigationClient } from "../navigation/NavigationClient.js";
import { FetchClient } from "../network/FetchClient.js";
import * as BrowserUtils from "../utils/BrowserUtils.js";
// Default timeout for popup windows and iframes in milliseconds
export const DEFAULT_POPUP_TIMEOUT_MS = 60000;
export const DEFAULT_IFRAME_TIMEOUT_MS = 10000;
export const DEFAULT_REDIRECT_TIMEOUT_MS = 30000;
export const DEFAULT_NATIVE_BROKER_HANDSHAKE_TIMEOUT_MS = 2000;
/**
* Use this to configure the auth options in the Configuration object
*/
export type BrowserAuthOptions = {
/**
* Client ID of your app registered with our Application registration portal : https://portal.azure.com/#blade/Microsoft_AAD_IAM/ActiveDirectoryMenuBlade/RegisteredAppsPreview in Microsoft Identity Platform
*/
clientId: string;
/**
* You can configure a specific authority, defaults to " " or "https://login.microsoftonline.com/common"
*/
authority?: string;
/**
* An array of URIs that are known to be valid. Used in B2C scenarios.
*/
knownAuthorities?: Array<string>;
/**
* A string containing the cloud discovery response. Used in AAD scenarios.
*/
cloudDiscoveryMetadata?: string;
/**
* A string containing the .well-known/openid-configuration endpoint response
*/
authorityMetadata?: string;
/**
* The redirect URI where authentication responses can be received by your application. It must exactly match one of the redirect URIs registered in the Azure portal.
*/
redirectUri?: string;
/**
* The redirect URI where the window navigates after a successful logout.
*/
postLogoutRedirectUri?: string | null;
/**
* Boolean indicating whether to navigate to the original request URL after the auth server navigates to the redirect URL.
*/
navigateToLoginRequestUrl?: boolean;
/**
* Array of capabilities which will be added to the claims.access_token.xms_cc request property on every network request.
*/
clientCapabilities?: Array<string>;
/**
* Enum that represents the protocol that msal follows. Used for configuring proper endpoints.
*/
protocolMode?: ProtocolMode;
/**
* Enum that configures options for the OIDC protocol mode.
*/
OIDCOptions?: OIDCOptions;
/**
* Enum that represents the Azure Cloud to use.
*/
azureCloudOptions?: AzureCloudOptions;
/**
* Flag of whether to use the local metadata cache
*/
skipAuthorityMetadataCache?: boolean;
/**
* App supports nested app auth or not; defaults to
*
* @deprecated This flag is deprecated and will be removed in the next major version. createNestablePublicClientApplication should be used instead.
*/
supportsNestedAppAuth?: boolean;
/**
* Callback that will be passed the url that MSAL will navigate to in redirect flows. Returning false in the callback will stop navigation.
*/
onRedirectNavigate?: (url: string) => boolean | void;
/**
* Flag of whether the STS will send back additional parameters to specify where the tokens should be retrieved from.
*/
instanceAware?: boolean;
};
/** @internal */
export type InternalAuthOptions = Omit<
Required<BrowserAuthOptions>,
"onRedirectNavigate"
> & {
OIDCOptions: Required<OIDCOptions>;
onRedirectNavigate?: (url: string) => boolean | void;
};
/**
* Use this to configure the below cache configuration options:
*/
export type CacheOptions = {
/**
* Used to specify the cacheLocation user wants to set. Valid values are "localStorage", "sessionStorage" and "memoryStorage".
*/
cacheLocation?: BrowserCacheLocation | string;
/**
* Used to specify the temporaryCacheLocation user wants to set. Valid values are "localStorage", "sessionStorage" and "memoryStorage".
*/
temporaryCacheLocation?: BrowserCacheLocation | string;
/**
* If set, MSAL stores the auth request state required for validation of the auth flows in the browser cookies. By default this flag is set to false.
*/
storeAuthStateInCookie?: boolean;
/**
* If set, MSAL sets the "Secure" flag on cookies so they can only be sent over HTTPS. By default this flag is set to true.
* @deprecated This option will be removed in a future major version and all cookies set will include the Secure attribute.
*/
secureCookies?: boolean;
/**
* If set, MSAL will attempt to migrate cache entries from older versions on initialization. By default this flag is set to true if cacheLocation is localStorage, otherwise false.
*/
cacheMigrationEnabled?: boolean;
/**
* Flag that determines whether access tokens are stored based on requested claims
*/
claimsBasedCachingEnabled?: boolean;
};
export type BrowserSystemOptions = SystemOptions & {
/**
* Used to initialize the Logger object (See ClientConfiguration.ts)
*/
loggerOptions?: LoggerOptions;
/**
* Network interface implementation
*/
networkClient?: INetworkModule;
/**
* Override the methods used to navigate to other webpages. Particularly useful if you are using a client-side router
*/
navigationClient?: INavigationClient;
/**
* Sets the timeout for waiting for a response hash in a popup. Will take precedence over loadFrameTimeout if both are set.
*/
windowHashTimeout?: number;
/**
* Sets the timeout for waiting for a response hash in an iframe. Will take precedence over loadFrameTimeout if both are set.
*/
iframeHashTimeout?: number;
/**
* Sets the timeout for waiting for a response hash in an iframe or popup
*/
loadFrameTimeout?: number;
/**
* Maximum time the library should wait for a frame to load
* @deprecated This was previously needed for older browsers which are no longer supported by MSAL.js. This option will be removed in the next major version
*/
navigateFrameWait?: number;
/**
* Time to wait for redirection to occur before resolving promise
*/
redirectNavigationTimeout?: number;
/**
* Sets whether popups are opened asynchronously. By default, this flag is set to false. When set to false, blank popups are opened before anything else happens. When set to true, popups are opened when making the network request.
*/
asyncPopups?: boolean;
/**
* Flag to enable redirect opertaions when the app is rendered in an iframe (to support scenarios such as embedded B2C login).
*/
allowRedirectInIframe?: boolean;
/**
* Flag to enable native broker support (e.g. acquiring tokens from WAM on Windows, MacBroker on Mac)
*/
allowPlatformBroker?: boolean;
/**
* Sets the timeout for waiting for the native broker handshake to resolve
*/
nativeBrokerHandshakeTimeout?: number;
/**
* Sets the interval length in milliseconds for polling the location attribute in popup windows (default is 30ms)
*/
pollIntervalMilliseconds?: number;
};
/**
* Telemetry Options
*/
export type BrowserTelemetryOptions = {
/**
* Telemetry information sent on request
* - appName: Unique string name of an application
* - appVersion: Version of the application using MSAL
*/
application?: ApplicationTelemetry;
client?: IPerformanceClient;
};
/**
* This object allows you to configure important elements of MSAL functionality and is passed into the constructor of PublicClientApplication
*/
export type Configuration = {
/**
* This is where you configure auth elements like clientID, authority used for authenticating against the Microsoft Identity Platform
*/
auth: BrowserAuthOptions;
/**
* This is where you configure cache location and whether to store cache in cookies
*/
cache?: CacheOptions;
/**
* This is where you can configure the network client, logger, token renewal offset
*/
system?: BrowserSystemOptions;
/**
* This is where you can configure telemetry data and options
*/
telemetry?: BrowserTelemetryOptions;
};
/** @internal */
export type BrowserConfiguration = {
auth: InternalAuthOptions;
cache: Required<CacheOptions>;
system: Required<BrowserSystemOptions>;
telemetry: Required<BrowserTelemetryOptions>;
};
/**
* MSAL function that sets the default options when not explicitly configured from app developer
*
* @param auth
* @param cache
* @param system
*
* @returns Configuration object
*/
export function buildConfiguration(
{
auth: userInputAuth,
cache: userInputCache,
system: userInputSystem,
telemetry: userInputTelemetry,
}: Configuration,
isBrowserEnvironment: boolean
): BrowserConfiguration {
// Default auth options for browser
const DEFAULT_AUTH_OPTIONS: InternalAuthOptions = {
clientId: Constants.EMPTY_STRING,
authority: `${Constants.DEFAULT_AUTHORITY}`,
knownAuthorities: [],
cloudDiscoveryMetadata: Constants.EMPTY_STRING,
authorityMetadata: Constants.EMPTY_STRING,
redirectUri:
typeof window !== "undefined" ? BrowserUtils.getCurrentUri() : "",
postLogoutRedirectUri: Constants.EMPTY_STRING,
navigateToLoginRequestUrl: true,
clientCapabilities: [],
protocolMode: ProtocolMode.AAD,
OIDCOptions: {
serverResponseType: ServerResponseType.FRAGMENT,
defaultScopes: [
Constants.OPENID_SCOPE,
Constants.PROFILE_SCOPE,
Constants.OFFLINE_ACCESS_SCOPE,
],
},
azureCloudOptions: {
azureCloudInstance: AzureCloudInstance.None,
tenant: Constants.EMPTY_STRING,
},
skipAuthorityMetadataCache: false,
supportsNestedAppAuth: false,
instanceAware: false,
};
// Default cache options for browser
const DEFAULT_CACHE_OPTIONS: Required<CacheOptions> = {
cacheLocation: BrowserCacheLocation.SessionStorage,
temporaryCacheLocation: BrowserCacheLocation.SessionStorage,
storeAuthStateInCookie: false,
secureCookies: false,
// Default cache migration to true if cache location is localStorage since entries are preserved across tabs/windows. Migration has little to no benefit in sessionStorage and memoryStorage
cacheMigrationEnabled:
userInputCache &&
userInputCache.cacheLocation === BrowserCacheLocation.LocalStorage
? true
: false,
claimsBasedCachingEnabled: false,
};
// Default logger options for browser
const DEFAULT_LOGGER_OPTIONS: LoggerOptions = {
// eslint-disable-next-line @typescript-eslint/no-empty-function
loggerCallback: (): void => {
// allow users to not set logger call back
},
logLevel: LogLevel.Info,
piiLoggingEnabled: false,
};
// Default system options for browser
const DEFAULT_BROWSER_SYSTEM_OPTIONS: Required<BrowserSystemOptions> = {
...DEFAULT_SYSTEM_OPTIONS,
loggerOptions: DEFAULT_LOGGER_OPTIONS,
networkClient: isBrowserEnvironment
? new FetchClient()
: StubbedNetworkModule,
navigationClient: new NavigationClient(),
loadFrameTimeout: 0,
// If loadFrameTimeout is provided, use that as default.
windowHashTimeout:
userInputSystem?.loadFrameTimeout || DEFAULT_POPUP_TIMEOUT_MS,
iframeHashTimeout:
userInputSystem?.loadFrameTimeout || DEFAULT_IFRAME_TIMEOUT_MS,
navigateFrameWait: 0,
redirectNavigationTimeout: DEFAULT_REDIRECT_TIMEOUT_MS,
asyncPopups: false,
allowRedirectInIframe: false,
allowPlatformBroker: false,
nativeBrokerHandshakeTimeout:
userInputSystem?.nativeBrokerHandshakeTimeout ||
DEFAULT_NATIVE_BROKER_HANDSHAKE_TIMEOUT_MS,
pollIntervalMilliseconds: BrowserConstants.DEFAULT_POLL_INTERVAL_MS,
};
const providedSystemOptions: Required<BrowserSystemOptions> = {
...DEFAULT_BROWSER_SYSTEM_OPTIONS,
...userInputSystem,
loggerOptions: userInputSystem?.loggerOptions || DEFAULT_LOGGER_OPTIONS,
};
const DEFAULT_TELEMETRY_OPTIONS: Required<BrowserTelemetryOptions> = {
application: {
appName: Constants.EMPTY_STRING,
appVersion: Constants.EMPTY_STRING,
},
client: new StubPerformanceClient(),
};
// Throw an error if user has set OIDCOptions without being in OIDC protocol mode
if (
userInputAuth?.protocolMode !== ProtocolMode.OIDC &&
userInputAuth?.OIDCOptions
) {
const logger = new Logger(providedSystemOptions.loggerOptions);
logger.warning(
JSON.stringify(
createClientConfigurationError(
ClientConfigurationErrorCodes.cannotSetOIDCOptions
)
)
);
}
// Throw an error if user has set allowPlatformBroker to true without being in AAD protocol mode
if (
userInputAuth?.protocolMode &&
userInputAuth.protocolMode !== ProtocolMode.AAD &&
providedSystemOptions?.allowPlatformBroker
) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.cannotAllowPlatformBroker
);
}
const overlayedConfig: BrowserConfiguration = {
auth: {
...DEFAULT_AUTH_OPTIONS,
...userInputAuth,
OIDCOptions: {
...DEFAULT_AUTH_OPTIONS.OIDCOptions,
...userInputAuth?.OIDCOptions,
},
},
cache: { ...DEFAULT_CACHE_OPTIONS, ...userInputCache },
system: providedSystemOptions,
telemetry: { ...DEFAULT_TELEMETRY_OPTIONS, ...userInputTelemetry },
};
return overlayedConfig;
}
+42
View File
@@ -0,0 +1,42 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { NestedAppOperatingContext } from "../operatingcontext/NestedAppOperatingContext.js";
import { StandardOperatingContext } from "../operatingcontext/StandardOperatingContext.js";
import { IController } from "./IController.js";
import { Configuration } from "../config/Configuration.js";
import { StandardController } from "./StandardController.js";
import { NestedAppAuthController } from "./NestedAppAuthController.js";
import { InitializeApplicationRequest } from "../request/InitializeApplicationRequest.js";
export async function createV3Controller(
config: Configuration,
request?: InitializeApplicationRequest
): Promise<IController> {
const standard = new StandardOperatingContext(config);
await standard.initialize();
return StandardController.createController(standard, request);
}
export async function createController(
config: Configuration
): Promise<IController | null> {
const standard = new StandardOperatingContext(config);
const nestedApp = new NestedAppOperatingContext(config);
const operatingContexts = [standard.initialize(), nestedApp.initialize()];
await Promise.all(operatingContexts);
if (nestedApp.isAvailable() && config.auth.supportsNestedAppAuth) {
return NestedAppAuthController.createController(nestedApp);
} else if (standard.isAvailable()) {
return StandardController.createController(standard);
} else {
// Since neither of the actual operating contexts are available keep the UnknownOperatingContextController
return null;
}
}
+124
View File
@@ -0,0 +1,124 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AccountInfo,
Logger,
PerformanceCallbackFunction,
IPerformanceClient,
AccountFilter,
} from "@azure/msal-common/browser";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { SilentRequest } from "../request/SilentRequest.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import { EndSessionRequest } from "../request/EndSessionRequest.js";
import { ApiId, WrapperSKU } from "../utils/BrowserConstants.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import { EndSessionPopupRequest } from "../request/EndSessionPopupRequest.js";
import { ITokenCache } from "../cache/ITokenCache.js";
import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js";
import { BrowserConfiguration } from "../config/Configuration.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { EventCallbackFunction } from "../event/EventMessage.js";
import { ClearCacheRequest } from "../request/ClearCacheRequest.js";
import { InitializeApplicationRequest } from "../request/InitializeApplicationRequest.js";
import { EventType } from "../event/EventType.js";
export interface IController {
// TODO: Make request mandatory in the next major version?
initialize(request?: InitializeApplicationRequest): Promise<void>;
acquireTokenPopup(request: PopupRequest): Promise<AuthenticationResult>;
acquireTokenRedirect(request: RedirectRequest): Promise<void>;
acquireTokenSilent(
silentRequest: SilentRequest
): Promise<AuthenticationResult>;
acquireTokenByCode(
request: AuthorizationCodeRequest
): Promise<AuthenticationResult>;
acquireTokenNative(
request: PopupRequest | SilentRequest | SsoSilentRequest,
apiId: ApiId,
accountId?: string
): Promise<AuthenticationResult>;
addEventCallback(
callback: EventCallbackFunction,
eventTypes?: Array<EventType>
): string | null;
removeEventCallback(callbackId: string): void;
addPerformanceCallback(callback: PerformanceCallbackFunction): string;
removePerformanceCallback(callbackId: string): boolean;
enableAccountStorageEvents(): void;
disableAccountStorageEvents(): void;
getAccount(accountFilter: AccountFilter): AccountInfo | null;
getAccountByHomeId(homeAccountId: string): AccountInfo | null;
getAccountByLocalId(localId: string): AccountInfo | null;
getAccountByUsername(userName: string): AccountInfo | null;
getAllAccounts(accountFilter?: AccountFilter): AccountInfo[];
handleRedirectPromise(hash?: string): Promise<AuthenticationResult | null>;
loginPopup(request?: PopupRequest): Promise<AuthenticationResult>;
loginRedirect(request?: RedirectRequest): Promise<void>;
logout(logoutRequest?: EndSessionRequest): Promise<void>;
logoutRedirect(logoutRequest?: EndSessionRequest): Promise<void>;
logoutPopup(logoutRequest?: EndSessionPopupRequest): Promise<void>;
clearCache(logoutRequest?: ClearCacheRequest): Promise<void>;
ssoSilent(request: SsoSilentRequest): Promise<AuthenticationResult>;
getTokenCache(): ITokenCache;
getLogger(): Logger;
setLogger(logger: Logger): void;
setActiveAccount(account: AccountInfo | null): void;
getActiveAccount(): AccountInfo | null;
initializeWrapperLibrary(sku: WrapperSKU, version: string): void;
setNavigationClient(navigationClient: INavigationClient): void;
/** @internal */
getConfiguration(): BrowserConfiguration;
hydrateCache(
result: AuthenticationResult,
request:
| SilentRequest
| SsoSilentRequest
| RedirectRequest
| PopupRequest
): Promise<void>;
/** @internal */
isBrowserEnv(): boolean;
/** @internal */
getPerformanceClient(): IPerformanceClient;
}
@@ -0,0 +1,884 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
CommonAuthorizationUrlRequest,
CommonSilentFlowRequest,
PerformanceCallbackFunction,
AccountInfo,
Logger,
ICrypto,
IPerformanceClient,
DEFAULT_CRYPTO_IMPLEMENTATION,
PerformanceEvents,
TimeUtils,
buildStaticAuthorityOptions,
AccountEntity,
OIDC_DEFAULT_SCOPES,
BaseAuthRequest,
AccountFilter,
AuthError,
} from "@azure/msal-common/browser";
import { ITokenCache } from "../cache/ITokenCache.js";
import { BrowserConfiguration } from "../config/Configuration.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js";
import { EndSessionPopupRequest } from "../request/EndSessionPopupRequest.js";
import { EndSessionRequest } from "../request/EndSessionRequest.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { SilentRequest } from "../request/SilentRequest.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import {
ApiId,
WrapperSKU,
InteractionType,
DEFAULT_REQUEST,
CacheLookupPolicy,
} from "../utils/BrowserConstants.js";
import { IController } from "./IController.js";
import { NestedAppOperatingContext } from "../operatingcontext/NestedAppOperatingContext.js";
import { IBridgeProxy } from "../naa/IBridgeProxy.js";
import { CryptoOps } from "../crypto/CryptoOps.js";
import { NestedAppAuthAdapter } from "../naa/mapping/NestedAppAuthAdapter.js";
import { NestedAppAuthError } from "../error/NestedAppAuthError.js";
import { EventHandler } from "../event/EventHandler.js";
import { EventType } from "../event/EventType.js";
import { EventCallbackFunction, EventError } from "../event/EventMessage.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import {
BrowserCacheManager,
DEFAULT_BROWSER_CACHE_MANAGER,
} from "../cache/BrowserCacheManager.js";
import { ClearCacheRequest } from "../request/ClearCacheRequest.js";
import * as AccountManager from "../cache/AccountManager.js";
import { AccountContext } from "../naa/BridgeAccountContext.js";
import { InitializeApplicationRequest } from "../request/InitializeApplicationRequest.js";
import { createNewGuid } from "../crypto/BrowserCrypto.js";
export class NestedAppAuthController implements IController {
// OperatingContext
protected readonly operatingContext: NestedAppOperatingContext;
// BridgeProxy
protected readonly bridgeProxy: IBridgeProxy;
// Crypto interface implementation
protected readonly browserCrypto: ICrypto;
// Input configuration by developer/user
protected readonly config: BrowserConfiguration;
// Storage interface implementation
protected readonly browserStorage!: BrowserCacheManager;
// Logger
protected logger: Logger;
// Performance telemetry client
protected readonly performanceClient: IPerformanceClient;
// EventHandler
protected readonly eventHandler: EventHandler;
// NestedAppAuthAdapter
protected readonly nestedAppAuthAdapter: NestedAppAuthAdapter;
// currentAccount for NAA apps
protected currentAccountContext: AccountContext | null;
constructor(operatingContext: NestedAppOperatingContext) {
this.operatingContext = operatingContext;
const proxy = this.operatingContext.getBridgeProxy();
if (proxy !== undefined) {
this.bridgeProxy = proxy;
} else {
throw new Error("unexpected: bridgeProxy is undefined");
}
// Set the configuration.
this.config = operatingContext.getConfig();
// Initialize logger
this.logger = this.operatingContext.getLogger();
// Initialize performance client
this.performanceClient = this.config.telemetry.client;
// Initialize the crypto class.
this.browserCrypto = operatingContext.isBrowserEnvironment()
? new CryptoOps(this.logger, this.performanceClient, true)
: DEFAULT_CRYPTO_IMPLEMENTATION;
this.eventHandler = new EventHandler(this.logger);
// Initialize the browser storage class.
this.browserStorage = this.operatingContext.isBrowserEnvironment()
? new BrowserCacheManager(
this.config.auth.clientId,
this.config.cache,
this.browserCrypto,
this.logger,
this.performanceClient,
this.eventHandler,
buildStaticAuthorityOptions(this.config.auth)
)
: DEFAULT_BROWSER_CACHE_MANAGER(
this.config.auth.clientId,
this.logger,
this.performanceClient,
this.eventHandler
);
this.nestedAppAuthAdapter = new NestedAppAuthAdapter(
this.config.auth.clientId,
this.config.auth.clientCapabilities,
this.browserCrypto,
this.logger
);
// Set the active account if available
const accountContext = this.bridgeProxy.getAccountContext();
this.currentAccountContext = accountContext ? accountContext : null;
}
/**
* Factory function to create a new instance of NestedAppAuthController
* @param operatingContext
* @returns Promise<IController>
*/
static async createController(
operatingContext: NestedAppOperatingContext
): Promise<IController> {
const controller = new NestedAppAuthController(operatingContext);
return Promise.resolve(controller);
}
/**
* Specific implementation of initialize function for NestedAppAuthController
* @returns
*/
async initialize(request?: InitializeApplicationRequest): Promise<void> {
const initCorrelationId = request?.correlationId || createNewGuid();
await this.browserStorage.initialize(initCorrelationId);
return Promise.resolve();
}
/**
* Validate the incoming request and add correlationId if not present
* @param request
* @returns
*/
private ensureValidRequest<
T extends
| SsoSilentRequest
| SilentRequest
| PopupRequest
| RedirectRequest
>(request: T): T {
if (request?.correlationId) {
return request;
}
return {
...request,
correlationId: this.browserCrypto.createNewGuid(),
};
}
/**
* Internal implementation of acquireTokenInteractive flow
* @param request
* @returns
*/
private async acquireTokenInteractive(
request: PopupRequest | RedirectRequest
): Promise<AuthenticationResult> {
const validRequest = this.ensureValidRequest(request);
this.eventHandler.emitEvent(
EventType.ACQUIRE_TOKEN_START,
InteractionType.Popup,
validRequest
);
const atPopupMeasurement = this.performanceClient.startMeasurement(
PerformanceEvents.AcquireTokenPopup,
validRequest.correlationId
);
atPopupMeasurement?.add({ nestedAppAuthRequest: true });
try {
const naaRequest =
this.nestedAppAuthAdapter.toNaaTokenRequest(validRequest);
const reqTimestamp = TimeUtils.nowSeconds();
const response = await this.bridgeProxy.getTokenInteractive(
naaRequest
);
const result: AuthenticationResult = {
...this.nestedAppAuthAdapter.fromNaaTokenResponse(
naaRequest,
response,
reqTimestamp
),
};
// cache the tokens in the response
await this.hydrateCache(result, request);
// cache the account context in memory after successful token fetch
this.currentAccountContext = {
homeAccountId: result.account.homeAccountId,
environment: result.account.environment,
tenantId: result.account.tenantId,
};
this.eventHandler.emitEvent(
EventType.ACQUIRE_TOKEN_SUCCESS,
InteractionType.Popup,
result
);
atPopupMeasurement.add({
accessTokenSize: result.accessToken.length,
idTokenSize: result.idToken.length,
});
atPopupMeasurement.end({
success: true,
requestId: result.requestId,
});
return result;
} catch (e) {
const error =
e instanceof AuthError
? e
: this.nestedAppAuthAdapter.fromBridgeError(e);
this.eventHandler.emitEvent(
EventType.ACQUIRE_TOKEN_FAILURE,
InteractionType.Popup,
null,
e as EventError
);
atPopupMeasurement.end(
{
success: false,
},
e
);
throw error;
}
}
/**
* Internal implementation of acquireTokenSilent flow
* @param request
* @returns
*/
private async acquireTokenSilentInternal(
request: SilentRequest
): Promise<AuthenticationResult> {
const validRequest = this.ensureValidRequest(request);
this.eventHandler.emitEvent(
EventType.ACQUIRE_TOKEN_START,
InteractionType.Silent,
validRequest
);
// Look for tokens in the cache first
const result = await this.acquireTokenFromCache(validRequest);
if (result) {
this.eventHandler.emitEvent(
EventType.ACQUIRE_TOKEN_SUCCESS,
InteractionType.Silent,
result
);
return result;
}
// proceed with acquiring tokens via the host
const ssoSilentMeasurement = this.performanceClient.startMeasurement(
PerformanceEvents.SsoSilent,
validRequest.correlationId
);
ssoSilentMeasurement?.increment({
visibilityChangeCount: 0,
});
ssoSilentMeasurement?.add({
nestedAppAuthRequest: true,
});
try {
const naaRequest =
this.nestedAppAuthAdapter.toNaaTokenRequest(validRequest);
const reqTimestamp = TimeUtils.nowSeconds();
const response = await this.bridgeProxy.getTokenSilent(naaRequest);
const result: AuthenticationResult =
this.nestedAppAuthAdapter.fromNaaTokenResponse(
naaRequest,
response,
reqTimestamp
);
// cache the tokens in the response
await this.hydrateCache(result, request);
// cache the account context in memory after successful token fetch
this.currentAccountContext = {
homeAccountId: result.account.homeAccountId,
environment: result.account.environment,
tenantId: result.account.tenantId,
};
this.eventHandler.emitEvent(
EventType.ACQUIRE_TOKEN_SUCCESS,
InteractionType.Silent,
result
);
ssoSilentMeasurement?.add({
accessTokenSize: result.accessToken.length,
idTokenSize: result.idToken.length,
});
ssoSilentMeasurement?.end({
success: true,
requestId: result.requestId,
});
return result;
} catch (e) {
const error =
e instanceof AuthError
? e
: this.nestedAppAuthAdapter.fromBridgeError(e);
this.eventHandler.emitEvent(
EventType.ACQUIRE_TOKEN_FAILURE,
InteractionType.Silent,
null,
e as EventError
);
ssoSilentMeasurement?.end(
{
success: false,
},
e
);
throw error;
}
}
/**
* acquires tokens from cache
* @param request
* @returns
*/
private async acquireTokenFromCache(
request: SilentRequest
): Promise<AuthenticationResult | null> {
const atsMeasurement = this.performanceClient.startMeasurement(
PerformanceEvents.AcquireTokenSilent,
request.correlationId
);
atsMeasurement?.add({
nestedAppAuthRequest: true,
});
// if the request has claims, we cannot look up in the cache
if (request.claims) {
this.logger.verbose(
"Claims are present in the request, skipping cache lookup"
);
return null;
}
// if the request has forceRefresh, we cannot look up in the cache
if (request.forceRefresh) {
this.logger.verbose(
"forceRefresh is set to true, skipping cache lookup"
);
return null;
}
// respect cache lookup policy
let result: AuthenticationResult | null = null;
if (!request.cacheLookupPolicy) {
request.cacheLookupPolicy = CacheLookupPolicy.Default;
}
switch (request.cacheLookupPolicy) {
case CacheLookupPolicy.Default:
case CacheLookupPolicy.AccessToken:
case CacheLookupPolicy.AccessTokenAndRefreshToken:
result = await this.acquireTokenFromCacheInternal(request);
break;
default:
return null;
}
if (result) {
this.eventHandler.emitEvent(
EventType.ACQUIRE_TOKEN_SUCCESS,
InteractionType.Silent,
result
);
atsMeasurement?.add({
accessTokenSize: result?.accessToken.length,
idTokenSize: result?.idToken.length,
});
atsMeasurement?.end({
success: true,
});
return result;
}
this.logger.error(
"Cached tokens are not found for the account, proceeding with silent token request."
);
this.eventHandler.emitEvent(
EventType.ACQUIRE_TOKEN_FAILURE,
InteractionType.Silent,
null
);
atsMeasurement?.end({
success: false,
});
return null;
}
/**
*
* @param request
* @returns
*/
private async acquireTokenFromCacheInternal(
request: SilentRequest
): Promise<AuthenticationResult | null> {
// always prioritize the account context from the bridge
const accountContext =
this.bridgeProxy.getAccountContext() || this.currentAccountContext;
let currentAccount: AccountInfo | null = null;
if (accountContext) {
currentAccount = AccountManager.getAccount(
accountContext,
this.logger,
this.browserStorage
);
}
// fall back to brokering if no cached account is found
if (!currentAccount) {
this.logger.verbose(
"No active account found, falling back to the host"
);
return Promise.resolve(null);
}
this.logger.verbose(
"active account found, attempting to acquire token silently"
);
const authRequest: BaseAuthRequest = {
...request,
correlationId:
request.correlationId || this.browserCrypto.createNewGuid(),
authority: request.authority || currentAccount.environment,
scopes: request.scopes?.length
? request.scopes
: [...OIDC_DEFAULT_SCOPES],
};
// fetch access token and check for expiry
const tokenKeys = this.browserStorage.getTokenKeys();
const cachedAccessToken = this.browserStorage.getAccessToken(
currentAccount,
authRequest,
tokenKeys,
currentAccount.tenantId,
this.performanceClient,
authRequest.correlationId
);
// If there is no access token, log it and return null
if (!cachedAccessToken) {
this.logger.verbose("No cached access token found");
return Promise.resolve(null);
} else if (
TimeUtils.wasClockTurnedBack(cachedAccessToken.cachedAt) ||
TimeUtils.isTokenExpired(
cachedAccessToken.expiresOn,
this.config.system.tokenRenewalOffsetSeconds
)
) {
this.logger.verbose("Cached access token has expired");
return Promise.resolve(null);
}
const cachedIdToken = this.browserStorage.getIdToken(
currentAccount,
tokenKeys,
currentAccount.tenantId,
this.performanceClient,
authRequest.correlationId
);
if (!cachedIdToken) {
this.logger.verbose("No cached id token found");
return Promise.resolve(null);
}
return this.nestedAppAuthAdapter.toAuthenticationResultFromCache(
currentAccount,
cachedIdToken,
cachedAccessToken,
authRequest,
authRequest.correlationId
);
}
/**
* acquireTokenPopup flow implementation
* @param request
* @returns
*/
async acquireTokenPopup(
request: PopupRequest
): Promise<AuthenticationResult> {
return this.acquireTokenInteractive(request);
}
/**
* acquireTokenRedirect flow is not supported in nested app auth
* @param request
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
acquireTokenRedirect(request: RedirectRequest): Promise<void> {
throw NestedAppAuthError.createUnsupportedError();
}
/**
* acquireTokenSilent flow implementation
* @param silentRequest
* @returns
*/
async acquireTokenSilent(
silentRequest: SilentRequest
): Promise<AuthenticationResult> {
return this.acquireTokenSilentInternal(silentRequest);
}
/**
* Hybrid flow is not currently supported in nested app auth
* @param request
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
acquireTokenByCode(
request: AuthorizationCodeRequest // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<AuthenticationResult> {
throw NestedAppAuthError.createUnsupportedError();
}
/**
* acquireTokenNative flow is not currently supported in nested app auth
* @param request
* @param apiId
* @param accountId
*/
acquireTokenNative(
request: // eslint-disable-line @typescript-eslint/no-unused-vars
| SilentRequest
| Partial<
Omit<
CommonAuthorizationUrlRequest,
| "requestedClaimsHash"
| "responseMode"
| "codeChallenge"
| "codeChallengeMethod"
| "platformBroker"
>
>
| PopupRequest,
apiId: ApiId, // eslint-disable-line @typescript-eslint/no-unused-vars
accountId?: string | undefined // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<AuthenticationResult> {
throw NestedAppAuthError.createUnsupportedError();
}
/**
* acquireTokenByRefreshToken flow is not currently supported in nested app auth
* @param commonRequest
* @param silentRequest
*/
acquireTokenByRefreshToken(
commonRequest: CommonSilentFlowRequest, // eslint-disable-line @typescript-eslint/no-unused-vars
silentRequest: SilentRequest // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<AuthenticationResult> {
throw NestedAppAuthError.createUnsupportedError();
}
/**
* Adds event callbacks to array
* @param callback
* @param eventTypes
*/
addEventCallback(
callback: EventCallbackFunction,
eventTypes?: Array<EventType>
): string | null {
return this.eventHandler.addEventCallback(callback, eventTypes);
}
/**
* Removes callback with provided id from callback array
* @param callbackId
*/
removeEventCallback(callbackId: string): void {
this.eventHandler.removeEventCallback(callbackId);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
addPerformanceCallback(callback: PerformanceCallbackFunction): string {
throw NestedAppAuthError.createUnsupportedError();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
removePerformanceCallback(callbackId: string): boolean {
throw NestedAppAuthError.createUnsupportedError();
}
enableAccountStorageEvents(): void {
throw NestedAppAuthError.createUnsupportedError();
}
disableAccountStorageEvents(): void {
throw NestedAppAuthError.createUnsupportedError();
}
// #region Account APIs
/**
* Returns all the accounts in the cache that match the optional filter. If no filter is provided, all accounts are returned.
* @param accountFilter - (Optional) filter to narrow down the accounts returned
* @returns Array of AccountInfo objects in cache
*/
getAllAccounts(accountFilter?: AccountFilter): AccountInfo[] {
return AccountManager.getAllAccounts(
this.logger,
this.browserStorage,
this.isBrowserEnv(),
accountFilter
);
}
/**
* Returns the first account found in the cache that matches the account filter passed in.
* @param accountFilter
* @returns The first account found in the cache matching the provided filter or null if no account could be found.
*/
getAccount(accountFilter: AccountFilter): AccountInfo | null {
return AccountManager.getAccount(
accountFilter,
this.logger,
this.browserStorage
);
}
/**
* Returns the signed in account matching username.
* (the account object is created at the time of successful login)
* or null when no matching account is found.
* This API is provided for convenience but getAccountById should be used for best reliability
* @param username
* @returns The account object stored in MSAL
*/
getAccountByUsername(username: string): AccountInfo | null {
return AccountManager.getAccountByUsername(
username,
this.logger,
this.browserStorage
);
}
/**
* Returns the signed in account matching homeAccountId.
* (the account object is created at the time of successful login)
* or null when no matching account is found
* @param homeAccountId
* @returns The account object stored in MSAL
*/
getAccountByHomeId(homeAccountId: string): AccountInfo | null {
return AccountManager.getAccountByHomeId(
homeAccountId,
this.logger,
this.browserStorage
);
}
/**
* Returns the signed in account matching localAccountId.
* (the account object is created at the time of successful login)
* or null when no matching account is found
* @param localAccountId
* @returns The account object stored in MSAL
*/
getAccountByLocalId(localAccountId: string): AccountInfo | null {
return AccountManager.getAccountByLocalId(
localAccountId,
this.logger,
this.browserStorage
);
}
/**
* Sets the account to use as the active account. If no account is passed to the acquireToken APIs, then MSAL will use this active account.
* @param account
*/
setActiveAccount(account: AccountInfo | null): void {
/*
* StandardController uses this to allow the developer to set the active account
* in the nested app auth scenario the active account is controlled by the app hosting the nested app
*/
return AccountManager.setActiveAccount(account, this.browserStorage);
}
/**
* Gets the currently active account
*/
getActiveAccount(): AccountInfo | null {
return AccountManager.getActiveAccount(this.browserStorage);
}
// #endregion
handleRedirectPromise(
hash?: string | undefined // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<AuthenticationResult | null> {
return Promise.resolve(null);
}
loginPopup(
request?: PopupRequest | undefined // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<AuthenticationResult> {
return this.acquireTokenInteractive(request || DEFAULT_REQUEST);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loginRedirect(request?: RedirectRequest | undefined): Promise<void> {
throw NestedAppAuthError.createUnsupportedError();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
logout(logoutRequest?: EndSessionRequest | undefined): Promise<void> {
throw NestedAppAuthError.createUnsupportedError();
}
logoutRedirect(
logoutRequest?: EndSessionRequest | undefined // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<void> {
throw NestedAppAuthError.createUnsupportedError();
}
logoutPopup(
logoutRequest?: EndSessionPopupRequest | undefined // eslint-disable-line @typescript-eslint/no-unused-vars
): Promise<void> {
throw NestedAppAuthError.createUnsupportedError();
}
ssoSilent(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
request: Partial<
Omit<
CommonAuthorizationUrlRequest,
| "requestedClaimsHash"
| "responseMode"
| "codeChallenge"
| "codeChallengeMethod"
| "platformBroker"
>
>
): Promise<AuthenticationResult> {
return this.acquireTokenSilentInternal(request as SilentRequest);
}
getTokenCache(): ITokenCache {
throw NestedAppAuthError.createUnsupportedError();
}
/**
* Returns the logger instance
*/
public getLogger(): Logger {
return this.logger;
}
/**
* Replaces the default logger set in configurations with new Logger with new configurations
* @param logger Logger instance
*/
setLogger(logger: Logger): void {
this.logger = logger;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
initializeWrapperLibrary(sku: WrapperSKU, version: string): void {
/*
* Standard controller uses this to set the sku and version of the wrapper library in the storage
* we do nothing here
*/
return;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setNavigationClient(navigationClient: INavigationClient): void {
this.logger.warning(
"setNavigationClient is not supported in nested app auth"
);
}
getConfiguration(): BrowserConfiguration {
return this.config;
}
isBrowserEnv(): boolean {
return this.operatingContext.isBrowserEnvironment();
}
getBrowserCrypto(): ICrypto {
return this.browserCrypto;
}
getPerformanceClient(): IPerformanceClient {
throw NestedAppAuthError.createUnsupportedError();
}
getRedirectResponse(): Map<string, Promise<AuthenticationResult | null>> {
throw NestedAppAuthError.createUnsupportedError();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async clearCache(logoutRequest?: ClearCacheRequest): Promise<void> {
throw NestedAppAuthError.createUnsupportedError();
}
async hydrateCache(
result: AuthenticationResult,
request:
| SilentRequest
| SsoSilentRequest
| RedirectRequest
| PopupRequest
): Promise<void> {
this.logger.verbose("hydrateCache called");
const accountEntity = AccountEntity.createFromAccountInfo(
result.account,
result.cloudGraphHostName,
result.msGraphHost
);
await this.browserStorage.setAccount(
accountEntity,
result.correlationId
);
return this.browserStorage.hydrateCache(result, request);
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,383 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
CommonAuthorizationUrlRequest,
CommonSilentFlowRequest,
PerformanceCallbackFunction,
AccountInfo,
Logger,
ICrypto,
IPerformanceClient,
DEFAULT_CRYPTO_IMPLEMENTATION,
AccountFilter,
} from "@azure/msal-common/browser";
import { ITokenCache } from "../cache/ITokenCache.js";
import { BrowserConfiguration } from "../config/Configuration.js";
import {
BrowserCacheManager,
DEFAULT_BROWSER_CACHE_MANAGER,
} from "../cache/BrowserCacheManager.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js";
import { EndSessionPopupRequest } from "../request/EndSessionPopupRequest.js";
import { EndSessionRequest } from "../request/EndSessionRequest.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { SilentRequest } from "../request/SilentRequest.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { ApiId, WrapperSKU } from "../utils/BrowserConstants.js";
import { IController } from "./IController.js";
import { UnknownOperatingContext } from "../operatingcontext/UnknownOperatingContext.js";
import { CryptoOps } from "../crypto/CryptoOps.js";
import {
blockAPICallsBeforeInitialize,
blockNonBrowserEnvironment,
} from "../utils/BrowserUtils.js";
import { EventCallbackFunction } from "../event/EventMessage.js";
import { ClearCacheRequest } from "../request/ClearCacheRequest.js";
import { EventType } from "../event/EventType.js";
import { EventHandler } from "../event/EventHandler.js";
/**
* UnknownOperatingContextController class
*
* - Until initialize method is called, this controller is the default
* - AFter initialize method is called, this controller will be swapped out for the appropriate controller
* if the operating context can be determined; otherwise this controller will continued be used
*
* - Why do we have this? We don't want to dynamically import (download) all of the code in StandardController if we don't need to.
*
* - Only includes implementation for getAccounts and handleRedirectPromise
* - All other methods are will throw initialization error (because either initialize method or the factory method were not used)
* - This controller is necessary for React Native wrapper, server side rendering and any other scenario where we don't have a DOM
*
*/
export class UnknownOperatingContextController implements IController {
// OperatingContext
protected readonly operatingContext: UnknownOperatingContext;
// Logger
protected logger: Logger;
// Storage interface implementation
protected readonly browserStorage: BrowserCacheManager;
// Input configuration by developer/user
protected readonly config: BrowserConfiguration;
// Performance telemetry client
protected readonly performanceClient: IPerformanceClient;
// Event handler
private readonly eventHandler: EventHandler;
// Crypto interface implementation
protected readonly browserCrypto: ICrypto;
// Flag to indicate if in browser environment
protected isBrowserEnvironment: boolean;
// Flag representing whether or not the initialize API has been called and completed
protected initialized: boolean = false;
constructor(operatingContext: UnknownOperatingContext) {
this.operatingContext = operatingContext;
this.isBrowserEnvironment =
this.operatingContext.isBrowserEnvironment();
this.config = operatingContext.getConfig();
this.logger = operatingContext.getLogger();
// Initialize performance client
this.performanceClient = this.config.telemetry.client;
// Initialize the crypto class.
this.browserCrypto = this.isBrowserEnvironment
? new CryptoOps(this.logger, this.performanceClient)
: DEFAULT_CRYPTO_IMPLEMENTATION;
this.eventHandler = new EventHandler(this.logger);
// Initialize the browser storage class.
this.browserStorage = this.isBrowserEnvironment
? new BrowserCacheManager(
this.config.auth.clientId,
this.config.cache,
this.browserCrypto,
this.logger,
this.performanceClient,
this.eventHandler,
undefined
)
: DEFAULT_BROWSER_CACHE_MANAGER(
this.config.auth.clientId,
this.logger,
this.performanceClient,
this.eventHandler
);
}
getBrowserStorage(): BrowserCacheManager {
return this.browserStorage;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getAccount(accountFilter: AccountFilter): AccountInfo | null {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getAccountByHomeId(homeAccountId: string): AccountInfo | null {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getAccountByLocalId(localAccountId: string): AccountInfo | null {
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
getAccountByUsername(username: string): AccountInfo | null {
return null;
}
getAllAccounts(): AccountInfo[] {
return [];
}
initialize(): Promise<void> {
this.initialized = true;
return Promise.resolve();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
acquireTokenPopup(request: PopupRequest): Promise<AuthenticationResult> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Promise<AuthenticationResult>;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
acquireTokenRedirect(request: RedirectRequest): Promise<void> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return Promise.resolve();
}
acquireTokenSilent(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
silentRequest: SilentRequest
): Promise<AuthenticationResult> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Promise<AuthenticationResult>;
}
acquireTokenByCode(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
request: AuthorizationCodeRequest
): Promise<AuthenticationResult> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Promise<AuthenticationResult>;
}
acquireTokenNative(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
request:
| PopupRequest
| SilentRequest
| Partial<
Omit<
CommonAuthorizationUrlRequest,
| "responseMode"
| "codeChallenge"
| "codeChallengeMethod"
| "requestedClaimsHash"
| "platformBroker"
>
>,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
apiId: ApiId,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
accountId?: string | undefined
): Promise<AuthenticationResult> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Promise<AuthenticationResult>;
}
acquireTokenByRefreshToken(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
commonRequest: CommonSilentFlowRequest,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
silentRequest: SilentRequest
): Promise<AuthenticationResult> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Promise<AuthenticationResult>;
}
addEventCallback(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
callback: EventCallbackFunction,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
eventTypes?: Array<EventType>
): string | null {
return null;
}
removeEventCallback(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
callbackId: string
): void {}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
addPerformanceCallback(callback: PerformanceCallbackFunction): string {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return "";
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
removePerformanceCallback(callbackId: string): boolean {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return true;
}
enableAccountStorageEvents(): void {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
}
disableAccountStorageEvents(): void {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
}
handleRedirectPromise(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
hash?: string | undefined
): Promise<AuthenticationResult | null> {
blockAPICallsBeforeInitialize(this.initialized);
return Promise.resolve(null);
}
loginPopup(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
request?: PopupRequest | undefined
): Promise<AuthenticationResult> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Promise<AuthenticationResult>;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
loginRedirect(request?: RedirectRequest | undefined): Promise<void> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Promise<void>;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
logout(logoutRequest?: EndSessionRequest | undefined): Promise<void> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Promise<void>;
}
logoutRedirect(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
logoutRequest?: EndSessionRequest | undefined
): Promise<void> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Promise<void>;
}
logoutPopup(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
logoutRequest?: EndSessionPopupRequest | undefined
): Promise<void> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Promise<void>;
}
ssoSilent(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
request: Partial<
Omit<
CommonAuthorizationUrlRequest,
| "responseMode"
| "codeChallenge"
| "codeChallengeMethod"
| "requestedClaimsHash"
| "platformBroker"
>
>
): Promise<AuthenticationResult> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Promise<AuthenticationResult>;
}
getTokenCache(): ITokenCache {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as ITokenCache;
}
getLogger(): Logger {
return this.logger;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setLogger(logger: Logger): void {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setActiveAccount(account: AccountInfo | null): void {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
}
getActiveAccount(): AccountInfo | null {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return null;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
initializeWrapperLibrary(sku: WrapperSKU, version: string): void {
this.browserStorage.setWrapperMetadata(sku, version);
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
setNavigationClient(navigationClient: INavigationClient): void {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
}
getConfiguration(): BrowserConfiguration {
return this.config;
}
isBrowserEnv(): boolean {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return true;
}
getBrowserCrypto(): ICrypto {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as ICrypto;
}
getPerformanceClient(): IPerformanceClient {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as IPerformanceClient;
}
getRedirectResponse(): Map<string, Promise<AuthenticationResult | null>> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
return {} as Map<string, Promise<AuthenticationResult | null>>;
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async clearCache(logoutRequest?: ClearCacheRequest): Promise<void> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
async hydrateCache(
// eslint-disable-next-line @typescript-eslint/no-unused-vars
result: AuthenticationResult,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
request:
| SilentRequest
| SsoSilentRequest
| RedirectRequest
| PopupRequest
): Promise<void> {
blockAPICallsBeforeInitialize(this.initialized);
blockNonBrowserEnvironment();
}
}
+345
View File
@@ -0,0 +1,345 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import {
IPerformanceClient,
PerformanceEvents,
} from "@azure/msal-common/browser";
import { KEY_FORMAT_JWK } from "../utils/BrowserConstants.js";
import { urlEncodeArr } from "../encode/Base64Encode.js";
import { base64DecToArr } from "../encode/Base64Decode.js";
/**
* This file defines functions used by the browser library to perform cryptography operations such as
* hashing and encoding. It also has helper functions to validate the availability of specific APIs.
*/
/**
* See here for more info on RsaHashedKeyGenParams: https://developer.mozilla.org/en-US/docs/Web/API/RsaHashedKeyGenParams
*/
// Algorithms
const PKCS1_V15_KEYGEN_ALG = "RSASSA-PKCS1-v1_5";
const AES_GCM = "AES-GCM";
const HKDF = "HKDF";
// SHA-256 hashing algorithm
const S256_HASH_ALG = "SHA-256";
// MOD length for PoP tokens
const MODULUS_LENGTH = 2048;
// Public Exponent
const PUBLIC_EXPONENT: Uint8Array = new Uint8Array([0x01, 0x00, 0x01]);
// UUID hex digits
const UUID_CHARS = "0123456789abcdef";
// Array to store UINT32 random value
const UINT32_ARR = new Uint32Array(1);
// Key Format
const RAW = "raw";
// Key Usages
const ENCRYPT = "encrypt";
const DECRYPT = "decrypt";
const DERIVE_KEY = "deriveKey";
// Suberror
const SUBTLE_SUBERROR = "crypto_subtle_undefined";
const keygenAlgorithmOptions: RsaHashedKeyGenParams = {
name: PKCS1_V15_KEYGEN_ALG,
hash: S256_HASH_ALG,
modulusLength: MODULUS_LENGTH,
publicExponent: PUBLIC_EXPONENT,
};
/**
* Check whether browser crypto is available.
*/
export function validateCryptoAvailable(
skipValidateSubtleCrypto: boolean
): void {
if (!window) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.nonBrowserEnvironment
);
}
if (!window.crypto) {
throw createBrowserAuthError(BrowserAuthErrorCodes.cryptoNonExistent);
}
if (!skipValidateSubtleCrypto && !window.crypto.subtle) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.cryptoNonExistent,
SUBTLE_SUBERROR
);
}
}
/**
* Returns a sha-256 hash of the given dataString as an ArrayBuffer.
* @param dataString {string} data string
* @param performanceClient {?IPerformanceClient}
* @param correlationId {?string} correlation id
*/
export async function sha256Digest(
dataString: string,
performanceClient?: IPerformanceClient,
correlationId?: string
): Promise<ArrayBuffer> {
performanceClient?.addQueueMeasurement(
PerformanceEvents.Sha256Digest,
correlationId
);
const encoder = new TextEncoder();
const data = encoder.encode(dataString);
return window.crypto.subtle.digest(
S256_HASH_ALG,
data
) as Promise<ArrayBuffer>;
}
/**
* Populates buffer with cryptographically random values.
* @param dataBuffer
*/
export function getRandomValues(dataBuffer: Uint8Array): Uint8Array {
return window.crypto.getRandomValues(dataBuffer);
}
/**
* Returns random Uint32 value.
* @returns {number}
*/
function getRandomUint32(): number {
window.crypto.getRandomValues(UINT32_ARR);
return UINT32_ARR[0];
}
/**
* Creates a UUID v7 from the current timestamp.
* Implementation relies on the system clock to guarantee increasing order of generated identifiers.
* @returns {number}
*/
export function createNewGuid(): string {
const currentTimestamp = Date.now();
const baseRand = getRandomUint32() * 0x400 + (getRandomUint32() & 0x3ff);
// Result byte array
const bytes = new Uint8Array(16);
// A 12-bit `rand_a` field value
const randA = Math.trunc(baseRand / 2 ** 30);
// The higher 30 bits of 62-bit `rand_b` field value
const randBHi = baseRand & (2 ** 30 - 1);
// The lower 32 bits of 62-bit `rand_b` field value
const randBLo = getRandomUint32();
bytes[0] = currentTimestamp / 2 ** 40;
bytes[1] = currentTimestamp / 2 ** 32;
bytes[2] = currentTimestamp / 2 ** 24;
bytes[3] = currentTimestamp / 2 ** 16;
bytes[4] = currentTimestamp / 2 ** 8;
bytes[5] = currentTimestamp;
bytes[6] = 0x70 | (randA >>> 8);
bytes[7] = randA;
bytes[8] = 0x80 | (randBHi >>> 24);
bytes[9] = randBHi >>> 16;
bytes[10] = randBHi >>> 8;
bytes[11] = randBHi;
bytes[12] = randBLo >>> 24;
bytes[13] = randBLo >>> 16;
bytes[14] = randBLo >>> 8;
bytes[15] = randBLo;
let text = "";
for (let i = 0; i < bytes.length; i++) {
text += UUID_CHARS.charAt(bytes[i] >>> 4);
text += UUID_CHARS.charAt(bytes[i] & 0xf);
if (i === 3 || i === 5 || i === 7 || i === 9) {
text += "-";
}
}
return text;
}
/**
* Generates a keypair based on current keygen algorithm config.
* @param extractable
* @param usages
*/
export async function generateKeyPair(
extractable: boolean,
usages: Array<KeyUsage>
): Promise<CryptoKeyPair> {
return window.crypto.subtle.generateKey(
keygenAlgorithmOptions,
extractable,
usages
) as Promise<CryptoKeyPair>;
}
/**
* Export key as Json Web Key (JWK)
* @param key
*/
export async function exportJwk(key: CryptoKey): Promise<JsonWebKey> {
return window.crypto.subtle.exportKey(
KEY_FORMAT_JWK,
key
) as Promise<JsonWebKey>;
}
/**
* Imports key as Json Web Key (JWK), can set extractable and usages.
* @param key
* @param extractable
* @param usages
*/
export async function importJwk(
key: JsonWebKey,
extractable: boolean,
usages: Array<KeyUsage>
): Promise<CryptoKey> {
return window.crypto.subtle.importKey(
KEY_FORMAT_JWK,
key,
keygenAlgorithmOptions,
extractable,
usages
) as Promise<CryptoKey>;
}
/**
* Signs given data with given key
* @param key
* @param data
*/
export async function sign(
key: CryptoKey,
data: ArrayBuffer
): Promise<ArrayBuffer> {
return window.crypto.subtle.sign(
keygenAlgorithmOptions,
key,
data
) as Promise<ArrayBuffer>;
}
/**
* Generates symmetric base encryption key. This may be stored as all encryption/decryption keys will be derived from this one.
*/
export async function generateBaseKey(): Promise<ArrayBuffer> {
const key = await window.crypto.subtle.generateKey(
{
name: AES_GCM,
length: 256,
},
true,
[ENCRYPT, DECRYPT]
);
return window.crypto.subtle.exportKey(RAW, key);
}
/**
* Returns the raw key to be passed into the key derivation function
* @param baseKey
* @returns
*/
export async function generateHKDF(baseKey: ArrayBuffer): Promise<CryptoKey> {
return window.crypto.subtle.importKey(RAW, baseKey, HKDF, false, [
DERIVE_KEY,
]);
}
/**
* Given a base key and a nonce generates a derived key to be used in encryption and decryption.
* Note: every time we encrypt a new key is derived
* @param baseKey
* @param nonce
* @returns
*/
async function deriveKey(
baseKey: CryptoKey,
nonce: ArrayBuffer,
context: string
): Promise<CryptoKey> {
return window.crypto.subtle.deriveKey(
{
name: HKDF,
salt: nonce,
hash: S256_HASH_ALG,
info: new TextEncoder().encode(context),
},
baseKey,
{ name: AES_GCM, length: 256 },
false,
[ENCRYPT, DECRYPT]
);
}
/**
* Encrypt the given data given a base key. Returns encrypted data and a nonce that must be provided during decryption
* @param key
* @param rawData
*/
export async function encrypt(
baseKey: CryptoKey,
rawData: string,
context: string
): Promise<{ data: string; nonce: string }> {
const encodedData = new TextEncoder().encode(rawData);
// The nonce must never be reused with a given key.
const nonce = window.crypto.getRandomValues(new Uint8Array(16));
const derivedKey = await deriveKey(baseKey, nonce, context);
const encryptedData = await window.crypto.subtle.encrypt(
{
name: AES_GCM,
iv: new Uint8Array(12), // New key is derived for every encrypt so we don't need a new nonce
},
derivedKey,
encodedData
);
return {
data: urlEncodeArr(new Uint8Array(encryptedData)),
nonce: urlEncodeArr(nonce),
};
}
/**
* Decrypt data with the given key and nonce
* @param key
* @param nonce
* @param encryptedData
* @returns
*/
export async function decrypt(
baseKey: CryptoKey,
nonce: string,
context: string,
encryptedData: string
): Promise<string> {
const encodedData = base64DecToArr(encryptedData);
const derivedKey = await deriveKey(baseKey, base64DecToArr(nonce), context);
const decryptedData = await window.crypto.subtle.decrypt(
{
name: AES_GCM,
iv: new Uint8Array(12), // New key is derived for every encrypt so we don't need a new nonce
},
derivedKey,
encodedData
);
return new TextDecoder().decode(decryptedData);
}
/**
* Returns the SHA-256 hash of an input string
* @param plainText
*/
export async function hashString(plainText: string): Promise<string> {
const hashBuffer: ArrayBuffer = await sha256Digest(plainText);
const hashBytes = new Uint8Array(hashBuffer);
return urlEncodeArr(hashBytes);
}
+285
View File
@@ -0,0 +1,285 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ICrypto,
IPerformanceClient,
JoseHeader,
Logger,
PerformanceEvents,
ShrOptions,
SignedHttpRequest,
SignedHttpRequestParameters,
} from "@azure/msal-common/browser";
import {
base64Encode,
urlEncode,
urlEncodeArr,
} from "../encode/Base64Encode.js";
import { base64Decode } from "../encode/Base64Decode.js";
import * as BrowserCrypto from "./BrowserCrypto.js";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { AsyncMemoryStorage } from "../cache/AsyncMemoryStorage.js";
export type CachedKeyPair = {
publicKey: CryptoKey;
privateKey: CryptoKey;
requestMethod?: string;
requestUri?: string;
};
/**
* This class implements MSAL's crypto interface, which allows it to perform base64 encoding and decoding, generating cryptographically random GUIDs and
* implementing Proof Key for Code Exchange specs for the OAuth Authorization Code Flow using PKCE (rfc here: https://tools.ietf.org/html/rfc7636).
*/
export class CryptoOps implements ICrypto {
private logger: Logger;
/**
* CryptoOps can be used in contexts outside a PCA instance,
* meaning there won't be a performance manager available.
*/
private performanceClient: IPerformanceClient | undefined;
private static POP_KEY_USAGES: Array<KeyUsage> = ["sign", "verify"];
private static EXTRACTABLE: boolean = true;
private cache: AsyncMemoryStorage<CachedKeyPair>;
constructor(
logger: Logger,
performanceClient?: IPerformanceClient,
skipValidateSubtleCrypto?: boolean
) {
this.logger = logger;
// Browser crypto needs to be validated first before any other classes can be set.
BrowserCrypto.validateCryptoAvailable(
skipValidateSubtleCrypto ?? false
);
this.cache = new AsyncMemoryStorage<CachedKeyPair>(this.logger);
this.performanceClient = performanceClient;
}
/**
* Creates a new random GUID - used to populate state and nonce.
* @returns string (GUID)
*/
createNewGuid(): string {
return BrowserCrypto.createNewGuid();
}
/**
* Encodes input string to base64.
* @param input
*/
base64Encode(input: string): string {
return base64Encode(input);
}
/**
* Decodes input string from base64.
* @param input
*/
base64Decode(input: string): string {
return base64Decode(input);
}
/**
* Encodes input string to base64 URL safe string.
* @param input
*/
base64UrlEncode(input: string): string {
return urlEncode(input);
}
/**
* Stringifies and base64Url encodes input public key
* @param inputKid
* @returns Base64Url encoded public key
*/
encodeKid(inputKid: string): string {
return this.base64UrlEncode(JSON.stringify({ kid: inputKid }));
}
/**
* Generates a keypair, stores it and returns a thumbprint
* @param request
*/
async getPublicKeyThumbprint(
request: SignedHttpRequestParameters
): Promise<string> {
const publicKeyThumbMeasurement =
this.performanceClient?.startMeasurement(
PerformanceEvents.CryptoOptsGetPublicKeyThumbprint,
request.correlationId
);
// Generate Keypair
const keyPair: CryptoKeyPair = await BrowserCrypto.generateKeyPair(
CryptoOps.EXTRACTABLE,
CryptoOps.POP_KEY_USAGES
);
// Generate Thumbprint for Public Key
const publicKeyJwk: JsonWebKey = await BrowserCrypto.exportJwk(
keyPair.publicKey
);
const pubKeyThumprintObj: JsonWebKey = {
e: publicKeyJwk.e,
kty: publicKeyJwk.kty,
n: publicKeyJwk.n,
};
const publicJwkString: string =
getSortedObjectString(pubKeyThumprintObj);
const publicJwkHash = await this.hashString(publicJwkString);
// Generate Thumbprint for Private Key
const privateKeyJwk: JsonWebKey = await BrowserCrypto.exportJwk(
keyPair.privateKey
);
// Re-import private key to make it unextractable
const unextractablePrivateKey: CryptoKey =
await BrowserCrypto.importJwk(privateKeyJwk, false, ["sign"]);
// Store Keypair data in keystore
await this.cache.setItem(publicJwkHash, {
privateKey: unextractablePrivateKey,
publicKey: keyPair.publicKey,
requestMethod: request.resourceRequestMethod,
requestUri: request.resourceRequestUri,
});
if (publicKeyThumbMeasurement) {
publicKeyThumbMeasurement.end({
success: true,
});
}
return publicJwkHash;
}
/**
* Removes cryptographic keypair from key store matching the keyId passed in
* @param kid
*/
async removeTokenBindingKey(kid: string): Promise<boolean> {
await this.cache.removeItem(kid);
const keyFound = await this.cache.containsKey(kid);
return !keyFound;
}
/**
* Removes all cryptographic keys from IndexedDB storage
*/
async clearKeystore(): Promise<boolean> {
// Delete in-memory keystores
this.cache.clearInMemory();
/**
* There is only one database, so calling clearPersistent on asymmetric keystore takes care of
* every persistent keystore
*/
try {
await this.cache.clearPersistent();
return true;
} catch (e) {
if (e instanceof Error) {
this.logger.error(
`Clearing keystore failed with error: ${e.message}`
);
} else {
this.logger.error(
"Clearing keystore failed with unknown error"
);
}
return false;
}
}
/**
* Signs the given object as a jwt payload with private key retrieved by given kid.
* @param payload
* @param kid
*/
async signJwt(
payload: SignedHttpRequest,
kid: string,
shrOptions?: ShrOptions,
correlationId?: string
): Promise<string> {
const signJwtMeasurement = this.performanceClient?.startMeasurement(
PerformanceEvents.CryptoOptsSignJwt,
correlationId
);
const cachedKeyPair = await this.cache.getItem(kid);
if (!cachedKeyPair) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.cryptoKeyNotFound
);
}
// Get public key as JWK
const publicKeyJwk = await BrowserCrypto.exportJwk(
cachedKeyPair.publicKey
);
const publicKeyJwkString = getSortedObjectString(publicKeyJwk);
// Base64URL encode public key thumbprint with keyId only: BASE64URL({ kid: "FULL_PUBLIC_KEY_HASH" })
const encodedKeyIdThumbprint = urlEncode(JSON.stringify({ kid: kid }));
// Generate header
const shrHeader = JoseHeader.getShrHeaderString({
...shrOptions?.header,
alg: publicKeyJwk.alg,
kid: encodedKeyIdThumbprint,
});
const encodedShrHeader = urlEncode(shrHeader);
// Generate payload
payload.cnf = {
jwk: JSON.parse(publicKeyJwkString),
};
const encodedPayload = urlEncode(JSON.stringify(payload));
// Form token string
const tokenString = `${encodedShrHeader}.${encodedPayload}`;
// Sign token
const encoder = new TextEncoder();
const tokenBuffer = encoder.encode(tokenString);
const signatureBuffer = await BrowserCrypto.sign(
cachedKeyPair.privateKey,
tokenBuffer
);
const encodedSignature = urlEncodeArr(new Uint8Array(signatureBuffer));
const signedJwt = `${tokenString}.${encodedSignature}`;
if (signJwtMeasurement) {
signJwtMeasurement.end({
success: true,
});
}
return signedJwt;
}
/**
* Returns the SHA-256 hash of an input string
* @param plainText
*/
async hashString(plainText: string): Promise<string> {
return BrowserCrypto.hashString(plainText);
}
}
function getSortedObjectString(obj: object): string {
return JSON.stringify(obj, Object.keys(obj).sort());
}
+115
View File
@@ -0,0 +1,115 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
IPerformanceClient,
Logger,
PerformanceEvents,
PkceCodes,
invoke,
invokeAsync,
} from "@azure/msal-common/browser";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { urlEncodeArr } from "../encode/Base64Encode.js";
import { getRandomValues, sha256Digest } from "./BrowserCrypto.js";
// Constant byte array length
const RANDOM_BYTE_ARR_LENGTH = 32;
/**
* This file defines APIs to generate PKCE codes and code verifiers.
*/
/**
* Generates PKCE Codes. See the RFC for more information: https://tools.ietf.org/html/rfc7636
*/
export async function generatePkceCodes(
performanceClient: IPerformanceClient,
logger: Logger,
correlationId: string
): Promise<PkceCodes> {
performanceClient.addQueueMeasurement(
PerformanceEvents.GeneratePkceCodes,
correlationId
);
const codeVerifier = invoke(
generateCodeVerifier,
PerformanceEvents.GenerateCodeVerifier,
logger,
performanceClient,
correlationId
)(performanceClient, logger, correlationId);
const codeChallenge = await invokeAsync(
generateCodeChallengeFromVerifier,
PerformanceEvents.GenerateCodeChallengeFromVerifier,
logger,
performanceClient,
correlationId
)(codeVerifier, performanceClient, logger, correlationId);
return {
verifier: codeVerifier,
challenge: codeChallenge,
};
}
/**
* Generates a random 32 byte buffer and returns the base64
* encoded string to be used as a PKCE Code Verifier
*/
function generateCodeVerifier(
performanceClient: IPerformanceClient,
logger: Logger,
correlationId: string
): string {
try {
// Generate random values as utf-8
const buffer: Uint8Array = new Uint8Array(RANDOM_BYTE_ARR_LENGTH);
invoke(
getRandomValues,
PerformanceEvents.GetRandomValues,
logger,
performanceClient,
correlationId
)(buffer);
// encode verifier as base64
const pkceCodeVerifierB64: string = urlEncodeArr(buffer);
return pkceCodeVerifierB64;
} catch (e) {
throw createBrowserAuthError(BrowserAuthErrorCodes.pkceNotCreated);
}
}
/**
* Creates a base64 encoded PKCE Code Challenge string from the
* hash created from the PKCE Code Verifier supplied
*/
async function generateCodeChallengeFromVerifier(
pkceCodeVerifier: string,
performanceClient: IPerformanceClient,
logger: Logger,
correlationId: string
): Promise<string> {
performanceClient.addQueueMeasurement(
PerformanceEvents.GenerateCodeChallengeFromVerifier,
correlationId
);
try {
// hashed verifier
const pkceHashedCodeVerifier = await invokeAsync(
sha256Digest,
PerformanceEvents.Sha256Digest,
logger,
performanceClient,
correlationId
)(pkceCodeVerifier, performanceClient, correlationId);
// encode hash as base64
return urlEncodeArr(new Uint8Array(pkceHashedCodeVerifier));
} catch (e) {
throw createBrowserAuthError(BrowserAuthErrorCodes.pkceNotCreated);
}
}
+76
View File
@@ -0,0 +1,76 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CryptoOps } from "./CryptoOps.js";
import {
Logger,
LoggerOptions,
PopTokenGenerator,
SignedHttpRequestParameters,
} from "@azure/msal-common/browser";
import { version, name } from "../packageMetadata.js";
export type SignedHttpRequestOptions = {
loggerOptions: LoggerOptions;
};
export class SignedHttpRequest {
private popTokenGenerator: PopTokenGenerator;
private cryptoOps: CryptoOps;
private shrParameters: SignedHttpRequestParameters;
private logger: Logger;
constructor(
shrParameters: SignedHttpRequestParameters,
shrOptions?: SignedHttpRequestOptions
) {
const loggerOptions = (shrOptions && shrOptions.loggerOptions) || {};
this.logger = new Logger(loggerOptions, name, version);
this.cryptoOps = new CryptoOps(this.logger);
this.popTokenGenerator = new PopTokenGenerator(this.cryptoOps);
this.shrParameters = shrParameters;
}
/**
* Generates and caches a keypair for the given request options.
* @returns Public key digest, which should be sent to the token issuer.
*/
async generatePublicKeyThumbprint(): Promise<string> {
const { kid } = await this.popTokenGenerator.generateKid(
this.shrParameters
);
return kid;
}
/**
* Generates a signed http request for the given payload with the given key.
* @param payload Payload to sign (e.g. access token)
* @param publicKeyThumbprint Public key digest (from generatePublicKeyThumbprint API)
* @param claims Additional claims to include/override in the signed JWT
* @returns Pop token signed with the corresponding private key
*/
async signRequest(
payload: string,
publicKeyThumbprint: string,
claims?: object
): Promise<string> {
return this.popTokenGenerator.signPayload(
payload,
publicKeyThumbprint,
this.shrParameters,
claims
);
}
/**
* Removes cached keys from browser for given public key thumbprint
* @param publicKeyThumbprint Public key digest (from generatePublicKeyThumbprint API)
* @returns If keys are properly deleted
*/
async removeKeys(publicKeyThumbprint: string): Promise<boolean> {
return this.cryptoOps.removeTokenBindingKey(publicKeyThumbprint);
}
}
+46
View File
@@ -0,0 +1,46 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
BrowserAuthErrorCodes,
createBrowserAuthError,
} from "../error/BrowserAuthError.js";
/**
* Class which exposes APIs to decode base64 strings to plaintext. See here for implementation details:
* https://developer.mozilla.org/en-US/docs/Glossary/Base64#the_unicode_problem
*/
/**
* Returns a URL-safe plaintext decoded string from b64 encoded input.
* @param input
*/
export function base64Decode(input: string): string {
return new TextDecoder().decode(base64DecToArr(input));
}
/**
* Decodes base64 into Uint8Array
* @param base64String
*/
export function base64DecToArr(base64String: string): Uint8Array {
let encodedString = base64String.replace(/-/g, "+").replace(/_/g, "/");
switch (encodedString.length % 4) {
case 0:
break;
case 2:
encodedString += "==";
break;
case 3:
encodedString += "=";
break;
default:
throw createBrowserAuthError(
BrowserAuthErrorCodes.invalidBase64String
);
}
const binString = atob(encodedString);
return Uint8Array.from(binString, (m) => m.codePointAt(0) || 0);
}
+52
View File
@@ -0,0 +1,52 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Class which exposes APIs to encode plaintext to base64 encoded string. See here for implementation details:
* https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding#Solution_2_%E2%80%93_JavaScript's_UTF-16_%3E_UTF-8_%3E_base64
*/
/**
* Returns URL Safe b64 encoded string from a plaintext string.
* @param input
*/
export function urlEncode(input: string): string {
return encodeURIComponent(
base64Encode(input)
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_")
);
}
/**
* Returns URL Safe b64 encoded string from an int8Array.
* @param inputArr
*/
export function urlEncodeArr(inputArr: Uint8Array): string {
return base64EncArr(inputArr)
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
/**
* Returns b64 encoded string from plaintext string.
* @param input
*/
export function base64Encode(input: string): string {
return base64EncArr(new TextEncoder().encode(input));
}
/**
* Base64 encode byte array
* @param aBytes
*/
function base64EncArr(aBytes: Uint8Array): string {
const binString = Array.from(aBytes, (x) => String.fromCodePoint(x)).join(
""
);
return btoa(binString);
}
+367
View File
@@ -0,0 +1,367 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthError } from "@azure/msal-common/browser";
import * as BrowserAuthErrorCodes from "./BrowserAuthErrorCodes.js";
export { BrowserAuthErrorCodes }; // Allow importing as "BrowserAuthErrorCodes"
const ErrorLink = "For more visit: aka.ms/msaljs/browser-errors";
/**
* BrowserAuthErrorMessage class containing string constants used by error codes and messages.
*/
export const BrowserAuthErrorMessages = {
[BrowserAuthErrorCodes.pkceNotCreated]:
"The PKCE code challenge and verifier could not be generated.",
[BrowserAuthErrorCodes.cryptoNonExistent]:
"The crypto object or function is not available.",
[BrowserAuthErrorCodes.emptyNavigateUri]:
"Navigation URI is empty. Please check stack trace for more info.",
[BrowserAuthErrorCodes.hashEmptyError]: `Hash value cannot be processed because it is empty. Please verify that your redirectUri is not clearing the hash. ${ErrorLink}`,
[BrowserAuthErrorCodes.noStateInHash]:
"Hash does not contain state. Please verify that the request originated from msal.",
[BrowserAuthErrorCodes.hashDoesNotContainKnownProperties]: `Hash does not contain known properites. Please verify that your redirectUri is not changing the hash. ${ErrorLink}`,
[BrowserAuthErrorCodes.unableToParseState]:
"Unable to parse state. Please verify that the request originated from msal.",
[BrowserAuthErrorCodes.stateInteractionTypeMismatch]:
"Hash contains state but the interaction type does not match the caller.",
[BrowserAuthErrorCodes.interactionInProgress]: `Interaction is currently in progress. Please ensure that this interaction has been completed before calling an interactive API. ${ErrorLink}`,
[BrowserAuthErrorCodes.popupWindowError]:
"Error opening popup window. This can happen if you are using IE or if popups are blocked in the browser.",
[BrowserAuthErrorCodes.emptyWindowError]:
"window.open returned null or undefined window object.",
[BrowserAuthErrorCodes.userCancelled]: "User cancelled the flow.",
[BrowserAuthErrorCodes.monitorPopupTimeout]: `Token acquisition in popup failed due to timeout. ${ErrorLink}`,
[BrowserAuthErrorCodes.monitorWindowTimeout]: `Token acquisition in iframe failed due to timeout. ${ErrorLink}`,
[BrowserAuthErrorCodes.redirectInIframe]:
"Redirects are not supported for iframed or brokered applications. Please ensure you are using MSAL.js in a top frame of the window if using the redirect APIs, or use the popup APIs.",
[BrowserAuthErrorCodes.blockIframeReload]: `Request was blocked inside an iframe because MSAL detected an authentication response. ${ErrorLink}`,
[BrowserAuthErrorCodes.blockNestedPopups]:
"Request was blocked inside a popup because MSAL detected it was running in a popup.",
[BrowserAuthErrorCodes.iframeClosedPrematurely]:
"The iframe being monitored was closed prematurely.",
[BrowserAuthErrorCodes.silentLogoutUnsupported]:
"Silent logout not supported. Please call logoutRedirect or logoutPopup instead.",
[BrowserAuthErrorCodes.noAccountError]:
"No account object provided to acquireTokenSilent and no active account has been set. Please call setActiveAccount or provide an account on the request.",
[BrowserAuthErrorCodes.silentPromptValueError]:
"The value given for the prompt value is not valid for silent requests - must be set to 'none' or 'no_session'.",
[BrowserAuthErrorCodes.noTokenRequestCacheError]:
"No token request found in cache.",
[BrowserAuthErrorCodes.unableToParseTokenRequestCacheError]:
"The cached token request could not be parsed.",
[BrowserAuthErrorCodes.noCachedAuthorityError]:
"No cached authority found.",
[BrowserAuthErrorCodes.authRequestNotSetError]:
"Auth Request not set. Please ensure initiateAuthRequest was called from the InteractionHandler",
[BrowserAuthErrorCodes.invalidCacheType]: "Invalid cache type",
[BrowserAuthErrorCodes.nonBrowserEnvironment]:
"Login and token requests are not supported in non-browser environments.",
[BrowserAuthErrorCodes.databaseNotOpen]: "Database is not open!",
[BrowserAuthErrorCodes.noNetworkConnectivity]:
"No network connectivity. Check your internet connection.",
[BrowserAuthErrorCodes.postRequestFailed]:
"Network request failed: If the browser threw a CORS error, check that the redirectUri is registered in the Azure App Portal as type 'SPA'",
[BrowserAuthErrorCodes.getRequestFailed]:
"Network request failed. Please check the network trace to determine root cause.",
[BrowserAuthErrorCodes.failedToParseResponse]:
"Failed to parse network response. Check network trace.",
[BrowserAuthErrorCodes.unableToLoadToken]: "Error loading token to cache.",
[BrowserAuthErrorCodes.cryptoKeyNotFound]:
"Cryptographic Key or Keypair not found in browser storage.",
[BrowserAuthErrorCodes.authCodeRequired]:
"An authorization code must be provided (as the `code` property on the request) to this flow.",
[BrowserAuthErrorCodes.authCodeOrNativeAccountIdRequired]:
"An authorization code or nativeAccountId must be provided to this flow.",
[BrowserAuthErrorCodes.spaCodeAndNativeAccountIdPresent]:
"Request cannot contain both spa code and native account id.",
[BrowserAuthErrorCodes.databaseUnavailable]:
"IndexedDB, which is required for persistent cryptographic key storage, is unavailable. This may be caused by browser privacy features which block persistent storage in third-party contexts.",
[BrowserAuthErrorCodes.unableToAcquireTokenFromNativePlatform]: `Unable to acquire token from native platform. ${ErrorLink}`,
[BrowserAuthErrorCodes.nativeHandshakeTimeout]:
"Timed out while attempting to establish connection to browser extension",
[BrowserAuthErrorCodes.nativeExtensionNotInstalled]:
"Native extension is not installed. If you think this is a mistake call the initialize function.",
[BrowserAuthErrorCodes.nativeConnectionNotEstablished]: `Connection to native platform has not been established. Please install a compatible browser extension and run initialize(). ${ErrorLink}`,
[BrowserAuthErrorCodes.uninitializedPublicClientApplication]: `You must call and await the initialize function before attempting to call any other MSAL API. ${ErrorLink}`,
[BrowserAuthErrorCodes.nativePromptNotSupported]:
"The provided prompt is not supported by the native platform. This request should be routed to the web based flow.",
[BrowserAuthErrorCodes.invalidBase64String]:
"Invalid base64 encoded string.",
[BrowserAuthErrorCodes.invalidPopTokenRequest]:
"Invalid PoP token request. The request should not have both a popKid value and signPopToken set to true.",
[BrowserAuthErrorCodes.failedToBuildHeaders]:
"Failed to build request headers object.",
[BrowserAuthErrorCodes.failedToParseHeaders]:
"Failed to parse response headers",
};
/**
* BrowserAuthErrorMessage class containing string constants used by error codes and messages.
* @deprecated Use exported BrowserAuthErrorCodes instead.
* In your app you can do :
* ```
* import { BrowserAuthErrorCodes } from "@azure/msal-browser";
* ```
*/
export const BrowserAuthErrorMessage = {
pkceNotGenerated: {
code: BrowserAuthErrorCodes.pkceNotCreated,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.pkceNotCreated],
},
cryptoDoesNotExist: {
code: BrowserAuthErrorCodes.cryptoNonExistent,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.cryptoNonExistent],
},
emptyNavigateUriError: {
code: BrowserAuthErrorCodes.emptyNavigateUri,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.emptyNavigateUri],
},
hashEmptyError: {
code: BrowserAuthErrorCodes.hashEmptyError,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.hashEmptyError],
},
hashDoesNotContainStateError: {
code: BrowserAuthErrorCodes.noStateInHash,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.noStateInHash],
},
hashDoesNotContainKnownPropertiesError: {
code: BrowserAuthErrorCodes.hashDoesNotContainKnownProperties,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.hashDoesNotContainKnownProperties
],
},
unableToParseStateError: {
code: BrowserAuthErrorCodes.unableToParseState,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.unableToParseState
],
},
stateInteractionTypeMismatchError: {
code: BrowserAuthErrorCodes.stateInteractionTypeMismatch,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.stateInteractionTypeMismatch
],
},
interactionInProgress: {
code: BrowserAuthErrorCodes.interactionInProgress,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.interactionInProgress
],
},
popupWindowError: {
code: BrowserAuthErrorCodes.popupWindowError,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.popupWindowError],
},
emptyWindowError: {
code: BrowserAuthErrorCodes.emptyWindowError,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.emptyWindowError],
},
userCancelledError: {
code: BrowserAuthErrorCodes.userCancelled,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.userCancelled],
},
monitorPopupTimeoutError: {
code: BrowserAuthErrorCodes.monitorPopupTimeout,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.monitorPopupTimeout
],
},
monitorIframeTimeoutError: {
code: BrowserAuthErrorCodes.monitorWindowTimeout,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.monitorWindowTimeout
],
},
redirectInIframeError: {
code: BrowserAuthErrorCodes.redirectInIframe,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.redirectInIframe],
},
blockTokenRequestsInHiddenIframeError: {
code: BrowserAuthErrorCodes.blockIframeReload,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.blockIframeReload],
},
blockAcquireTokenInPopupsError: {
code: BrowserAuthErrorCodes.blockNestedPopups,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.blockNestedPopups],
},
iframeClosedPrematurelyError: {
code: BrowserAuthErrorCodes.iframeClosedPrematurely,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.iframeClosedPrematurely
],
},
silentLogoutUnsupportedError: {
code: BrowserAuthErrorCodes.silentLogoutUnsupported,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.silentLogoutUnsupported
],
},
noAccountError: {
code: BrowserAuthErrorCodes.noAccountError,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.noAccountError],
},
silentPromptValueError: {
code: BrowserAuthErrorCodes.silentPromptValueError,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.silentPromptValueError
],
},
noTokenRequestCacheError: {
code: BrowserAuthErrorCodes.noTokenRequestCacheError,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.noTokenRequestCacheError
],
},
unableToParseTokenRequestCacheError: {
code: BrowserAuthErrorCodes.unableToParseTokenRequestCacheError,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.unableToParseTokenRequestCacheError
],
},
noCachedAuthorityError: {
code: BrowserAuthErrorCodes.noCachedAuthorityError,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.noCachedAuthorityError
],
},
authRequestNotSet: {
code: BrowserAuthErrorCodes.authRequestNotSetError,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.authRequestNotSetError
],
},
invalidCacheType: {
code: BrowserAuthErrorCodes.invalidCacheType,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.invalidCacheType],
},
notInBrowserEnvironment: {
code: BrowserAuthErrorCodes.nonBrowserEnvironment,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.nonBrowserEnvironment
],
},
databaseNotOpen: {
code: BrowserAuthErrorCodes.databaseNotOpen,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.databaseNotOpen],
},
noNetworkConnectivity: {
code: BrowserAuthErrorCodes.noNetworkConnectivity,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.noNetworkConnectivity
],
},
postRequestFailed: {
code: BrowserAuthErrorCodes.postRequestFailed,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.postRequestFailed],
},
getRequestFailed: {
code: BrowserAuthErrorCodes.getRequestFailed,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.getRequestFailed],
},
failedToParseNetworkResponse: {
code: BrowserAuthErrorCodes.failedToParseResponse,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.failedToParseResponse
],
},
unableToLoadTokenError: {
code: BrowserAuthErrorCodes.unableToLoadToken,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.unableToLoadToken],
},
signingKeyNotFoundInStorage: {
code: BrowserAuthErrorCodes.cryptoKeyNotFound,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.cryptoKeyNotFound],
},
authCodeRequired: {
code: BrowserAuthErrorCodes.authCodeRequired,
desc: BrowserAuthErrorMessages[BrowserAuthErrorCodes.authCodeRequired],
},
authCodeOrNativeAccountRequired: {
code: BrowserAuthErrorCodes.authCodeOrNativeAccountIdRequired,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.authCodeOrNativeAccountIdRequired
],
},
spaCodeAndNativeAccountPresent: {
code: BrowserAuthErrorCodes.spaCodeAndNativeAccountIdPresent,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.spaCodeAndNativeAccountIdPresent
],
},
databaseUnavailable: {
code: BrowserAuthErrorCodes.databaseUnavailable,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.databaseUnavailable
],
},
unableToAcquireTokenFromNativePlatform: {
code: BrowserAuthErrorCodes.unableToAcquireTokenFromNativePlatform,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.unableToAcquireTokenFromNativePlatform
],
},
nativeHandshakeTimeout: {
code: BrowserAuthErrorCodes.nativeHandshakeTimeout,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.nativeHandshakeTimeout
],
},
nativeExtensionNotInstalled: {
code: BrowserAuthErrorCodes.nativeExtensionNotInstalled,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.nativeExtensionNotInstalled
],
},
nativeConnectionNotEstablished: {
code: BrowserAuthErrorCodes.nativeConnectionNotEstablished,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.nativeConnectionNotEstablished
],
},
uninitializedPublicClientApplication: {
code: BrowserAuthErrorCodes.uninitializedPublicClientApplication,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.uninitializedPublicClientApplication
],
},
nativePromptNotSupported: {
code: BrowserAuthErrorCodes.nativePromptNotSupported,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.nativePromptNotSupported
],
},
invalidBase64StringError: {
code: BrowserAuthErrorCodes.invalidBase64String,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.invalidBase64String
],
},
invalidPopTokenRequest: {
code: BrowserAuthErrorCodes.invalidPopTokenRequest,
desc: BrowserAuthErrorMessages[
BrowserAuthErrorCodes.invalidPopTokenRequest
],
},
};
/**
* Browser library error class thrown by the MSAL.js library for SPAs
*/
export class BrowserAuthError extends AuthError {
constructor(errorCode: string, subError?: string) {
super(errorCode, BrowserAuthErrorMessages[errorCode], subError);
Object.setPrototypeOf(this, BrowserAuthError.prototype);
this.name = "BrowserAuthError";
}
}
export function createBrowserAuthError(
errorCode: string,
subError?: string
): BrowserAuthError {
return new BrowserAuthError(errorCode, subError);
}
+60
View File
@@ -0,0 +1,60 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const pkceNotCreated = "pkce_not_created";
export const cryptoNonExistent = "crypto_nonexistent";
export const emptyNavigateUri = "empty_navigate_uri";
export const hashEmptyError = "hash_empty_error";
export const noStateInHash = "no_state_in_hash";
export const hashDoesNotContainKnownProperties =
"hash_does_not_contain_known_properties";
export const unableToParseState = "unable_to_parse_state";
export const stateInteractionTypeMismatch = "state_interaction_type_mismatch";
export const interactionInProgress = "interaction_in_progress";
export const popupWindowError = "popup_window_error";
export const emptyWindowError = "empty_window_error";
export const userCancelled = "user_cancelled";
export const monitorPopupTimeout = "monitor_popup_timeout";
export const monitorWindowTimeout = "monitor_window_timeout";
export const redirectInIframe = "redirect_in_iframe";
export const blockIframeReload = "block_iframe_reload";
export const blockNestedPopups = "block_nested_popups";
export const iframeClosedPrematurely = "iframe_closed_prematurely";
export const silentLogoutUnsupported = "silent_logout_unsupported";
export const noAccountError = "no_account_error";
export const silentPromptValueError = "silent_prompt_value_error";
export const noTokenRequestCacheError = "no_token_request_cache_error";
export const unableToParseTokenRequestCacheError =
"unable_to_parse_token_request_cache_error";
export const noCachedAuthorityError = "no_cached_authority_error";
export const authRequestNotSetError = "auth_request_not_set_error";
export const invalidCacheType = "invalid_cache_type";
export const nonBrowserEnvironment = "non_browser_environment";
export const databaseNotOpen = "database_not_open";
export const noNetworkConnectivity = "no_network_connectivity";
export const postRequestFailed = "post_request_failed";
export const getRequestFailed = "get_request_failed";
export const failedToParseResponse = "failed_to_parse_response";
export const unableToLoadToken = "unable_to_load_token";
export const cryptoKeyNotFound = "crypto_key_not_found";
export const authCodeRequired = "auth_code_required";
export const authCodeOrNativeAccountIdRequired =
"auth_code_or_nativeAccountId_required";
export const spaCodeAndNativeAccountIdPresent =
"spa_code_and_nativeAccountId_present";
export const databaseUnavailable = "database_unavailable";
export const unableToAcquireTokenFromNativePlatform =
"unable_to_acquire_token_from_native_platform";
export const nativeHandshakeTimeout = "native_handshake_timeout";
export const nativeExtensionNotInstalled = "native_extension_not_installed";
export const nativeConnectionNotEstablished =
"native_connection_not_established";
export const uninitializedPublicClientApplication =
"uninitialized_public_client_application";
export const nativePromptNotSupported = "native_prompt_not_supported";
export const invalidBase64String = "invalid_base64_string";
export const invalidPopTokenRequest = "invalid_pop_token_request";
export const failedToBuildHeaders = "failed_to_build_headers";
export const failedToParseHeaders = "failed_to_parse_headers";
@@ -0,0 +1,64 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthError } from "@azure/msal-common/browser";
import * as BrowserConfigurationAuthErrorCodes from "./BrowserConfigurationAuthErrorCodes.js";
export { BrowserConfigurationAuthErrorCodes };
export const BrowserConfigurationAuthErrorMessages = {
[BrowserConfigurationAuthErrorCodes.storageNotSupported]:
"Given storage configuration option was not supported.",
[BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled]:
"Stub instance of Public Client Application was called. If using msal-react, please ensure context is not used without a provider. For more visit: aka.ms/msaljs/browser-errors",
[BrowserConfigurationAuthErrorCodes.inMemRedirectUnavailable]:
"Redirect cannot be supported. In-memory storage was selected and storeAuthStateInCookie=false, which would cause the library to be unable to handle the incoming hash. If you would like to use the redirect API, please use session/localStorage or set storeAuthStateInCookie=true.",
};
/**
* BrowserAuthErrorMessage class containing string constants used by error codes and messages.
* @deprecated Use BrowserAuthErrorCodes instead
*/
export const BrowserConfigurationAuthErrorMessage = {
storageNotSupportedError: {
code: BrowserConfigurationAuthErrorCodes.storageNotSupported,
desc: BrowserConfigurationAuthErrorMessages[
BrowserConfigurationAuthErrorCodes.storageNotSupported
],
},
stubPcaInstanceCalled: {
code: BrowserConfigurationAuthErrorCodes.stubbedPublicClientApplicationCalled,
desc: BrowserConfigurationAuthErrorMessages[
BrowserConfigurationAuthErrorCodes
.stubbedPublicClientApplicationCalled
],
},
inMemRedirectUnavailable: {
code: BrowserConfigurationAuthErrorCodes.inMemRedirectUnavailable,
desc: BrowserConfigurationAuthErrorMessages[
BrowserConfigurationAuthErrorCodes.inMemRedirectUnavailable
],
},
};
/**
* Browser library error class thrown by the MSAL.js library for SPAs
*/
export class BrowserConfigurationAuthError extends AuthError {
constructor(errorCode: string, errorMessage?: string) {
super(errorCode, errorMessage);
this.name = "BrowserConfigurationAuthError";
Object.setPrototypeOf(this, BrowserConfigurationAuthError.prototype);
}
}
export function createBrowserConfigurationAuthError(
errorCode: string
): BrowserConfigurationAuthError {
return new BrowserConfigurationAuthError(
errorCode,
BrowserConfigurationAuthErrorMessages[errorCode]
);
}
@@ -0,0 +1,9 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const storageNotSupported = "storage_not_supported";
export const stubbedPublicClientApplicationCalled =
"stubbed_public_client_application_called";
export const inMemRedirectUnavailable = "in_mem_redirect_unavailable";
+113
View File
@@ -0,0 +1,113 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthError,
InteractionRequiredAuthError,
InteractionRequiredAuthErrorCodes,
createInteractionRequiredAuthError,
} from "@azure/msal-common/browser";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "./BrowserAuthError.js";
import * as NativeAuthErrorCodes from "./NativeAuthErrorCodes.js";
import * as NativeStatusCodes from "../broker/nativeBroker/NativeStatusCodes.js";
export { NativeAuthErrorCodes };
export type OSError = {
error?: number;
protocol_error?: string;
properties?: object;
status?: string;
retryable?: boolean;
};
const INVALID_METHOD_ERROR = -2147186943;
export const NativeAuthErrorMessages = {
[NativeAuthErrorCodes.userSwitch]:
"User attempted to switch accounts in the native broker, which is not allowed. All new accounts must sign-in through the standard web flow first, please try again.",
};
export class NativeAuthError extends AuthError {
ext: OSError | undefined;
constructor(errorCode: string, description?: string, ext?: OSError) {
super(errorCode, description);
Object.setPrototypeOf(this, NativeAuthError.prototype);
this.name = "NativeAuthError";
this.ext = ext;
}
}
/**
* These errors should result in a fallback to the 'standard' browser based auth flow.
*/
export function isFatalNativeAuthError(error: NativeAuthError): boolean {
if (
error.ext &&
error.ext.status &&
(error.ext.status === NativeStatusCodes.PERSISTENT_ERROR ||
error.ext.status === NativeStatusCodes.DISABLED)
) {
return true;
}
if (
error.ext &&
error.ext.error &&
error.ext.error === INVALID_METHOD_ERROR
) {
return true;
}
switch (error.errorCode) {
case NativeAuthErrorCodes.contentError:
return true;
default:
return false;
}
}
/**
* Create the appropriate error object based on the WAM status code.
* @param code
* @param description
* @param ext
* @returns
*/
export function createNativeAuthError(
code: string,
description?: string,
ext?: OSError
): AuthError {
if (ext && ext.status) {
switch (ext.status) {
case NativeStatusCodes.ACCOUNT_UNAVAILABLE:
return createInteractionRequiredAuthError(
InteractionRequiredAuthErrorCodes.nativeAccountUnavailable
);
case NativeStatusCodes.USER_INTERACTION_REQUIRED:
return new InteractionRequiredAuthError(code, description);
case NativeStatusCodes.USER_CANCEL:
return createBrowserAuthError(
BrowserAuthErrorCodes.userCancelled
);
case NativeStatusCodes.NO_NETWORK:
return createBrowserAuthError(
BrowserAuthErrorCodes.noNetworkConnectivity
);
}
}
return new NativeAuthError(
code,
NativeAuthErrorMessages[code] || description,
ext
);
}
+7
View File
@@ -0,0 +1,7 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const contentError = "ContentError";
export const userSwitch = "user_switch";
+32
View File
@@ -0,0 +1,32 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthError } from "@azure/msal-common/browser";
/**
* NestedAppAuthErrorMessage class containing string constants used by error codes and messages.
*/
export const NestedAppAuthErrorMessage = {
unsupportedMethod: {
code: "unsupported_method",
desc: "This method is not supported in nested app environment.",
},
};
export class NestedAppAuthError extends AuthError {
constructor(errorCode: string, errorMessage?: string) {
super(errorCode, errorMessage);
Object.setPrototypeOf(this, NestedAppAuthError.prototype);
this.name = "NestedAppAuthError";
}
public static createUnsupportedError(): NestedAppAuthError {
return new NestedAppAuthError(
NestedAppAuthErrorMessage.unsupportedMethod.code,
NestedAppAuthErrorMessage.unsupportedMethod.desc
);
}
}
+161
View File
@@ -0,0 +1,161 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Logger } from "@azure/msal-common/browser";
import { InteractionType } from "../utils/BrowserConstants.js";
import {
EventCallbackFunction,
EventError,
EventMessage,
EventPayload,
} from "./EventMessage.js";
import { EventType } from "./EventType.js";
import { createGuid } from "../utils/BrowserUtils.js";
const BROADCAST_CHANNEL_NAME = "msal.broadcast.event";
export class EventHandler {
// Callback for subscribing to events
private eventCallbacks: Map<
string,
[EventCallbackFunction, Array<EventType>]
>;
private logger: Logger;
private broadcastChannel: BroadcastChannel;
constructor(logger?: Logger) {
this.eventCallbacks = new Map();
this.logger = logger || new Logger({});
this.broadcastChannel = new BroadcastChannel(BROADCAST_CHANNEL_NAME);
this.invokeCrossTabCallbacks = this.invokeCrossTabCallbacks.bind(this);
}
/**
* Adds event callbacks to array
* @param callback - callback to be invoked when an event is raised
* @param eventTypes - list of events that this callback will be invoked for, if not provided callback will be invoked for all events
* @param callbackId - Identifier for the callback, used to locate and remove the callback when no longer required
*/
addEventCallback(
callback: EventCallbackFunction,
eventTypes?: Array<EventType>,
callbackId?: string
): string | null {
if (typeof window !== "undefined") {
const id = callbackId || createGuid();
if (this.eventCallbacks.has(id)) {
this.logger.error(
`Event callback with id: ${id} is already registered. Please provide a unique id or remove the existing callback and try again.`
);
return null;
}
this.eventCallbacks.set(id, [callback, eventTypes || []]);
this.logger.verbose(`Event callback registered with id: ${id}`);
return id;
}
return null;
}
/**
* Removes callback with provided id from callback array
* @param callbackId
*/
removeEventCallback(callbackId: string): void {
this.eventCallbacks.delete(callbackId);
this.logger.verbose(`Event callback ${callbackId} removed.`);
}
/**
* Emits events by calling callback with event message
* @param eventType
* @param interactionType
* @param payload
* @param error
*/
emitEvent(
eventType: EventType,
interactionType?: InteractionType,
payload?: EventPayload,
error?: EventError
): void {
const message: EventMessage = {
eventType: eventType,
interactionType: interactionType || null,
payload: payload || null,
error: error || null,
timestamp: Date.now(),
};
switch (eventType) {
case EventType.ACCOUNT_ADDED:
case EventType.ACCOUNT_REMOVED:
case EventType.ACTIVE_ACCOUNT_CHANGED:
// Send event to other open tabs / MSAL instances on same domain
this.broadcastChannel.postMessage(message);
break;
default:
// Emit event to callbacks registered in this instance
this.invokeCallbacks(message);
break;
}
}
/**
* Invoke registered callbacks
* @param message
*/
private invokeCallbacks(message: EventMessage): void {
this.eventCallbacks.forEach(
(
[callback, eventTypes]: [
EventCallbackFunction,
Array<EventType>
],
callbackId: string
) => {
if (
eventTypes.length === 0 ||
eventTypes.includes(message.eventType)
) {
this.logger.verbose(
`Emitting event to callback ${callbackId}: ${message.eventType}`
);
callback.apply(null, [message]);
}
}
);
}
/**
* Wrapper around invokeCallbacks to handle broadcast events received from other tabs/instances
* @param event
*/
private invokeCrossTabCallbacks(event: MessageEvent): void {
const message = event.data as EventMessage;
this.invokeCallbacks(message);
}
/**
* Listen for events broadcasted from other tabs/instances
*/
subscribeCrossTab(): void {
this.broadcastChannel.addEventListener(
"message",
this.invokeCrossTabCallbacks
);
}
/**
* Unsubscribe from broadcast events
*/
unsubscribeCrossTab(): void {
this.broadcastChannel.removeEventListener(
"message",
this.invokeCrossTabCallbacks
);
}
}
+126
View File
@@ -0,0 +1,126 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthError, AccountInfo } from "@azure/msal-common/browser";
import { EventType } from "./EventType.js";
import {
InteractionStatus,
InteractionType,
} from "../utils/BrowserConstants.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { SilentRequest } from "../request/SilentRequest.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import { EndSessionRequest } from "../request/EndSessionRequest.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
export type EventMessage = {
eventType: EventType;
interactionType: InteractionType | null;
payload: EventPayload;
error: EventError;
timestamp: number;
};
export type PopupEvent = {
popupWindow: Window;
};
export type EventPayload =
| AccountInfo
| PopupRequest
| RedirectRequest
| SilentRequest
| SsoSilentRequest
| EndSessionRequest
| AuthenticationResult
| PopupEvent
| null;
export type EventError = AuthError | Error | null;
export type EventCallbackFunction = (message: EventMessage) => void;
export class EventMessageUtils {
/**
* Gets interaction status from event message
* @param message
* @param currentStatus
*/
static getInteractionStatusFromEvent(
message: EventMessage,
currentStatus?: InteractionStatus
): InteractionStatus | null {
switch (message.eventType) {
case EventType.LOGIN_START:
return InteractionStatus.Login;
case EventType.SSO_SILENT_START:
return InteractionStatus.SsoSilent;
case EventType.ACQUIRE_TOKEN_START:
if (
message.interactionType === InteractionType.Redirect ||
message.interactionType === InteractionType.Popup
) {
return InteractionStatus.AcquireToken;
}
break;
case EventType.HANDLE_REDIRECT_START:
return InteractionStatus.HandleRedirect;
case EventType.LOGOUT_START:
return InteractionStatus.Logout;
case EventType.SSO_SILENT_SUCCESS:
case EventType.SSO_SILENT_FAILURE:
if (
currentStatus &&
currentStatus !== InteractionStatus.SsoSilent
) {
// Prevent this event from clearing any status other than ssoSilent
break;
}
return InteractionStatus.None;
case EventType.LOGOUT_END:
if (
currentStatus &&
currentStatus !== InteractionStatus.Logout
) {
// Prevent this event from clearing any status other than logout
break;
}
return InteractionStatus.None;
case EventType.HANDLE_REDIRECT_END:
if (
currentStatus &&
currentStatus !== InteractionStatus.HandleRedirect
) {
// Prevent this event from clearing any status other than handleRedirect
break;
}
return InteractionStatus.None;
case EventType.LOGIN_SUCCESS:
case EventType.LOGIN_FAILURE:
case EventType.ACQUIRE_TOKEN_SUCCESS:
case EventType.ACQUIRE_TOKEN_FAILURE:
case EventType.RESTORE_FROM_BFCACHE:
if (
message.interactionType === InteractionType.Redirect ||
message.interactionType === InteractionType.Popup
) {
if (
currentStatus &&
currentStatus !== InteractionStatus.Login &&
currentStatus !== InteractionStatus.AcquireToken
) {
// Prevent this event from clearing any status other than login or acquireToken
break;
}
return InteractionStatus.None;
}
break;
default:
break;
}
return null;
}
}
+34
View File
@@ -0,0 +1,34 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const EventType = {
INITIALIZE_START: "msal:initializeStart",
INITIALIZE_END: "msal:initializeEnd",
ACCOUNT_ADDED: "msal:accountAdded",
ACCOUNT_REMOVED: "msal:accountRemoved",
ACTIVE_ACCOUNT_CHANGED: "msal:activeAccountChanged",
LOGIN_START: "msal:loginStart",
LOGIN_SUCCESS: "msal:loginSuccess",
LOGIN_FAILURE: "msal:loginFailure",
ACQUIRE_TOKEN_START: "msal:acquireTokenStart",
ACQUIRE_TOKEN_SUCCESS: "msal:acquireTokenSuccess",
ACQUIRE_TOKEN_FAILURE: "msal:acquireTokenFailure",
ACQUIRE_TOKEN_NETWORK_START: "msal:acquireTokenFromNetworkStart",
SSO_SILENT_START: "msal:ssoSilentStart",
SSO_SILENT_SUCCESS: "msal:ssoSilentSuccess",
SSO_SILENT_FAILURE: "msal:ssoSilentFailure",
ACQUIRE_TOKEN_BY_CODE_START: "msal:acquireTokenByCodeStart",
ACQUIRE_TOKEN_BY_CODE_SUCCESS: "msal:acquireTokenByCodeSuccess",
ACQUIRE_TOKEN_BY_CODE_FAILURE: "msal:acquireTokenByCodeFailure",
HANDLE_REDIRECT_START: "msal:handleRedirectStart",
HANDLE_REDIRECT_END: "msal:handleRedirectEnd",
POPUP_OPENED: "msal:popupOpened",
LOGOUT_START: "msal:logoutStart",
LOGOUT_SUCCESS: "msal:logoutSuccess",
LOGOUT_FAILURE: "msal:logoutFailure",
LOGOUT_END: "msal:logoutEnd",
RESTORE_FROM_BFCACHE: "msal:restoreFromBFCache",
} as const;
export type EventType = (typeof EventType)[keyof typeof EventType];
+162
View File
@@ -0,0 +1,162 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* @packageDocumentation
* @module @azure/msal-browser
*/
import * as BrowserUtils from "./utils/BrowserUtils.js";
export { BrowserUtils };
export {
PublicClientApplication,
createNestablePublicClientApplication,
createStandardPublicClientApplication,
} from "./app/PublicClientApplication.js";
export { PublicClientNext } from "./app/PublicClientNext.js";
export { IController } from "./controllers/IController.js";
export {
Configuration,
BrowserAuthOptions,
CacheOptions,
BrowserSystemOptions,
BrowserTelemetryOptions,
BrowserConfiguration,
DEFAULT_IFRAME_TIMEOUT_MS,
} from "./config/Configuration.js";
export {
InteractionType,
InteractionStatus,
BrowserCacheLocation,
WrapperSKU,
ApiId,
CacheLookupPolicy,
} from "./utils/BrowserConstants.js";
// Browser Errors
export {
BrowserAuthError,
BrowserAuthErrorMessage,
BrowserAuthErrorCodes,
} from "./error/BrowserAuthError.js";
export {
BrowserConfigurationAuthError,
BrowserConfigurationAuthErrorCodes,
BrowserConfigurationAuthErrorMessage,
} from "./error/BrowserConfigurationAuthError.js";
// Interfaces
export {
IPublicClientApplication,
stubbedPublicClientApplication,
} from "./app/IPublicClientApplication.js";
export { INavigationClient } from "./navigation/INavigationClient.js";
export { NavigationClient } from "./navigation/NavigationClient.js";
export { NavigationOptions } from "./navigation/NavigationOptions.js";
export { PopupRequest } from "./request/PopupRequest.js";
export { RedirectRequest } from "./request/RedirectRequest.js";
export { SilentRequest } from "./request/SilentRequest.js";
export { SsoSilentRequest } from "./request/SsoSilentRequest.js";
export { EndSessionRequest } from "./request/EndSessionRequest.js";
export { EndSessionPopupRequest } from "./request/EndSessionPopupRequest.js";
export { AuthorizationUrlRequest } from "./request/AuthorizationUrlRequest.js";
export { AuthorizationCodeRequest } from "./request/AuthorizationCodeRequest.js";
export { AuthenticationResult } from "./response/AuthenticationResult.js";
export { ClearCacheRequest } from "./request/ClearCacheRequest.js";
export { InitializeApplicationRequest } from "./request/InitializeApplicationRequest.js";
// Cache
export { LoadTokenOptions } from "./cache/TokenCache.js";
export { ITokenCache } from "./cache/ITokenCache.js";
// Storage
export { MemoryStorage } from "./cache/MemoryStorage.js";
export { LocalStorage } from "./cache/LocalStorage.js";
export { SessionStorage } from "./cache/SessionStorage.js";
export { IWindowStorage } from "./cache/IWindowStorage.js";
// Events
export {
EventMessage,
EventPayload,
EventError,
EventCallbackFunction,
EventMessageUtils,
PopupEvent,
} from "./event/EventMessage.js";
export { EventType } from "./event/EventType.js";
export { EventHandler } from "./event/EventHandler.js";
export {
SignedHttpRequest,
SignedHttpRequestOptions,
} from "./crypto/SignedHttpRequest.js";
export {
PopupWindowAttributes,
PopupSize,
PopupPosition,
} from "./request/PopupWindowAttributes.js";
// Telemetry
export { BrowserPerformanceClient } from "./telemetry/BrowserPerformanceClient.js";
export { BrowserPerformanceMeasurement } from "./telemetry/BrowserPerformanceMeasurement.js";
// Common Object Formats
export {
AuthenticationScheme,
// Account
AccountInfo,
AccountEntity,
IdTokenClaims,
// Error
AuthError,
AuthErrorCodes,
AuthErrorMessage,
ClientAuthError,
ClientAuthErrorCodes,
ClientAuthErrorMessage,
ClientConfigurationError,
ClientConfigurationErrorCodes,
ClientConfigurationErrorMessage,
InteractionRequiredAuthError,
InteractionRequiredAuthErrorCodes,
InteractionRequiredAuthErrorMessage,
ServerError,
// Network
INetworkModule,
NetworkResponse,
NetworkRequestOptions,
// Logger Object
ILoggerCallback,
Logger,
LogLevel,
// Protocol Mode
ProtocolMode,
ServerResponseType,
PromptValue,
// Server Response
ExternalTokenResponse,
// Utils
StringUtils,
UrlString,
JsonWebTokenTypes,
// AzureCloudInstance enum
AzureCloudInstance,
AzureCloudOptions,
AuthenticationHeaderParser,
OIDC_DEFAULT_SCOPES,
PerformanceCallbackFunction,
PerformanceEvent,
PerformanceEvents,
// Telemetry
InProgressPerformanceEvent,
TenantProfile,
IPerformanceClient,
StubPerformanceClient,
} from "@azure/msal-common/browser";
export { version } from "./packageMetadata.js";
@@ -0,0 +1,257 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ICrypto,
INetworkModule,
Logger,
AccountInfo,
AccountEntity,
UrlString,
ServerTelemetryManager,
ServerTelemetryRequest,
createClientConfigurationError,
ClientConfigurationErrorCodes,
Authority,
AuthorityOptions,
AuthorityFactory,
IPerformanceClient,
PerformanceEvents,
AzureCloudOptions,
invokeAsync,
StringDict,
} from "@azure/msal-common/browser";
import { BrowserConfiguration } from "../config/Configuration.js";
import { BrowserCacheManager } from "../cache/BrowserCacheManager.js";
import { EventHandler } from "../event/EventHandler.js";
import { EndSessionRequest } from "../request/EndSessionRequest.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import { version } from "../packageMetadata.js";
import { BrowserConstants } from "../utils/BrowserConstants.js";
import * as BrowserUtils from "../utils/BrowserUtils.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { ClearCacheRequest } from "../request/ClearCacheRequest.js";
import { createNewGuid } from "../crypto/BrowserCrypto.js";
export abstract class BaseInteractionClient {
protected config: BrowserConfiguration;
protected browserStorage: BrowserCacheManager;
protected browserCrypto: ICrypto;
protected networkClient: INetworkModule;
protected logger: Logger;
protected eventHandler: EventHandler;
protected navigationClient: INavigationClient;
protected nativeMessageHandler: NativeMessageHandler | undefined;
protected correlationId: string;
protected performanceClient: IPerformanceClient;
constructor(
config: BrowserConfiguration,
storageImpl: BrowserCacheManager,
browserCrypto: ICrypto,
logger: Logger,
eventHandler: EventHandler,
navigationClient: INavigationClient,
performanceClient: IPerformanceClient,
nativeMessageHandler?: NativeMessageHandler,
correlationId?: string
) {
this.config = config;
this.browserStorage = storageImpl;
this.browserCrypto = browserCrypto;
this.networkClient = this.config.system.networkClient;
this.eventHandler = eventHandler;
this.navigationClient = navigationClient;
this.nativeMessageHandler = nativeMessageHandler;
this.correlationId = correlationId || createNewGuid();
this.logger = logger.clone(
BrowserConstants.MSAL_SKU,
version,
this.correlationId
);
this.performanceClient = performanceClient;
}
abstract acquireToken(
request: RedirectRequest | PopupRequest | SsoSilentRequest
): Promise<AuthenticationResult | void>;
abstract logout(
request: EndSessionRequest | ClearCacheRequest | undefined
): Promise<void>;
protected async clearCacheOnLogout(
account?: AccountInfo | null
): Promise<void> {
if (account) {
if (
AccountEntity.accountInfoIsEqual(
account,
this.browserStorage.getActiveAccount(),
false
)
) {
this.logger.verbose("Setting active account to null");
this.browserStorage.setActiveAccount(null);
}
// Clear given account.
try {
await this.browserStorage.removeAccount(
AccountEntity.generateAccountCacheKey(account)
);
this.logger.verbose(
"Cleared cache items belonging to the account provided in the logout request."
);
} catch (error) {
this.logger.error(
"Account provided in logout request was not found. Local cache unchanged."
);
}
} else {
try {
this.logger.verbose(
"No account provided in logout request, clearing all cache items.",
this.correlationId
);
// Clear all accounts and tokens
await this.browserStorage.clear();
// Clear any stray keys from IndexedDB
await this.browserCrypto.clearKeystore();
} catch (e) {
this.logger.error(
"Attempted to clear all MSAL cache items and failed. Local cache unchanged."
);
}
}
}
/**
*
* Use to get the redirect uri configured in MSAL or null.
* @param requestRedirectUri
* @returns Redirect URL
*
*/
getRedirectUri(requestRedirectUri?: string): string {
this.logger.verbose("getRedirectUri called");
const redirectUri = requestRedirectUri || this.config.auth.redirectUri;
return UrlString.getAbsoluteUrl(
redirectUri,
BrowserUtils.getCurrentUri()
);
}
/**
*
* @param apiId
* @param correlationId
* @param forceRefresh
*/
protected initializeServerTelemetryManager(
apiId: number,
forceRefresh?: boolean
): ServerTelemetryManager {
this.logger.verbose("initializeServerTelemetryManager called");
const telemetryPayload: ServerTelemetryRequest = {
clientId: this.config.auth.clientId,
correlationId: this.correlationId,
apiId: apiId,
forceRefresh: forceRefresh || false,
wrapperSKU: this.browserStorage.getWrapperMetadata()[0],
wrapperVer: this.browserStorage.getWrapperMetadata()[1],
};
return new ServerTelemetryManager(
telemetryPayload,
this.browserStorage
);
}
/**
* Used to get a discovered version of the default authority.
* @param params {
* requestAuthority?: string;
* requestAzureCloudOptions?: AzureCloudOptions;
* requestExtraQueryParameters?: StringDict;
* account?: AccountInfo;
* }
*/
protected async getDiscoveredAuthority(params: {
requestAuthority?: string;
requestAzureCloudOptions?: AzureCloudOptions;
requestExtraQueryParameters?: StringDict;
account?: AccountInfo;
}): Promise<Authority> {
const { account } = params;
const instanceAwareEQ =
params.requestExtraQueryParameters &&
params.requestExtraQueryParameters.hasOwnProperty("instance_aware")
? params.requestExtraQueryParameters["instance_aware"]
: undefined;
this.performanceClient.addQueueMeasurement(
PerformanceEvents.StandardInteractionClientGetDiscoveredAuthority,
this.correlationId
);
const authorityOptions: AuthorityOptions = {
protocolMode: this.config.auth.protocolMode,
OIDCOptions: this.config.auth.OIDCOptions,
knownAuthorities: this.config.auth.knownAuthorities,
cloudDiscoveryMetadata: this.config.auth.cloudDiscoveryMetadata,
authorityMetadata: this.config.auth.authorityMetadata,
skipAuthorityMetadataCache:
this.config.auth.skipAuthorityMetadataCache,
};
// build authority string based on auth params, precedence - azureCloudInstance + tenant >> authority
const resolvedAuthority =
params.requestAuthority || this.config.auth.authority;
const resolvedInstanceAware = instanceAwareEQ?.length
? instanceAwareEQ === "true"
: this.config.auth.instanceAware;
const userAuthority =
account && resolvedInstanceAware
? this.config.auth.authority.replace(
UrlString.getDomainFromUrl(resolvedAuthority),
account.environment
)
: resolvedAuthority;
// fall back to the authority from config
const builtAuthority = Authority.generateAuthority(
userAuthority,
params.requestAzureCloudOptions ||
this.config.auth.azureCloudOptions
);
const discoveredAuthority = await invokeAsync(
AuthorityFactory.createDiscoveredInstance,
PerformanceEvents.AuthorityFactoryCreateDiscoveredInstance,
this.logger,
this.performanceClient,
this.correlationId
)(
builtAuthority,
this.config.system.networkClient,
this.browserStorage,
authorityOptions,
this.logger,
this.correlationId,
this.performanceClient
);
if (account && !discoveredAuthority.isAlias(account.environment)) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.authorityMismatch
);
}
return discoveredAuthority;
}
}
@@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthorizationCodeClient,
ClientConfiguration,
} from "@azure/msal-common/browser";
export class HybridSpaAuthorizationCodeClient extends AuthorizationCodeClient {
constructor(config: ClientConfiguration) {
super(config);
this.includeRedirectUri = false;
}
}
File diff suppressed because it is too large Load Diff
+820
View File
@@ -0,0 +1,820 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
CommonAuthorizationCodeRequest,
AuthorizationCodeClient,
ThrottlingUtils,
CommonEndSessionRequest,
UrlString,
AuthError,
OIDC_DEFAULT_SCOPES,
ProtocolUtils,
PerformanceEvents,
IPerformanceClient,
Logger,
ICrypto,
ProtocolMode,
ServerResponseType,
invokeAsync,
invoke,
PkceCodes,
} from "@azure/msal-common/browser";
import { StandardInteractionClient } from "./StandardInteractionClient.js";
import { EventType } from "../event/EventType.js";
import {
InteractionType,
ApiId,
BrowserConstants,
} from "../utils/BrowserConstants.js";
import { EndSessionPopupRequest } from "../request/EndSessionPopupRequest.js";
import { NavigationOptions } from "../navigation/NavigationOptions.js";
import * as BrowserUtils from "../utils/BrowserUtils.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { NativeInteractionClient } from "./NativeInteractionClient.js";
import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import { EventHandler } from "../event/EventHandler.js";
import { BrowserCacheManager } from "../cache/BrowserCacheManager.js";
import { BrowserConfiguration } from "../config/Configuration.js";
import { InteractionHandler } from "../interaction_handler/InteractionHandler.js";
import { PopupWindowAttributes } from "../request/PopupWindowAttributes.js";
import { EventError } from "../event/EventMessage.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import * as ResponseHandler from "../response/ResponseHandler.js";
export type PopupParams = {
popup?: Window | null;
popupName: string;
popupWindowAttributes: PopupWindowAttributes;
popupWindowParent: Window;
};
export class PopupClient extends StandardInteractionClient {
private currentWindow: Window | undefined;
protected nativeStorage: BrowserCacheManager;
constructor(
config: BrowserConfiguration,
storageImpl: BrowserCacheManager,
browserCrypto: ICrypto,
logger: Logger,
eventHandler: EventHandler,
navigationClient: INavigationClient,
performanceClient: IPerformanceClient,
nativeStorageImpl: BrowserCacheManager,
nativeMessageHandler?: NativeMessageHandler,
correlationId?: string
) {
super(
config,
storageImpl,
browserCrypto,
logger,
eventHandler,
navigationClient,
performanceClient,
nativeMessageHandler,
correlationId
);
// Properly sets this reference for the unload event.
this.unloadWindow = this.unloadWindow.bind(this);
this.nativeStorage = nativeStorageImpl;
}
/**
* Acquires tokens by opening a popup window to the /authorize endpoint of the authority
* @param request
* @param pkceCodes
*/
acquireToken(
request: PopupRequest,
pkceCodes?: PkceCodes
): Promise<AuthenticationResult> {
try {
const popupName = this.generatePopupName(
request.scopes || OIDC_DEFAULT_SCOPES,
request.authority || this.config.auth.authority
);
const popupParams: PopupParams = {
popupName,
popupWindowAttributes: request.popupWindowAttributes || {},
popupWindowParent: request.popupWindowParent ?? window,
};
this.performanceClient.addFields(
{ isAsyncPopup: this.config.system.asyncPopups },
this.correlationId
);
// asyncPopups flag is true. Acquires token without first opening popup. Popup will be opened later asynchronously.
if (this.config.system.asyncPopups) {
this.logger.verbose("asyncPopups set to true, acquiring token");
// Passes on popup position and dimensions if in request
return this.acquireTokenPopupAsync(
request,
popupParams,
pkceCodes
);
} else {
// asyncPopups flag is set to false. Opens popup before acquiring token.
this.logger.verbose(
"asyncPopup set to false, opening popup before acquiring token"
);
popupParams.popup = this.openSizedPopup(
"about:blank",
popupParams
);
return this.acquireTokenPopupAsync(
request,
popupParams,
pkceCodes
);
}
} catch (e) {
return Promise.reject(e);
}
}
/**
* Clears local cache for the current user then opens a popup window prompting the user to sign-out of the server
* @param logoutRequest
*/
logout(logoutRequest?: EndSessionPopupRequest): Promise<void> {
try {
this.logger.verbose("logoutPopup called");
const validLogoutRequest =
this.initializeLogoutRequest(logoutRequest);
const popupParams: PopupParams = {
popupName: this.generateLogoutPopupName(validLogoutRequest),
popupWindowAttributes:
logoutRequest?.popupWindowAttributes || {},
popupWindowParent: logoutRequest?.popupWindowParent ?? window,
};
const authority = logoutRequest && logoutRequest.authority;
const mainWindowRedirectUri =
logoutRequest && logoutRequest.mainWindowRedirectUri;
// asyncPopups flag is true. Acquires token without first opening popup. Popup will be opened later asynchronously.
if (this.config.system.asyncPopups) {
this.logger.verbose("asyncPopups set to true");
// Passes on popup position and dimensions if in request
return this.logoutPopupAsync(
validLogoutRequest,
popupParams,
authority,
mainWindowRedirectUri
);
} else {
// asyncPopups flag is set to false. Opens popup before logging out.
this.logger.verbose("asyncPopup set to false, opening popup");
popupParams.popup = this.openSizedPopup(
"about:blank",
popupParams
);
return this.logoutPopupAsync(
validLogoutRequest,
popupParams,
authority,
mainWindowRedirectUri
);
}
} catch (e) {
// Since this function is synchronous we need to reject
return Promise.reject(e);
}
}
/**
* Helper which obtains an access_token for your API via opening a popup window in the user's browser
* @param request
* @param popupParams
* @param pkceCodes
*
* @returns A promise that is fulfilled when this function has completed, or rejected if an error was raised.
*/
protected async acquireTokenPopupAsync(
request: PopupRequest,
popupParams: PopupParams,
pkceCodes?: PkceCodes
): Promise<AuthenticationResult> {
this.logger.verbose("acquireTokenPopupAsync called");
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.acquireTokenPopup
);
const validRequest = await invokeAsync(
this.initializeAuthorizationRequest.bind(this),
PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest,
this.logger,
this.performanceClient,
this.correlationId
)(request, InteractionType.Popup);
/*
* Skip pre-connect for async popups to reduce time between user interaction and popup window creation to avoid
* popup from being blocked by browsers with shorter popup timers
*/
if (popupParams.popup) {
BrowserUtils.preconnect(validRequest.authority);
}
try {
// Create auth code request and generate PKCE params
const authCodeRequest: CommonAuthorizationCodeRequest =
await invokeAsync(
this.initializeAuthorizationCodeRequest.bind(this),
PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest,
this.logger,
this.performanceClient,
this.correlationId
)(validRequest, pkceCodes);
// Initialize the client
const authClient: AuthorizationCodeClient = await invokeAsync(
this.createAuthCodeClient.bind(this),
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
this.logger,
this.performanceClient,
this.correlationId
)({
serverTelemetryManager,
requestAuthority: validRequest.authority,
requestAzureCloudOptions: validRequest.azureCloudOptions,
requestExtraQueryParameters: validRequest.extraQueryParameters,
account: validRequest.account,
});
const isPlatformBroker =
NativeMessageHandler.isPlatformBrokerAvailable(
this.config,
this.logger,
this.nativeMessageHandler,
request.authenticationScheme
);
// Start measurement for server calls with native brokering enabled
let fetchNativeAccountIdMeasurement;
if (isPlatformBroker) {
fetchNativeAccountIdMeasurement =
this.performanceClient.startMeasurement(
PerformanceEvents.FetchAccountIdWithNativeBroker,
request.correlationId
);
}
// Create acquire token url.
const navigateUrl = await authClient.getAuthCodeUrl({
...validRequest,
platformBroker: isPlatformBroker,
});
// Create popup interaction handler.
const interactionHandler = new InteractionHandler(
authClient,
this.browserStorage,
authCodeRequest,
this.logger,
this.performanceClient
);
// Show the UI once the url has been created. Get the window handle for the popup.
const popupWindow: Window = this.initiateAuthRequest(
navigateUrl,
popupParams
);
this.eventHandler.emitEvent(
EventType.POPUP_OPENED,
InteractionType.Popup,
{ popupWindow },
null
);
// 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 responseString = await this.monitorPopupForHash(
popupWindow,
popupParams.popupWindowParent
);
const serverParams = invoke(
ResponseHandler.deserializeResponse,
PerformanceEvents.DeserializeResponse,
this.logger,
this.performanceClient,
this.correlationId
)(
responseString,
this.config.auth.OIDCOptions.serverResponseType,
this.logger
);
// Remove throttle if it exists
ThrottlingUtils.removeThrottle(
this.browserStorage,
this.config.auth.clientId,
authCodeRequest
);
if (serverParams.accountId) {
this.logger.verbose(
"Account id found in hash, calling WAM for token"
);
// end measurement for server call with native brokering enabled
if (fetchNativeAccountIdMeasurement) {
fetchNativeAccountIdMeasurement.end({
success: true,
isNativeBroker: true,
});
}
if (!this.nativeMessageHandler) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.nativeConnectionNotEstablished
);
}
const nativeInteractionClient = new NativeInteractionClient(
this.config,
this.browserStorage,
this.browserCrypto,
this.logger,
this.eventHandler,
this.navigationClient,
ApiId.acquireTokenPopup,
this.performanceClient,
this.nativeMessageHandler,
serverParams.accountId,
this.nativeStorage,
validRequest.correlationId
);
const { userRequestState } = ProtocolUtils.parseRequestState(
this.browserCrypto,
validRequest.state
);
return await nativeInteractionClient.acquireToken({
...validRequest,
state: userRequestState,
prompt: undefined, // Server should handle the prompt, ideally native broker can do this part silently
});
}
// Handle response from hash string.
const result = await interactionHandler.handleCodeResponse(
serverParams,
validRequest
);
return result;
} catch (e) {
// Close the synchronous popup if an error is thrown before the window unload event is registered
popupParams.popup?.close();
if (e instanceof AuthError) {
(e as AuthError).setCorrelationId(this.correlationId);
serverTelemetryManager.cacheFailedRequest(e);
}
throw e;
}
}
/**
*
* @param validRequest
* @param popupName
* @param requestAuthority
* @param popup
* @param mainWindowRedirectUri
* @param popupWindowAttributes
*/
protected async logoutPopupAsync(
validRequest: CommonEndSessionRequest,
popupParams: PopupParams,
requestAuthority?: string,
mainWindowRedirectUri?: string
): Promise<void> {
this.logger.verbose("logoutPopupAsync called");
this.eventHandler.emitEvent(
EventType.LOGOUT_START,
InteractionType.Popup,
validRequest
);
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.logoutPopup
);
try {
// Clear cache on logout
await this.clearCacheOnLogout(validRequest.account);
// Initialize the client
const authClient = await invokeAsync(
this.createAuthCodeClient.bind(this),
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
this.logger,
this.performanceClient,
this.correlationId
)({
serverTelemetryManager,
requestAuthority: requestAuthority,
account: validRequest.account || undefined,
});
try {
authClient.authority.endSessionEndpoint;
} catch {
if (
validRequest.account?.homeAccountId &&
validRequest.postLogoutRedirectUri &&
authClient.authority.protocolMode === ProtocolMode.OIDC
) {
void this.browserStorage.removeAccount(
validRequest.account?.homeAccountId
);
this.eventHandler.emitEvent(
EventType.LOGOUT_SUCCESS,
InteractionType.Popup,
validRequest
);
if (mainWindowRedirectUri) {
const navigationOptions: NavigationOptions = {
apiId: ApiId.logoutPopup,
timeout:
this.config.system.redirectNavigationTimeout,
noHistory: false,
};
const absoluteUrl = UrlString.getAbsoluteUrl(
mainWindowRedirectUri,
BrowserUtils.getCurrentUri()
);
await this.navigationClient.navigateInternal(
absoluteUrl,
navigationOptions
);
}
popupParams.popup?.close();
return;
}
}
// Create logout string and navigate user window to logout.
const logoutUri: string = authClient.getLogoutUri(validRequest);
this.eventHandler.emitEvent(
EventType.LOGOUT_SUCCESS,
InteractionType.Popup,
validRequest
);
// Open the popup window to requestUrl.
const popupWindow = this.openPopup(logoutUri, popupParams);
this.eventHandler.emitEvent(
EventType.POPUP_OPENED,
InteractionType.Popup,
{ popupWindow },
null
);
await this.monitorPopupForHash(
popupWindow,
popupParams.popupWindowParent
).catch(() => {
// Swallow any errors related to monitoring the window. Server logout is best effort
});
if (mainWindowRedirectUri) {
const navigationOptions: NavigationOptions = {
apiId: ApiId.logoutPopup,
timeout: this.config.system.redirectNavigationTimeout,
noHistory: false,
};
const absoluteUrl = UrlString.getAbsoluteUrl(
mainWindowRedirectUri,
BrowserUtils.getCurrentUri()
);
this.logger.verbose(
"Redirecting main window to url specified in the request"
);
this.logger.verbosePii(
`Redirecting main window to: ${absoluteUrl}`
);
await this.navigationClient.navigateInternal(
absoluteUrl,
navigationOptions
);
} else {
this.logger.verbose("No main window navigation requested");
}
} catch (e) {
// Close the synchronous popup if an error is thrown before the window unload event is registered
popupParams.popup?.close();
if (e instanceof AuthError) {
(e as AuthError).setCorrelationId(this.correlationId);
serverTelemetryManager.cacheFailedRequest(e);
}
this.browserStorage.setInteractionInProgress(false);
this.eventHandler.emitEvent(
EventType.LOGOUT_FAILURE,
InteractionType.Popup,
null,
e as EventError
);
this.eventHandler.emitEvent(
EventType.LOGOUT_END,
InteractionType.Popup
);
throw e;
}
this.eventHandler.emitEvent(
EventType.LOGOUT_END,
InteractionType.Popup
);
}
/**
* Opens a popup window with given request Url.
* @param requestUrl
*/
initiateAuthRequest(requestUrl: string, params: PopupParams): Window {
// Check that request url is not empty.
if (requestUrl) {
this.logger.infoPii(`Navigate to: ${requestUrl}`);
// Open the popup window to requestUrl.
return this.openPopup(requestUrl, params);
} else {
// Throw error if request URL is empty.
this.logger.error("Navigate url is empty");
throw createBrowserAuthError(
BrowserAuthErrorCodes.emptyNavigateUri
);
}
}
/**
* Monitors a window until it loads a url with the same origin.
* @param popupWindow - window that is being monitored
* @param timeout - timeout for processing hash once popup is redirected back to application
*/
monitorPopupForHash(
popupWindow: Window,
popupWindowParent: Window
): Promise<string> {
return new Promise<string>((resolve, reject) => {
this.logger.verbose(
"PopupHandler.monitorPopupForHash - polling started"
);
const intervalId = setInterval(() => {
// Window is closed
if (popupWindow.closed) {
this.logger.error(
"PopupHandler.monitorPopupForHash - window closed"
);
clearInterval(intervalId);
reject(
createBrowserAuthError(
BrowserAuthErrorCodes.userCancelled
)
);
return;
}
let href = "";
try {
/*
* Will throw if cross origin,
* which should be caught and ignored
* since we need the interval to keep running while on STS UI.
*/
href = popupWindow.location.href;
} catch (e) {}
// Don't process blank pages or cross domain
if (!href || href === "about:blank") {
return;
}
clearInterval(intervalId);
let responseString = "";
const responseType =
this.config.auth.OIDCOptions.serverResponseType;
if (popupWindow) {
if (responseType === ServerResponseType.QUERY) {
responseString = popupWindow.location.search;
} else {
responseString = popupWindow.location.hash;
}
}
this.logger.verbose(
"PopupHandler.monitorPopupForHash - popup window is on same origin as caller"
);
resolve(responseString);
}, this.config.system.pollIntervalMilliseconds);
}).finally(() => {
this.cleanPopup(popupWindow, popupWindowParent);
});
}
/**
* @hidden
*
* Configures popup window for login.
*
* @param urlNavigate
* @param title
* @param popUpWidth
* @param popUpHeight
* @param popupWindowAttributes
* @ignore
* @hidden
*/
openPopup(urlNavigate: string, popupParams: PopupParams): Window {
try {
let popupWindow;
// Popup window passed in, setting url to navigate to
if (popupParams.popup) {
popupWindow = popupParams.popup;
this.logger.verbosePii(
`Navigating popup window to: ${urlNavigate}`
);
popupWindow.location.assign(urlNavigate);
} else if (typeof popupParams.popup === "undefined") {
// Popup will be undefined if it was not passed in
this.logger.verbosePii(
`Opening popup window to: ${urlNavigate}`
);
popupWindow = this.openSizedPopup(urlNavigate, popupParams);
}
// Popup will be null if popups are blocked
if (!popupWindow) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.emptyWindowError
);
}
if (popupWindow.focus) {
popupWindow.focus();
}
this.currentWindow = popupWindow;
popupParams.popupWindowParent.addEventListener(
"beforeunload",
this.unloadWindow
);
return popupWindow;
} catch (e) {
this.logger.error(
"error opening popup " + (e as AuthError).message
);
this.browserStorage.setInteractionInProgress(false);
throw createBrowserAuthError(
BrowserAuthErrorCodes.popupWindowError
);
}
}
/**
* Helper function to set popup window dimensions and position
* @param urlNavigate
* @param popupName
* @param popupWindowAttributes
* @returns
*/
openSizedPopup(
urlNavigate: string,
{ popupName, popupWindowAttributes, popupWindowParent }: PopupParams
): Window | null {
/**
* adding winLeft and winTop to account for dual monitor
* using screenLeft and screenTop for IE8 and earlier
*/
const winLeft = popupWindowParent.screenLeft
? popupWindowParent.screenLeft
: popupWindowParent.screenX;
const winTop = popupWindowParent.screenTop
? popupWindowParent.screenTop
: popupWindowParent.screenY;
/**
* window.innerWidth displays browser window"s height and width excluding toolbars
* using document.documentElement.clientWidth for IE8 and earlier
*/
const winWidth =
popupWindowParent.innerWidth ||
document.documentElement.clientWidth ||
document.body.clientWidth;
const winHeight =
popupWindowParent.innerHeight ||
document.documentElement.clientHeight ||
document.body.clientHeight;
let width = popupWindowAttributes.popupSize?.width;
let height = popupWindowAttributes.popupSize?.height;
let top = popupWindowAttributes.popupPosition?.top;
let left = popupWindowAttributes.popupPosition?.left;
if (!width || width < 0 || width > winWidth) {
this.logger.verbose(
"Default popup window width used. Window width not configured or invalid."
);
width = BrowserConstants.POPUP_WIDTH;
}
if (!height || height < 0 || height > winHeight) {
this.logger.verbose(
"Default popup window height used. Window height not configured or invalid."
);
height = BrowserConstants.POPUP_HEIGHT;
}
if (!top || top < 0 || top > winHeight) {
this.logger.verbose(
"Default popup window top position used. Window top not configured or invalid."
);
top = Math.max(
0,
winHeight / 2 - BrowserConstants.POPUP_HEIGHT / 2 + winTop
);
}
if (!left || left < 0 || left > winWidth) {
this.logger.verbose(
"Default popup window left position used. Window left not configured or invalid."
);
left = Math.max(
0,
winWidth / 2 - BrowserConstants.POPUP_WIDTH / 2 + winLeft
);
}
return popupWindowParent.open(
urlNavigate,
popupName,
`width=${width}, height=${height}, top=${top}, left=${left}, scrollbars=yes`
);
}
/**
* Event callback to unload main window.
*/
unloadWindow(e: Event): void {
this.browserStorage.cleanRequestByInteractionType(
InteractionType.Popup
);
if (this.currentWindow) {
this.currentWindow.close();
}
// Guarantees browser unload will happen, so no other errors will be thrown.
e.preventDefault();
}
/**
* Closes popup, removes any state vars created during popup calls.
* @param popupWindow
*/
cleanPopup(popupWindow: Window, popupWindowParent: Window): void {
// Close window.
popupWindow.close();
// Remove window unload function
popupWindowParent.removeEventListener(
"beforeunload",
this.unloadWindow
);
// Interaction is completed - remove interaction status.
this.browserStorage.setInteractionInProgress(false);
}
/**
* Generates the name for the popup based on the client id and request
* @param clientId
* @param request
*/
generatePopupName(scopes: Array<string>, authority: string): string {
return `${BrowserConstants.POPUP_NAME_PREFIX}.${
this.config.auth.clientId
}.${scopes.join("-")}.${authority}.${this.correlationId}`;
}
/**
* Generates the name for the popup based on the client id and request for logouts
* @param clientId
* @param request
*/
generateLogoutPopupName(request: CommonEndSessionRequest): string {
const homeAccountId = request.account && request.account.homeAccountId;
return `${BrowserConstants.POPUP_NAME_PREFIX}.${this.config.auth.clientId}.${homeAccountId}.${this.correlationId}`;
}
}
@@ -0,0 +1,666 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
CommonAuthorizationCodeRequest,
AuthorizationCodeClient,
UrlString,
AuthError,
ServerTelemetryManager,
Constants,
ProtocolUtils,
ServerAuthorizationCodeResponse,
ThrottlingUtils,
ICrypto,
Logger,
IPerformanceClient,
PerformanceEvents,
ProtocolMode,
invokeAsync,
ServerResponseType,
UrlUtils,
InProgressPerformanceEvent,
} from "@azure/msal-common/browser";
import { StandardInteractionClient } from "./StandardInteractionClient.js";
import {
ApiId,
InteractionType,
TemporaryCacheKeys,
} from "../utils/BrowserConstants.js";
import { RedirectHandler } from "../interaction_handler/RedirectHandler.js";
import * as BrowserUtils from "../utils/BrowserUtils.js";
import { EndSessionRequest } from "../request/EndSessionRequest.js";
import { EventType } from "../event/EventType.js";
import { NavigationOptions } from "../navigation/NavigationOptions.js";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { NativeInteractionClient } from "./NativeInteractionClient.js";
import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js";
import { BrowserConfiguration } from "../config/Configuration.js";
import { BrowserCacheManager } from "../cache/BrowserCacheManager.js";
import { EventHandler } from "../event/EventHandler.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import { EventError } from "../event/EventMessage.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import * as ResponseHandler from "../response/ResponseHandler.js";
function getNavigationType(): NavigationTimingType | undefined {
if (
typeof window === "undefined" ||
typeof window.performance === "undefined" ||
typeof window.performance.getEntriesByType !== "function"
) {
return undefined;
}
const navigationEntries = window.performance.getEntriesByType("navigation");
const navigation = navigationEntries.length
? (navigationEntries[0] as PerformanceNavigationTiming)
: undefined;
return navigation?.type;
}
export class RedirectClient extends StandardInteractionClient {
protected nativeStorage: BrowserCacheManager;
constructor(
config: BrowserConfiguration,
storageImpl: BrowserCacheManager,
browserCrypto: ICrypto,
logger: Logger,
eventHandler: EventHandler,
navigationClient: INavigationClient,
performanceClient: IPerformanceClient,
nativeStorageImpl: BrowserCacheManager,
nativeMessageHandler?: NativeMessageHandler,
correlationId?: string
) {
super(
config,
storageImpl,
browserCrypto,
logger,
eventHandler,
navigationClient,
performanceClient,
nativeMessageHandler,
correlationId
);
this.nativeStorage = nativeStorageImpl;
}
/**
* Redirects the page to the /authorize endpoint of the IDP
* @param request
*/
async acquireToken(request: RedirectRequest): Promise<void> {
const validRequest = await invokeAsync(
this.initializeAuthorizationRequest.bind(this),
PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest,
this.logger,
this.performanceClient,
this.correlationId
)(request, InteractionType.Redirect);
this.browserStorage.updateCacheEntries(
validRequest.state,
validRequest.nonce,
validRequest.authority,
validRequest.loginHint || "",
validRequest.account || null
);
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.acquireTokenRedirect
);
const handleBackButton = (event: PageTransitionEvent) => {
// Clear temporary cache if the back button is clicked during the redirect flow.
if (event.persisted) {
this.logger.verbose(
"Page was restored from back/forward cache. Clearing temporary cache."
);
this.browserStorage.cleanRequestByState(validRequest.state);
this.eventHandler.emitEvent(
EventType.RESTORE_FROM_BFCACHE,
InteractionType.Redirect
);
}
};
try {
// Create auth code request and generate PKCE params
const authCodeRequest: CommonAuthorizationCodeRequest =
await invokeAsync(
this.initializeAuthorizationCodeRequest.bind(this),
PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest,
this.logger,
this.performanceClient,
this.correlationId
)(validRequest);
// Initialize the client
const authClient: AuthorizationCodeClient = await invokeAsync(
this.createAuthCodeClient.bind(this),
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
this.logger,
this.performanceClient,
this.correlationId
)({
serverTelemetryManager,
requestAuthority: validRequest.authority,
requestAzureCloudOptions: validRequest.azureCloudOptions,
requestExtraQueryParameters: validRequest.extraQueryParameters,
account: validRequest.account,
});
// Create redirect interaction handler.
const interactionHandler = new RedirectHandler(
authClient,
this.browserStorage,
authCodeRequest,
this.logger,
this.performanceClient
);
// Create acquire token url.
const navigateUrl = await authClient.getAuthCodeUrl({
...validRequest,
platformBroker: NativeMessageHandler.isPlatformBrokerAvailable(
this.config,
this.logger,
this.nativeMessageHandler,
request.authenticationScheme
),
});
const redirectStartPage = this.getRedirectStartPage(
request.redirectStartPage
);
this.logger.verbosePii(`Redirect start page: ${redirectStartPage}`);
// Clear temporary cache if the back button is clicked during the redirect flow.
window.addEventListener("pageshow", handleBackButton);
// Show the UI once the url has been created. Response will come back in the hash, which will be handled in the handleRedirectCallback function.
return await interactionHandler.initiateAuthRequest(navigateUrl, {
navigationClient: this.navigationClient,
redirectTimeout: this.config.system.redirectNavigationTimeout,
redirectStartPage: redirectStartPage,
onRedirectNavigate:
request.onRedirectNavigate ||
this.config.auth.onRedirectNavigate,
});
} catch (e) {
if (e instanceof AuthError) {
e.setCorrelationId(this.correlationId);
serverTelemetryManager.cacheFailedRequest(e);
}
window.removeEventListener("pageshow", handleBackButton);
this.browserStorage.cleanRequestByState(validRequest.state);
throw e;
}
}
/**
* Checks if navigateToLoginRequestUrl is set, and:
* - if true, performs logic to cache and navigate
* - if false, handles hash string and parses response
* @param hash {string} url hash
* @param parentMeasurement {InProgressPerformanceEvent} parent measurement
*/
async handleRedirectPromise(
hash: string = "",
parentMeasurement: InProgressPerformanceEvent
): Promise<AuthenticationResult | null> {
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.handleRedirectPromise
);
try {
if (!this.browserStorage.isInteractionInProgress(true)) {
this.logger.info(
"handleRedirectPromise called but there is no interaction in progress, returning null."
);
return null;
}
const [serverParams, responseString] = this.getRedirectResponse(
hash || ""
);
if (!serverParams) {
// Not a recognized server response hash or hash not associated with a redirect request
this.logger.info(
"handleRedirectPromise did not detect a response as a result of a redirect. Cleaning temporary cache."
);
this.browserStorage.cleanRequestByInteractionType(
InteractionType.Redirect
);
// Do not instrument "no_server_response" if user clicked back button
if (getNavigationType() !== "back_forward") {
parentMeasurement.event.errorCode = "no_server_response";
} else {
this.logger.verbose(
"Back navigation event detected. Muting no_server_response error"
);
}
return null;
}
// If navigateToLoginRequestUrl is true, get the url where the redirect request was initiated
const loginRequestUrl =
this.browserStorage.getTemporaryCache(
TemporaryCacheKeys.ORIGIN_URI,
true
) || Constants.EMPTY_STRING;
const loginRequestUrlNormalized =
UrlString.removeHashFromUrl(loginRequestUrl);
const currentUrlNormalized = UrlString.removeHashFromUrl(
window.location.href
);
if (
loginRequestUrlNormalized === currentUrlNormalized &&
this.config.auth.navigateToLoginRequestUrl
) {
// We are on the page we need to navigate to - handle hash
this.logger.verbose(
"Current page is loginRequestUrl, handling response"
);
if (loginRequestUrl.indexOf("#") > -1) {
// Replace current hash with non-msal hash, if present
BrowserUtils.replaceHash(loginRequestUrl);
}
const handleHashResult = await this.handleResponse(
serverParams,
serverTelemetryManager
);
return handleHashResult;
} else if (!this.config.auth.navigateToLoginRequestUrl) {
this.logger.verbose(
"NavigateToLoginRequestUrl set to false, handling response"
);
return await this.handleResponse(
serverParams,
serverTelemetryManager
);
} else if (
!BrowserUtils.isInIframe() ||
this.config.system.allowRedirectInIframe
) {
/*
* Returned from authority using redirect - need to perform navigation before processing response
* Cache the hash to be retrieved after the next redirect
*/
this.browserStorage.setTemporaryCache(
TemporaryCacheKeys.URL_HASH,
responseString,
true
);
const navigationOptions: NavigationOptions = {
apiId: ApiId.handleRedirectPromise,
timeout: this.config.system.redirectNavigationTimeout,
noHistory: true,
};
/**
* Default behavior is to redirect to the start page and not process the hash now.
* The start page is expected to also call handleRedirectPromise which will process the hash in one of the checks above.
*/
let processHashOnRedirect: boolean = true;
if (!loginRequestUrl || loginRequestUrl === "null") {
// Redirect to home page if login request url is null (real null or the string null)
const homepage = BrowserUtils.getHomepage();
// Cache the homepage under ORIGIN_URI to ensure cached hash is processed on homepage
this.browserStorage.setTemporaryCache(
TemporaryCacheKeys.ORIGIN_URI,
homepage,
true
);
this.logger.warning(
"Unable to get valid login request url from cache, redirecting to home page"
);
processHashOnRedirect =
await this.navigationClient.navigateInternal(
homepage,
navigationOptions
);
} else {
// Navigate to page that initiated the redirect request
this.logger.verbose(
`Navigating to loginRequestUrl: ${loginRequestUrl}`
);
processHashOnRedirect =
await this.navigationClient.navigateInternal(
loginRequestUrl,
navigationOptions
);
}
// If navigateInternal implementation returns false, handle the hash now
if (!processHashOnRedirect) {
return await this.handleResponse(
serverParams,
serverTelemetryManager
);
}
}
return null;
} catch (e) {
if (e instanceof AuthError) {
(e as AuthError).setCorrelationId(this.correlationId);
serverTelemetryManager.cacheFailedRequest(e);
}
this.browserStorage.cleanRequestByInteractionType(
InteractionType.Redirect
);
throw e;
}
}
/**
* Gets the response hash for a redirect request
* Returns null if interactionType in the state value is not "redirect" or the hash does not contain known properties
* @param hash
*/
protected getRedirectResponse(
userProvidedResponse: string
): [ServerAuthorizationCodeResponse | null, string] {
this.logger.verbose("getRedirectResponseHash called");
// Get current location hash from window or cache.
let responseString = userProvidedResponse;
if (!responseString) {
if (
this.config.auth.OIDCOptions.serverResponseType ===
ServerResponseType.QUERY
) {
responseString = window.location.search;
} else {
responseString = window.location.hash;
}
}
let response = UrlUtils.getDeserializedResponse(responseString);
if (response) {
try {
ResponseHandler.validateInteractionType(
response,
this.browserCrypto,
InteractionType.Redirect
);
} catch (e) {
if (e instanceof AuthError) {
this.logger.error(
`Interaction type validation failed due to ${e.errorCode}: ${e.errorMessage}`
);
}
return [null, ""];
}
BrowserUtils.clearHash(window);
this.logger.verbose(
"Hash contains known properties, returning response hash"
);
return [response, responseString];
}
const cachedHash = this.browserStorage.getTemporaryCache(
TemporaryCacheKeys.URL_HASH,
true
);
this.browserStorage.removeItem(
this.browserStorage.generateCacheKey(TemporaryCacheKeys.URL_HASH)
);
if (cachedHash) {
response = UrlUtils.getDeserializedResponse(cachedHash);
if (response) {
this.logger.verbose(
"Hash does not contain known properties, returning cached hash"
);
return [response, cachedHash];
}
}
return [null, ""];
}
/**
* Checks if hash exists and handles in window.
* @param hash
* @param state
*/
protected async handleResponse(
serverParams: ServerAuthorizationCodeResponse,
serverTelemetryManager: ServerTelemetryManager
): Promise<AuthenticationResult> {
const state = serverParams.state;
if (!state) {
throw createBrowserAuthError(BrowserAuthErrorCodes.noStateInHash);
}
const cachedRequest = this.browserStorage.getCachedRequest(state);
this.logger.verbose("handleResponse called, retrieved cached request");
if (serverParams.accountId) {
this.logger.verbose(
"Account id found in hash, calling WAM for token"
);
if (!this.nativeMessageHandler) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.nativeConnectionNotEstablished
);
}
const nativeInteractionClient = new NativeInteractionClient(
this.config,
this.browserStorage,
this.browserCrypto,
this.logger,
this.eventHandler,
this.navigationClient,
ApiId.acquireTokenPopup,
this.performanceClient,
this.nativeMessageHandler,
serverParams.accountId,
this.nativeStorage,
cachedRequest.correlationId
);
const { userRequestState } = ProtocolUtils.parseRequestState(
this.browserCrypto,
state
);
return nativeInteractionClient
.acquireToken({
...cachedRequest,
state: userRequestState,
prompt: undefined, // Server should handle the prompt, ideally native broker can do this part silently
})
.finally(() => {
this.browserStorage.cleanRequestByState(state);
});
}
// Hash contains known properties - handle and return in callback
const currentAuthority = this.browserStorage.getCachedAuthority(state);
if (!currentAuthority) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.noCachedAuthorityError
);
}
const authClient = await invokeAsync(
this.createAuthCodeClient.bind(this),
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
this.logger,
this.performanceClient,
this.correlationId
)({ serverTelemetryManager, requestAuthority: currentAuthority });
ThrottlingUtils.removeThrottle(
this.browserStorage,
this.config.auth.clientId,
cachedRequest
);
const interactionHandler = new RedirectHandler(
authClient,
this.browserStorage,
cachedRequest,
this.logger,
this.performanceClient
);
return interactionHandler.handleCodeResponse(serverParams, state);
}
/**
* Use to log out the current user, and redirect the user to the postLogoutRedirectUri.
* Default behaviour is to redirect the user to `window.location.href`.
* @param logoutRequest
*/
async logout(logoutRequest?: EndSessionRequest): Promise<void> {
this.logger.verbose("logoutRedirect called");
const validLogoutRequest = this.initializeLogoutRequest(logoutRequest);
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.logout
);
try {
this.eventHandler.emitEvent(
EventType.LOGOUT_START,
InteractionType.Redirect,
logoutRequest
);
// Clear cache on logout
await this.clearCacheOnLogout(validLogoutRequest.account);
const navigationOptions: NavigationOptions = {
apiId: ApiId.logout,
timeout: this.config.system.redirectNavigationTimeout,
noHistory: false,
};
const authClient = await invokeAsync(
this.createAuthCodeClient.bind(this),
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
this.logger,
this.performanceClient,
this.correlationId
)({
serverTelemetryManager,
requestAuthority: logoutRequest && logoutRequest.authority,
requestExtraQueryParameters:
logoutRequest?.extraQueryParameters,
account: (logoutRequest && logoutRequest.account) || undefined,
});
if (authClient.authority.protocolMode === ProtocolMode.OIDC) {
try {
authClient.authority.endSessionEndpoint;
} catch {
if (validLogoutRequest.account?.homeAccountId) {
void this.browserStorage.removeAccount(
validLogoutRequest.account?.homeAccountId
);
this.eventHandler.emitEvent(
EventType.LOGOUT_SUCCESS,
InteractionType.Redirect,
validLogoutRequest
);
return;
}
}
}
// Create logout string and navigate user window to logout.
const logoutUri: string =
authClient.getLogoutUri(validLogoutRequest);
this.eventHandler.emitEvent(
EventType.LOGOUT_SUCCESS,
InteractionType.Redirect,
validLogoutRequest
);
// Check if onRedirectNavigate is implemented, and invoke it if so
if (
logoutRequest &&
typeof logoutRequest.onRedirectNavigate === "function"
) {
const navigate = logoutRequest.onRedirectNavigate(logoutUri);
if (navigate !== false) {
this.logger.verbose(
"Logout onRedirectNavigate did not return false, navigating"
);
// Ensure interaction is in progress
if (!this.browserStorage.getInteractionInProgress()) {
this.browserStorage.setInteractionInProgress(true);
}
await this.navigationClient.navigateExternal(
logoutUri,
navigationOptions
);
return;
} else {
// Ensure interaction is not in progress
this.browserStorage.setInteractionInProgress(false);
this.logger.verbose(
"Logout onRedirectNavigate returned false, stopping navigation"
);
}
} else {
// Ensure interaction is in progress
if (!this.browserStorage.getInteractionInProgress()) {
this.browserStorage.setInteractionInProgress(true);
}
await this.navigationClient.navigateExternal(
logoutUri,
navigationOptions
);
return;
}
} catch (e) {
if (e instanceof AuthError) {
(e as AuthError).setCorrelationId(this.correlationId);
serverTelemetryManager.cacheFailedRequest(e);
}
this.eventHandler.emitEvent(
EventType.LOGOUT_FAILURE,
InteractionType.Redirect,
null,
e as EventError
);
this.eventHandler.emitEvent(
EventType.LOGOUT_END,
InteractionType.Redirect
);
throw e;
}
this.eventHandler.emitEvent(
EventType.LOGOUT_END,
InteractionType.Redirect
);
}
/**
* Use to get the redirectStartPage either from request or use current window
* @param requestStartPage
*/
protected getRedirectStartPage(requestStartPage?: string): string {
const redirectStartPage = requestStartPage || window.location.href;
return UrlString.getAbsoluteUrl(
redirectStartPage,
BrowserUtils.getCurrentUri()
);
}
}
@@ -0,0 +1,161 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ICrypto,
Logger,
CommonAuthorizationCodeRequest,
AuthError,
IPerformanceClient,
PerformanceEvents,
invokeAsync,
} from "@azure/msal-common/browser";
import { StandardInteractionClient } from "./StandardInteractionClient.js";
import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js";
import { BrowserConfiguration } from "../config/Configuration.js";
import { BrowserCacheManager } from "../cache/BrowserCacheManager.js";
import { EventHandler } from "../event/EventHandler.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { InteractionType, ApiId } from "../utils/BrowserConstants.js";
import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js";
import { HybridSpaAuthorizationCodeClient } from "./HybridSpaAuthorizationCodeClient.js";
import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { InteractionHandler } from "../interaction_handler/InteractionHandler.js";
export class SilentAuthCodeClient extends StandardInteractionClient {
private apiId: ApiId;
constructor(
config: BrowserConfiguration,
storageImpl: BrowserCacheManager,
browserCrypto: ICrypto,
logger: Logger,
eventHandler: EventHandler,
navigationClient: INavigationClient,
apiId: ApiId,
performanceClient: IPerformanceClient,
nativeMessageHandler?: NativeMessageHandler,
correlationId?: string
) {
super(
config,
storageImpl,
browserCrypto,
logger,
eventHandler,
navigationClient,
performanceClient,
nativeMessageHandler,
correlationId
);
this.apiId = apiId;
}
/**
* Acquires a token silently by redeeming an authorization code against the /token endpoint
* @param request
*/
async acquireToken(
request: AuthorizationCodeRequest
): Promise<AuthenticationResult> {
// Auth code payload is required
if (!request.code) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.authCodeRequired
);
}
// Create silent request
const silentRequest: AuthorizationUrlRequest = await invokeAsync(
this.initializeAuthorizationRequest.bind(this),
PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest,
this.logger,
this.performanceClient,
request.correlationId
)(request, InteractionType.Silent);
const serverTelemetryManager = this.initializeServerTelemetryManager(
this.apiId
);
try {
// Create auth code request (PKCE not needed)
const authCodeRequest: CommonAuthorizationCodeRequest = {
...silentRequest,
code: request.code,
};
// Initialize the client
const clientConfig = await invokeAsync(
this.getClientConfiguration.bind(this),
PerformanceEvents.StandardInteractionClientGetClientConfiguration,
this.logger,
this.performanceClient,
request.correlationId
)({
serverTelemetryManager,
requestAuthority: silentRequest.authority,
requestAzureCloudOptions: silentRequest.azureCloudOptions,
requestExtraQueryParameters: silentRequest.extraQueryParameters,
account: silentRequest.account,
});
const authClient: HybridSpaAuthorizationCodeClient =
new HybridSpaAuthorizationCodeClient(clientConfig);
this.logger.verbose("Auth code client created");
// Create silent handler
const interactionHandler = new InteractionHandler(
authClient,
this.browserStorage,
authCodeRequest,
this.logger,
this.performanceClient
);
// Handle auth code parameters from request
return await invokeAsync(
interactionHandler.handleCodeResponseFromServer.bind(
interactionHandler
),
PerformanceEvents.HandleCodeResponseFromServer,
this.logger,
this.performanceClient,
request.correlationId
)(
{
code: request.code,
msgraph_host: request.msGraphHost,
cloud_graph_host_name: request.cloudGraphHostName,
cloud_instance_host_name: request.cloudInstanceHostName,
},
silentRequest,
false
);
} catch (e) {
if (e instanceof AuthError) {
(e as AuthError).setCorrelationId(this.correlationId);
serverTelemetryManager.cacheFailedRequest(e);
}
throw e;
}
}
/**
* Currently Unsupported
*/
logout(): Promise<void> {
// Synchronous so we must reject
return Promise.reject(
createBrowserAuthError(
BrowserAuthErrorCodes.silentLogoutUnsupported
)
);
}
}
@@ -0,0 +1,95 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { StandardInteractionClient } from "./StandardInteractionClient.js";
import {
CommonSilentFlowRequest,
SilentFlowClient,
PerformanceEvents,
invokeAsync,
} from "@azure/msal-common/browser";
import { ApiId } from "../utils/BrowserConstants.js";
import {
BrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { ClearCacheRequest } from "../request/ClearCacheRequest.js";
export class SilentCacheClient extends StandardInteractionClient {
/**
* Returns unexpired tokens from the cache, if available
* @param silentRequest
*/
async acquireToken(
silentRequest: CommonSilentFlowRequest
): Promise<AuthenticationResult> {
this.performanceClient.addQueueMeasurement(
PerformanceEvents.SilentCacheClientAcquireToken,
silentRequest.correlationId
);
// Telemetry manager only used to increment cacheHits here
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.acquireTokenSilent_silentFlow
);
const clientConfig = await invokeAsync(
this.getClientConfiguration.bind(this),
PerformanceEvents.StandardInteractionClientGetClientConfiguration,
this.logger,
this.performanceClient,
this.correlationId
)({
serverTelemetryManager,
requestAuthority: silentRequest.authority,
requestAzureCloudOptions: silentRequest.azureCloudOptions,
account: silentRequest.account,
});
const silentAuthClient = new SilentFlowClient(
clientConfig,
this.performanceClient
);
this.logger.verbose("Silent auth client created");
try {
const response = await invokeAsync(
silentAuthClient.acquireCachedToken.bind(silentAuthClient),
PerformanceEvents.SilentFlowClientAcquireCachedToken,
this.logger,
this.performanceClient,
silentRequest.correlationId
)(silentRequest);
const authResponse = response[0] as AuthenticationResult;
this.performanceClient.addFields(
{
fromCache: true,
},
silentRequest.correlationId
);
return authResponse;
} catch (error) {
if (
error instanceof BrowserAuthError &&
error.errorCode === BrowserAuthErrorCodes.cryptoKeyNotFound
) {
this.logger.verbose(
"Signing keypair for bound access token not found. Refreshing bound access token and generating a new crypto keypair."
);
}
throw error;
}
}
/**
* API to silenty clear the browser cache.
* @param logoutRequest
*/
logout(logoutRequest?: ClearCacheRequest): Promise<void> {
this.logger.verbose("logoutRedirect called");
const validLogoutRequest = this.initializeLogoutRequest(logoutRequest);
return this.clearCacheOnLogout(validLogoutRequest?.account);
}
}
@@ -0,0 +1,348 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ICrypto,
Logger,
PromptValue,
CommonAuthorizationCodeRequest,
AuthorizationCodeClient,
AuthError,
ProtocolUtils,
IPerformanceClient,
PerformanceEvents,
invokeAsync,
invoke,
} from "@azure/msal-common/browser";
import { StandardInteractionClient } from "./StandardInteractionClient.js";
import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js";
import { BrowserConfiguration } from "../config/Configuration.js";
import { BrowserCacheManager } from "../cache/BrowserCacheManager.js";
import { EventHandler } from "../event/EventHandler.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import {
InteractionType,
ApiId,
BrowserConstants,
} from "../utils/BrowserConstants.js";
import {
initiateAuthRequest,
monitorIframeForHash,
} from "../interaction_handler/SilentHandler.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js";
import { NativeInteractionClient } from "./NativeInteractionClient.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { InteractionHandler } from "../interaction_handler/InteractionHandler.js";
import * as BrowserUtils from "../utils/BrowserUtils.js";
import * as ResponseHandler from "../response/ResponseHandler.js";
export class SilentIframeClient extends StandardInteractionClient {
protected apiId: ApiId;
protected nativeStorage: BrowserCacheManager;
constructor(
config: BrowserConfiguration,
storageImpl: BrowserCacheManager,
browserCrypto: ICrypto,
logger: Logger,
eventHandler: EventHandler,
navigationClient: INavigationClient,
apiId: ApiId,
performanceClient: IPerformanceClient,
nativeStorageImpl: BrowserCacheManager,
nativeMessageHandler?: NativeMessageHandler,
correlationId?: string
) {
super(
config,
storageImpl,
browserCrypto,
logger,
eventHandler,
navigationClient,
performanceClient,
nativeMessageHandler,
correlationId
);
this.apiId = apiId;
this.nativeStorage = nativeStorageImpl;
}
/**
* Acquires a token silently by opening a hidden iframe to the /authorize endpoint with prompt=none or prompt=no_session
* @param request
*/
async acquireToken(
request: SsoSilentRequest
): Promise<AuthenticationResult> {
this.performanceClient.addQueueMeasurement(
PerformanceEvents.SilentIframeClientAcquireToken,
request.correlationId
);
// Check that we have some SSO data
if (
!request.loginHint &&
!request.sid &&
(!request.account || !request.account.username)
) {
this.logger.warning(
"No user hint provided. The authorization server may need more information to complete this request."
);
}
// Check the prompt value
const inputRequest = { ...request };
if (inputRequest.prompt) {
if (
inputRequest.prompt !== PromptValue.NONE &&
inputRequest.prompt !== PromptValue.NO_SESSION
) {
this.logger.warning(
`SilentIframeClient. Replacing invalid prompt ${inputRequest.prompt} with ${PromptValue.NONE}`
);
inputRequest.prompt = PromptValue.NONE;
}
} else {
inputRequest.prompt = PromptValue.NONE;
}
// Create silent request
const silentRequest: AuthorizationUrlRequest = await invokeAsync(
this.initializeAuthorizationRequest.bind(this),
PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest,
this.logger,
this.performanceClient,
request.correlationId
)(inputRequest, InteractionType.Silent);
BrowserUtils.preconnect(silentRequest.authority);
const serverTelemetryManager = this.initializeServerTelemetryManager(
this.apiId
);
let authClient: AuthorizationCodeClient | undefined;
try {
// Initialize the client
authClient = await invokeAsync(
this.createAuthCodeClient.bind(this),
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
this.logger,
this.performanceClient,
request.correlationId
)({
serverTelemetryManager,
requestAuthority: silentRequest.authority,
requestAzureCloudOptions: silentRequest.azureCloudOptions,
requestExtraQueryParameters: silentRequest.extraQueryParameters,
account: silentRequest.account,
});
return await invokeAsync(
this.silentTokenHelper.bind(this),
PerformanceEvents.SilentIframeClientTokenHelper,
this.logger,
this.performanceClient,
request.correlationId
)(authClient, silentRequest);
} catch (e) {
if (e instanceof AuthError) {
(e as AuthError).setCorrelationId(this.correlationId);
serverTelemetryManager.cacheFailedRequest(e);
}
if (
!authClient ||
!(e instanceof AuthError) ||
e.errorCode !== BrowserConstants.INVALID_GRANT_ERROR
) {
throw e;
}
this.performanceClient.addFields(
{
retryError: e.errorCode,
},
this.correlationId
);
const retrySilentRequest: AuthorizationUrlRequest =
await invokeAsync(
this.initializeAuthorizationRequest.bind(this),
PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest,
this.logger,
this.performanceClient,
request.correlationId
)(inputRequest, InteractionType.Silent);
return await invokeAsync(
this.silentTokenHelper.bind(this),
PerformanceEvents.SilentIframeClientTokenHelper,
this.logger,
this.performanceClient,
this.correlationId
)(authClient, retrySilentRequest);
}
}
/**
* Currently Unsupported
*/
logout(): Promise<void> {
// Synchronous so we must reject
return Promise.reject(
createBrowserAuthError(
BrowserAuthErrorCodes.silentLogoutUnsupported
)
);
}
/**
* Helper which acquires an authorization code silently using a hidden iframe from given url
* using the scopes requested as part of the id, and exchanges the code for a set of OAuth tokens.
* @param navigateUrl
* @param userRequestScopes
*/
protected async silentTokenHelper(
authClient: AuthorizationCodeClient,
silentRequest: AuthorizationUrlRequest
): Promise<AuthenticationResult> {
const correlationId = silentRequest.correlationId;
this.performanceClient.addQueueMeasurement(
PerformanceEvents.SilentIframeClientTokenHelper,
correlationId
);
// Create auth code request and generate PKCE params
const authCodeRequest: CommonAuthorizationCodeRequest =
await invokeAsync(
this.initializeAuthorizationCodeRequest.bind(this),
PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest,
this.logger,
this.performanceClient,
correlationId
)(silentRequest);
// Create authorize request url
const navigateUrl = await invokeAsync(
authClient.getAuthCodeUrl.bind(authClient),
PerformanceEvents.GetAuthCodeUrl,
this.logger,
this.performanceClient,
correlationId
)({
...silentRequest,
platformBroker: NativeMessageHandler.isPlatformBrokerAvailable(
this.config,
this.logger,
this.nativeMessageHandler,
silentRequest.authenticationScheme
),
});
// Create silent handler
const interactionHandler = new InteractionHandler(
authClient,
this.browserStorage,
authCodeRequest,
this.logger,
this.performanceClient
);
// Get the frame handle for the silent request
const msalFrame = await invokeAsync(
initiateAuthRequest,
PerformanceEvents.SilentHandlerInitiateAuthRequest,
this.logger,
this.performanceClient,
correlationId
)(
navigateUrl,
this.performanceClient,
this.logger,
correlationId,
this.config.system.navigateFrameWait
);
const responseType = this.config.auth.OIDCOptions.serverResponseType;
// 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 responseString = await invokeAsync(
monitorIframeForHash,
PerformanceEvents.SilentHandlerMonitorIframeForHash,
this.logger,
this.performanceClient,
correlationId
)(
msalFrame,
this.config.system.iframeHashTimeout,
this.config.system.pollIntervalMilliseconds,
this.performanceClient,
this.logger,
correlationId,
responseType
);
const serverParams = invoke(
ResponseHandler.deserializeResponse,
PerformanceEvents.DeserializeResponse,
this.logger,
this.performanceClient,
this.correlationId
)(responseString, responseType, this.logger);
if (serverParams.accountId) {
this.logger.verbose(
"Account id found in hash, calling WAM for token"
);
if (!this.nativeMessageHandler) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.nativeConnectionNotEstablished
);
}
const nativeInteractionClient = new NativeInteractionClient(
this.config,
this.browserStorage,
this.browserCrypto,
this.logger,
this.eventHandler,
this.navigationClient,
this.apiId,
this.performanceClient,
this.nativeMessageHandler,
serverParams.accountId,
this.browserStorage,
correlationId
);
const { userRequestState } = ProtocolUtils.parseRequestState(
this.browserCrypto,
silentRequest.state
);
return invokeAsync(
nativeInteractionClient.acquireToken.bind(
nativeInteractionClient
),
PerformanceEvents.NativeInteractionClientAcquireToken,
this.logger,
this.performanceClient,
correlationId
)({
...silentRequest,
state: userRequestState,
prompt: silentRequest.prompt || PromptValue.NONE,
});
}
// Handle response from hash string
return invokeAsync(
interactionHandler.handleCodeResponse.bind(interactionHandler),
PerformanceEvents.HandleCodeResponse,
this.logger,
this.performanceClient,
correlationId
)(serverParams, silentRequest);
}
}
@@ -0,0 +1,129 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { StandardInteractionClient } from "./StandardInteractionClient.js";
import {
CommonSilentFlowRequest,
ServerTelemetryManager,
RefreshTokenClient,
AuthError,
AzureCloudOptions,
PerformanceEvents,
invokeAsync,
AccountInfo,
StringDict,
} from "@azure/msal-common/browser";
import { ApiId } from "../utils/BrowserConstants.js";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { initializeBaseRequest } from "../request/RequestHelpers.js";
export class SilentRefreshClient extends StandardInteractionClient {
/**
* Exchanges the refresh token for new tokens
* @param request
*/
async acquireToken(
request: CommonSilentFlowRequest
): Promise<AuthenticationResult> {
this.performanceClient.addQueueMeasurement(
PerformanceEvents.SilentRefreshClientAcquireToken,
request.correlationId
);
const baseRequest = await invokeAsync(
initializeBaseRequest,
PerformanceEvents.InitializeBaseRequest,
this.logger,
this.performanceClient,
request.correlationId
)(request, this.config, this.performanceClient, this.logger);
const silentRequest: CommonSilentFlowRequest = {
...request,
...baseRequest,
};
if (request.redirectUri) {
// Make sure any passed redirectUri is converted to an absolute URL - redirectUri is not a required parameter for refresh token redemption so only include if explicitly provided
silentRequest.redirectUri = this.getRedirectUri(
request.redirectUri
);
}
const serverTelemetryManager = this.initializeServerTelemetryManager(
ApiId.acquireTokenSilent_silentFlow
);
const refreshTokenClient = await this.createRefreshTokenClient({
serverTelemetryManager,
authorityUrl: silentRequest.authority,
azureCloudOptions: silentRequest.azureCloudOptions,
account: silentRequest.account,
});
// Send request to renew token. Auth module will throw errors if token cannot be renewed.
return invokeAsync(
refreshTokenClient.acquireTokenByRefreshToken.bind(
refreshTokenClient
),
PerformanceEvents.RefreshTokenClientAcquireTokenByRefreshToken,
this.logger,
this.performanceClient,
request.correlationId
)(silentRequest).catch((e: AuthError) => {
(e as AuthError).setCorrelationId(this.correlationId);
serverTelemetryManager.cacheFailedRequest(e);
throw e;
}) as Promise<AuthenticationResult>;
}
/**
* Currently Unsupported
*/
logout(): Promise<void> {
// Synchronous so we must reject
return Promise.reject(
createBrowserAuthError(
BrowserAuthErrorCodes.silentLogoutUnsupported
)
);
}
/**
* Creates a Refresh Client with the given authority, or the default authority.
* @param params {
* serverTelemetryManager: ServerTelemetryManager;
* authorityUrl?: string;
* azureCloudOptions?: AzureCloudOptions;
* extraQueryParams?: StringDict;
* account?: AccountInfo;
* }
*/
protected async createRefreshTokenClient(params: {
serverTelemetryManager: ServerTelemetryManager;
authorityUrl?: string;
azureCloudOptions?: AzureCloudOptions;
extraQueryParameters?: StringDict;
account?: AccountInfo;
}): Promise<RefreshTokenClient> {
// Create auth module.
const clientConfig = await invokeAsync(
this.getClientConfiguration.bind(this),
PerformanceEvents.StandardInteractionClientGetClientConfiguration,
this.logger,
this.performanceClient,
this.correlationId
)({
serverTelemetryManager: params.serverTelemetryManager,
requestAuthority: params.authorityUrl,
requestAzureCloudOptions: params.azureCloudOptions,
requestExtraQueryParameters: params.extraQueryParameters,
account: params.account,
});
return new RefreshTokenClient(clientConfig, this.performanceClient);
}
}
@@ -0,0 +1,392 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ServerTelemetryManager,
CommonAuthorizationCodeRequest,
Constants,
AuthorizationCodeClient,
ClientConfiguration,
UrlString,
CommonEndSessionRequest,
ProtocolUtils,
ResponseMode,
IdTokenClaims,
AccountInfo,
AzureCloudOptions,
PerformanceEvents,
invokeAsync,
BaseAuthRequest,
StringDict,
PkceCodes,
} from "@azure/msal-common/browser";
import { BaseInteractionClient } from "./BaseInteractionClient.js";
import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js";
import {
BrowserConstants,
InteractionType,
} from "../utils/BrowserConstants.js";
import { version } from "../packageMetadata.js";
import { BrowserStateObject } from "../utils/BrowserProtocolUtils.js";
import { EndSessionRequest } from "../request/EndSessionRequest.js";
import * as BrowserUtils from "../utils/BrowserUtils.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
import { PopupRequest } from "../request/PopupRequest.js";
import { SsoSilentRequest } from "../request/SsoSilentRequest.js";
import { generatePkceCodes } from "../crypto/PkceGenerator.js";
import { createNewGuid } from "../crypto/BrowserCrypto.js";
import { initializeBaseRequest } from "../request/RequestHelpers.js";
/**
* Defines the class structure and helper functions used by the "standard", non-brokered auth flows (popup, redirect, silent (RT), silent (iframe))
*/
export abstract class StandardInteractionClient extends BaseInteractionClient {
/**
* Generates an auth code request tied to the url request.
* @param request
* @param pkceCodes
*/
protected async initializeAuthorizationCodeRequest(
request: AuthorizationUrlRequest,
pkceCodes?: PkceCodes
): Promise<CommonAuthorizationCodeRequest> {
this.performanceClient.addQueueMeasurement(
PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest,
this.correlationId
);
const generatedPkceParams: PkceCodes =
pkceCodes ||
(await invokeAsync(
generatePkceCodes,
PerformanceEvents.GeneratePkceCodes,
this.logger,
this.performanceClient,
this.correlationId
)(this.performanceClient, this.logger, this.correlationId));
const authCodeRequest: CommonAuthorizationCodeRequest = {
...request,
redirectUri: request.redirectUri,
code: Constants.EMPTY_STRING,
codeVerifier: generatedPkceParams.verifier,
};
request.codeChallenge = generatedPkceParams.challenge;
request.codeChallengeMethod = Constants.S256_CODE_CHALLENGE_METHOD;
return authCodeRequest;
}
/**
* Initializer for the logout request.
* @param logoutRequest
*/
protected initializeLogoutRequest(
logoutRequest?: EndSessionRequest
): CommonEndSessionRequest {
this.logger.verbose(
"initializeLogoutRequest called",
logoutRequest?.correlationId
);
const validLogoutRequest: CommonEndSessionRequest = {
correlationId: this.correlationId || createNewGuid(),
...logoutRequest,
};
/**
* Set logout_hint to be login_hint from ID Token Claims if present
* and logoutHint attribute wasn't manually set in logout request
*/
if (logoutRequest) {
// If logoutHint isn't set and an account was passed in, try to extract logoutHint from ID Token Claims
if (!logoutRequest.logoutHint) {
if (logoutRequest.account) {
const logoutHint = this.getLogoutHintFromIdTokenClaims(
logoutRequest.account
);
if (logoutHint) {
this.logger.verbose(
"Setting logoutHint to login_hint ID Token Claim value for the account provided"
);
validLogoutRequest.logoutHint = logoutHint;
}
} else {
this.logger.verbose(
"logoutHint was not set and account was not passed into logout request, logoutHint will not be set"
);
}
} else {
this.logger.verbose(
"logoutHint has already been set in logoutRequest"
);
}
} else {
this.logger.verbose(
"logoutHint will not be set since no logout request was configured"
);
}
/*
* Only set redirect uri if logout request isn't provided or the set uri isn't null.
* Otherwise, use passed uri, config, or current page.
*/
if (!logoutRequest || logoutRequest.postLogoutRedirectUri !== null) {
if (logoutRequest && logoutRequest.postLogoutRedirectUri) {
this.logger.verbose(
"Setting postLogoutRedirectUri to uri set on logout request",
validLogoutRequest.correlationId
);
validLogoutRequest.postLogoutRedirectUri =
UrlString.getAbsoluteUrl(
logoutRequest.postLogoutRedirectUri,
BrowserUtils.getCurrentUri()
);
} else if (this.config.auth.postLogoutRedirectUri === null) {
this.logger.verbose(
"postLogoutRedirectUri configured as null and no uri set on request, not passing post logout redirect",
validLogoutRequest.correlationId
);
} else if (this.config.auth.postLogoutRedirectUri) {
this.logger.verbose(
"Setting postLogoutRedirectUri to configured uri",
validLogoutRequest.correlationId
);
validLogoutRequest.postLogoutRedirectUri =
UrlString.getAbsoluteUrl(
this.config.auth.postLogoutRedirectUri,
BrowserUtils.getCurrentUri()
);
} else {
this.logger.verbose(
"Setting postLogoutRedirectUri to current page",
validLogoutRequest.correlationId
);
validLogoutRequest.postLogoutRedirectUri =
UrlString.getAbsoluteUrl(
BrowserUtils.getCurrentUri(),
BrowserUtils.getCurrentUri()
);
}
} else {
this.logger.verbose(
"postLogoutRedirectUri passed as null, not setting post logout redirect uri",
validLogoutRequest.correlationId
);
}
return validLogoutRequest;
}
/**
* Parses login_hint ID Token Claim out of AccountInfo object to be used as
* logout_hint in end session request.
* @param account
*/
protected getLogoutHintFromIdTokenClaims(
account: AccountInfo
): string | null {
const idTokenClaims: IdTokenClaims | undefined = account.idTokenClaims;
if (idTokenClaims) {
if (idTokenClaims.login_hint) {
return idTokenClaims.login_hint;
} else {
this.logger.verbose(
"The ID Token Claims tied to the provided account do not contain a login_hint claim, logoutHint will not be added to logout request"
);
}
} else {
this.logger.verbose(
"The provided account does not contain ID Token Claims, logoutHint will not be added to logout request"
);
}
return null;
}
/**
* Creates an Authorization Code Client with the given authority, or the default authority.
* @param params {
* serverTelemetryManager: ServerTelemetryManager;
* authorityUrl?: string;
* requestAzureCloudOptions?: AzureCloudOptions;
* requestExtraQueryParameters?: StringDict;
* account?: AccountInfo;
* }
*/
protected async createAuthCodeClient(params: {
serverTelemetryManager: ServerTelemetryManager;
requestAuthority?: string;
requestAzureCloudOptions?: AzureCloudOptions;
requestExtraQueryParameters?: StringDict;
account?: AccountInfo;
}): Promise<AuthorizationCodeClient> {
this.performanceClient.addQueueMeasurement(
PerformanceEvents.StandardInteractionClientCreateAuthCodeClient,
this.correlationId
);
// Create auth module.
const clientConfig = await invokeAsync(
this.getClientConfiguration.bind(this),
PerformanceEvents.StandardInteractionClientGetClientConfiguration,
this.logger,
this.performanceClient,
this.correlationId
)(params);
return new AuthorizationCodeClient(
clientConfig,
this.performanceClient
);
}
/**
* Creates a Client Configuration object with the given request authority, or the default authority.
* @param params {
* serverTelemetryManager: ServerTelemetryManager;
* requestAuthority?: string;
* requestAzureCloudOptions?: AzureCloudOptions;
* requestExtraQueryParameters?: boolean;
* account?: AccountInfo;
* }
*/
protected async getClientConfiguration(params: {
serverTelemetryManager: ServerTelemetryManager;
requestAuthority?: string;
requestAzureCloudOptions?: AzureCloudOptions;
requestExtraQueryParameters?: StringDict;
account?: AccountInfo;
}): Promise<ClientConfiguration> {
const {
serverTelemetryManager,
requestAuthority,
requestAzureCloudOptions,
requestExtraQueryParameters,
account,
} = params;
this.performanceClient.addQueueMeasurement(
PerformanceEvents.StandardInteractionClientGetClientConfiguration,
this.correlationId
);
const discoveredAuthority = await invokeAsync(
this.getDiscoveredAuthority.bind(this),
PerformanceEvents.StandardInteractionClientGetDiscoveredAuthority,
this.logger,
this.performanceClient,
this.correlationId
)({
requestAuthority,
requestAzureCloudOptions,
requestExtraQueryParameters,
account,
});
const logger = this.config.system.loggerOptions;
return {
authOptions: {
clientId: this.config.auth.clientId,
authority: discoveredAuthority,
clientCapabilities: this.config.auth.clientCapabilities,
redirectUri: this.config.auth.redirectUri,
},
systemOptions: {
tokenRenewalOffsetSeconds:
this.config.system.tokenRenewalOffsetSeconds,
preventCorsPreflight: true,
},
loggerOptions: {
loggerCallback: logger.loggerCallback,
piiLoggingEnabled: logger.piiLoggingEnabled,
logLevel: logger.logLevel,
correlationId: this.correlationId,
},
cacheOptions: {
claimsBasedCachingEnabled:
this.config.cache.claimsBasedCachingEnabled,
},
cryptoInterface: this.browserCrypto,
networkInterface: this.networkClient,
storageInterface: this.browserStorage,
serverTelemetryManager: serverTelemetryManager,
libraryInfo: {
sku: BrowserConstants.MSAL_SKU,
version: version,
cpu: Constants.EMPTY_STRING,
os: Constants.EMPTY_STRING,
},
telemetry: this.config.telemetry,
};
}
/**
* Helper to initialize required request parameters for interactive APIs and ssoSilent()
* @param request
* @param interactionType
*/
protected async initializeAuthorizationRequest(
request: RedirectRequest | PopupRequest | SsoSilentRequest,
interactionType: InteractionType
): Promise<AuthorizationUrlRequest> {
this.performanceClient.addQueueMeasurement(
PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest,
this.correlationId
);
const redirectUri = this.getRedirectUri(request.redirectUri);
const browserState: BrowserStateObject = {
interactionType: interactionType,
};
const state = ProtocolUtils.setRequestState(
this.browserCrypto,
(request && request.state) || Constants.EMPTY_STRING,
browserState
);
const baseRequest: BaseAuthRequest = await invokeAsync(
initializeBaseRequest,
PerformanceEvents.InitializeBaseRequest,
this.logger,
this.performanceClient,
this.correlationId
)(
{ ...request, correlationId: this.correlationId },
this.config,
this.performanceClient,
this.logger
);
const validatedRequest: AuthorizationUrlRequest = {
...baseRequest,
redirectUri: redirectUri,
state: state,
nonce: request.nonce || createNewGuid(),
responseMode: this.config.auth.OIDCOptions
.serverResponseType as ResponseMode,
};
// Skip active account lookup if either login hint or session id is set
if (request.loginHint || request.sid) {
return validatedRequest;
}
const account =
request.account || this.browserStorage.getActiveAccount();
if (account) {
this.logger.verbose(
"Setting validated request account",
this.correlationId
);
this.logger.verbosePii(
`Setting validated request account: ${account.homeAccountId}`,
this.correlationId
);
validatedRequest.account = account;
}
return validatedRequest;
}
}
@@ -0,0 +1,178 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthorizationCodePayload,
CommonAuthorizationCodeRequest,
AuthorizationCodeClient,
CcsCredential,
Logger,
ServerError,
IPerformanceClient,
PerformanceEvents,
invokeAsync,
CcsCredentialType,
ServerAuthorizationCodeResponse,
} from "@azure/msal-common/browser";
import { BrowserCacheManager } from "../cache/BrowserCacheManager.js";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js";
/**
* Abstract class which defines operations for a browser interaction handling class.
*/
export class InteractionHandler {
protected authModule: AuthorizationCodeClient;
protected browserStorage: BrowserCacheManager;
protected authCodeRequest: CommonAuthorizationCodeRequest;
protected logger: Logger;
protected performanceClient: IPerformanceClient;
constructor(
authCodeModule: AuthorizationCodeClient,
storageImpl: BrowserCacheManager,
authCodeRequest: CommonAuthorizationCodeRequest,
logger: Logger,
performanceClient: IPerformanceClient
) {
this.authModule = authCodeModule;
this.browserStorage = storageImpl;
this.authCodeRequest = authCodeRequest;
this.logger = logger;
this.performanceClient = performanceClient;
}
/**
* Function to handle response parameters from hash.
* @param locationHash
*/
async handleCodeResponse(
response: ServerAuthorizationCodeResponse,
request: AuthorizationUrlRequest
): Promise<AuthenticationResult> {
this.performanceClient.addQueueMeasurement(
PerformanceEvents.HandleCodeResponse,
request.correlationId
);
let authCodeResponse;
try {
authCodeResponse = this.authModule.handleFragmentResponse(
response,
request.state
);
} catch (e) {
if (
e instanceof ServerError &&
e.subError === BrowserAuthErrorCodes.userCancelled
) {
// Translate server error caused by user closing native prompt to corresponding first class MSAL error
throw createBrowserAuthError(
BrowserAuthErrorCodes.userCancelled
);
} else {
throw e;
}
}
return invokeAsync(
this.handleCodeResponseFromServer.bind(this),
PerformanceEvents.HandleCodeResponseFromServer,
this.logger,
this.performanceClient,
request.correlationId
)(authCodeResponse, request);
}
/**
* Process auth code response from AAD
* @param authCodeResponse
* @param state
* @param authority
* @param networkModule
* @returns
*/
async handleCodeResponseFromServer(
authCodeResponse: AuthorizationCodePayload,
request: AuthorizationUrlRequest,
validateNonce: boolean = true
): Promise<AuthenticationResult> {
this.performanceClient.addQueueMeasurement(
PerformanceEvents.HandleCodeResponseFromServer,
request.correlationId
);
this.logger.trace(
"InteractionHandler.handleCodeResponseFromServer called"
);
// Assign code to request
this.authCodeRequest.code = authCodeResponse.code;
// Check for new cloud instance
if (authCodeResponse.cloud_instance_host_name) {
await invokeAsync(
this.authModule.updateAuthority.bind(this.authModule),
PerformanceEvents.UpdateTokenEndpointAuthority,
this.logger,
this.performanceClient,
request.correlationId
)(authCodeResponse.cloud_instance_host_name, request.correlationId);
}
// Nonce validation not needed when redirect not involved (e.g. hybrid spa, renewing token via rt)
if (validateNonce) {
// TODO: Assigning "response nonce" to "request nonce" is confusing. Refactor the function doing validation to accept request nonce directly
authCodeResponse.nonce = request.nonce || undefined;
}
authCodeResponse.state = request.state;
// Add CCS parameters if available
if (authCodeResponse.client_info) {
this.authCodeRequest.clientInfo = authCodeResponse.client_info;
} else {
const ccsCred = this.createCcsCredentials(request);
if (ccsCred) {
this.authCodeRequest.ccsCredential = ccsCred;
}
}
// Acquire token with retrieved code.
const tokenResponse = (await invokeAsync(
this.authModule.acquireToken.bind(this.authModule),
PerformanceEvents.AuthClientAcquireToken,
this.logger,
this.performanceClient,
request.correlationId
)(this.authCodeRequest, authCodeResponse)) as AuthenticationResult;
return tokenResponse;
}
/**
* Build ccs creds if available
*/
protected createCcsCredentials(
request: AuthorizationUrlRequest
): CcsCredential | null {
if (request.account) {
return {
credential: request.account.homeAccountId,
type: CcsCredentialType.HOME_ACCOUNT_ID,
};
} else if (request.loginHint) {
return {
credential: request.loginHint,
type: CcsCredentialType.UPN,
};
}
return null;
}
}
@@ -0,0 +1,251 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AuthorizationCodeClient,
CommonAuthorizationCodeRequest,
Logger,
ServerError,
IPerformanceClient,
createClientAuthError,
ClientAuthErrorCodes,
CcsCredential,
invokeAsync,
PerformanceEvents,
ServerAuthorizationCodeResponse,
} from "@azure/msal-common/browser";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { ApiId, TemporaryCacheKeys } from "../utils/BrowserConstants.js";
import { BrowserCacheManager } from "../cache/BrowserCacheManager.js";
import { INavigationClient } from "../navigation/INavigationClient.js";
import { NavigationOptions } from "../navigation/NavigationOptions.js";
import { AuthenticationResult } from "../response/AuthenticationResult.js";
export type RedirectParams = {
navigationClient: INavigationClient;
redirectTimeout: number;
redirectStartPage: string;
onRedirectNavigate?: (url: string) => void | boolean;
};
export class RedirectHandler {
authModule: AuthorizationCodeClient;
browserStorage: BrowserCacheManager;
authCodeRequest: CommonAuthorizationCodeRequest;
logger: Logger;
performanceClient: IPerformanceClient;
constructor(
authCodeModule: AuthorizationCodeClient,
storageImpl: BrowserCacheManager,
authCodeRequest: CommonAuthorizationCodeRequest,
logger: Logger,
performanceClient: IPerformanceClient
) {
this.authModule = authCodeModule;
this.browserStorage = storageImpl;
this.authCodeRequest = authCodeRequest;
this.logger = logger;
this.performanceClient = performanceClient;
}
/**
* Redirects window to given URL.
* @param urlNavigate
*/
async initiateAuthRequest(
requestUrl: string,
params: RedirectParams
): Promise<void> {
this.logger.verbose("RedirectHandler.initiateAuthRequest called");
// Navigate if valid URL
if (requestUrl) {
// Cache start page, returns to this page after redirectUri if navigateToLoginRequestUrl is true
if (params.redirectStartPage) {
this.logger.verbose(
"RedirectHandler.initiateAuthRequest: redirectStartPage set, caching start page"
);
this.browserStorage.setTemporaryCache(
TemporaryCacheKeys.ORIGIN_URI,
params.redirectStartPage,
true
);
}
// Set interaction status in the library.
this.browserStorage.setTemporaryCache(
TemporaryCacheKeys.CORRELATION_ID,
this.authCodeRequest.correlationId,
true
);
this.browserStorage.cacheCodeRequest(this.authCodeRequest);
this.logger.infoPii(
`RedirectHandler.initiateAuthRequest: Navigate to: ${requestUrl}`
);
const navigationOptions: NavigationOptions = {
apiId: ApiId.acquireTokenRedirect,
timeout: params.redirectTimeout,
noHistory: false,
};
// If onRedirectNavigate is implemented, invoke it and provide requestUrl
if (typeof params.onRedirectNavigate === "function") {
this.logger.verbose(
"RedirectHandler.initiateAuthRequest: Invoking onRedirectNavigate callback"
);
const navigate = params.onRedirectNavigate(requestUrl);
// Returning false from onRedirectNavigate will stop navigation
if (navigate !== false) {
this.logger.verbose(
"RedirectHandler.initiateAuthRequest: onRedirectNavigate did not return false, navigating"
);
await params.navigationClient.navigateExternal(
requestUrl,
navigationOptions
);
return;
} else {
this.logger.verbose(
"RedirectHandler.initiateAuthRequest: onRedirectNavigate returned false, stopping navigation"
);
return;
}
} else {
// Navigate window to request URL
this.logger.verbose(
"RedirectHandler.initiateAuthRequest: Navigating window to navigate url"
);
await params.navigationClient.navigateExternal(
requestUrl,
navigationOptions
);
return;
}
} else {
// Throw error if request URL is empty.
this.logger.info(
"RedirectHandler.initiateAuthRequest: Navigate url is empty"
);
throw createBrowserAuthError(
BrowserAuthErrorCodes.emptyNavigateUri
);
}
}
/**
* Handle authorization code response in the window.
* @param hash
*/
async handleCodeResponse(
response: ServerAuthorizationCodeResponse,
state: string
): Promise<AuthenticationResult> {
this.logger.verbose("RedirectHandler.handleCodeResponse called");
// Interaction is completed - remove interaction status.
this.browserStorage.setInteractionInProgress(false);
// Handle code response.
const stateKey = this.browserStorage.generateStateKey(state);
const requestState = this.browserStorage.getTemporaryCache(stateKey);
if (!requestState) {
throw createClientAuthError(
ClientAuthErrorCodes.stateNotFound,
"Cached State"
);
}
let authCodeResponse;
try {
authCodeResponse = this.authModule.handleFragmentResponse(
response,
requestState
);
} catch (e) {
if (
e instanceof ServerError &&
e.subError === BrowserAuthErrorCodes.userCancelled
) {
// Translate server error caused by user closing native prompt to corresponding first class MSAL error
throw createBrowserAuthError(
BrowserAuthErrorCodes.userCancelled
);
} else {
throw e;
}
}
// Get cached items
const nonceKey = this.browserStorage.generateNonceKey(requestState);
const cachedNonce = this.browserStorage.getTemporaryCache(nonceKey);
// Assign code to request
this.authCodeRequest.code = authCodeResponse.code;
// Check for new cloud instance
if (authCodeResponse.cloud_instance_host_name) {
await invokeAsync(
this.authModule.updateAuthority.bind(this.authModule),
PerformanceEvents.UpdateTokenEndpointAuthority,
this.logger,
this.performanceClient,
this.authCodeRequest.correlationId
)(
authCodeResponse.cloud_instance_host_name,
this.authCodeRequest.correlationId
);
}
authCodeResponse.nonce = cachedNonce || undefined;
authCodeResponse.state = requestState;
// Add CCS parameters if available
if (authCodeResponse.client_info) {
this.authCodeRequest.clientInfo = authCodeResponse.client_info;
} else {
const cachedCcsCred = this.checkCcsCredentials();
if (cachedCcsCred) {
this.authCodeRequest.ccsCredential = cachedCcsCred;
}
}
// Acquire token with retrieved code.
const tokenResponse = (await this.authModule.acquireToken(
this.authCodeRequest,
authCodeResponse
)) as AuthenticationResult;
this.browserStorage.cleanRequestByState(state);
return tokenResponse;
}
/**
* Looks up ccs creds in the cache
*/
protected checkCcsCredentials(): CcsCredential | null {
// Look up ccs credential in temp cache
const cachedCcsCred = this.browserStorage.getTemporaryCache(
TemporaryCacheKeys.CCS_CREDENTIAL,
true
);
if (cachedCcsCred) {
try {
return JSON.parse(cachedCcsCred) as CcsCredential;
} catch (e) {
this.authModule.logger.error(
"Cache credential could not be parsed"
);
this.authModule.logger.errorPii(
`Cache credential could not be parsed: ${cachedCcsCred}`
);
}
}
return null;
}
}
@@ -0,0 +1,221 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
Logger,
IPerformanceClient,
PerformanceEvents,
invokeAsync,
invoke,
ServerResponseType,
} from "@azure/msal-common/browser";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { DEFAULT_IFRAME_TIMEOUT_MS } from "../config/Configuration.js";
/**
* Creates a hidden iframe to given URL using user-requested scopes as an id.
* @param urlNavigate
* @param userRequestScopes
*/
export async function initiateAuthRequest(
requestUrl: string,
performanceClient: IPerformanceClient,
logger: Logger,
correlationId: string,
navigateFrameWait?: number
): Promise<HTMLIFrameElement> {
performanceClient.addQueueMeasurement(
PerformanceEvents.SilentHandlerInitiateAuthRequest,
correlationId
);
if (!requestUrl) {
// Throw error if request URL is empty.
logger.info("Navigate url is empty");
throw createBrowserAuthError(BrowserAuthErrorCodes.emptyNavigateUri);
}
if (navigateFrameWait) {
return invokeAsync(
loadFrame,
PerformanceEvents.SilentHandlerLoadFrame,
logger,
performanceClient,
correlationId
)(requestUrl, navigateFrameWait, performanceClient, correlationId);
}
return invoke(
loadFrameSync,
PerformanceEvents.SilentHandlerLoadFrameSync,
logger,
performanceClient,
correlationId
)(requestUrl);
}
/**
* Monitors an iframe content window until it loads a url with a known hash, or hits a specified timeout.
* @param iframe
* @param timeout
*/
export async function monitorIframeForHash(
iframe: HTMLIFrameElement,
timeout: number,
pollIntervalMilliseconds: number,
performanceClient: IPerformanceClient,
logger: Logger,
correlationId: string,
responseType: ServerResponseType
): Promise<string> {
performanceClient.addQueueMeasurement(
PerformanceEvents.SilentHandlerMonitorIframeForHash,
correlationId
);
return new Promise<string>((resolve, reject) => {
if (timeout < DEFAULT_IFRAME_TIMEOUT_MS) {
logger.warning(
`system.loadFrameTimeout or system.iframeHashTimeout set to lower (${timeout}ms) than the default (${DEFAULT_IFRAME_TIMEOUT_MS}ms). This may result in timeouts.`
);
}
/*
* Polling for iframes can be purely timing based,
* since we don't need to account for interaction.
*/
const timeoutId = window.setTimeout(() => {
window.clearInterval(intervalId);
reject(
createBrowserAuthError(
BrowserAuthErrorCodes.monitorWindowTimeout
)
);
}, timeout);
const intervalId = window.setInterval(() => {
let href: string = "";
const contentWindow = iframe.contentWindow;
try {
/*
* Will throw if cross origin,
* which should be caught and ignored
* since we need the interval to keep running while on STS UI.
*/
href = contentWindow ? contentWindow.location.href : "";
} catch (e) {}
if (!href || href === "about:blank") {
return;
}
let responseString = "";
if (contentWindow) {
if (responseType === ServerResponseType.QUERY) {
responseString = contentWindow.location.search;
} else {
responseString = contentWindow.location.hash;
}
}
window.clearTimeout(timeoutId);
window.clearInterval(intervalId);
resolve(responseString);
}, pollIntervalMilliseconds);
}).finally(() => {
invoke(
removeHiddenIframe,
PerformanceEvents.RemoveHiddenIframe,
logger,
performanceClient,
correlationId
)(iframe);
});
}
/**
* @hidden
* Loads iframe with authorization endpoint URL
* @ignore
* @deprecated
*/
function loadFrame(
urlNavigate: string,
navigateFrameWait: number,
performanceClient: IPerformanceClient,
correlationId: string
): Promise<HTMLIFrameElement> {
performanceClient.addQueueMeasurement(
PerformanceEvents.SilentHandlerLoadFrame,
correlationId
);
/*
* This trick overcomes iframe navigation in IE
* IE does not load the page consistently in iframe
*/
return new Promise((resolve, reject) => {
const frameHandle = createHiddenIframe();
window.setTimeout(() => {
if (!frameHandle) {
reject("Unable to load iframe");
return;
}
frameHandle.src = urlNavigate;
resolve(frameHandle);
}, navigateFrameWait);
});
}
/**
* @hidden
* Loads the iframe synchronously when the navigateTimeFrame is set to `0`
* @param urlNavigate
* @param frameName
* @param logger
*/
function loadFrameSync(urlNavigate: string): HTMLIFrameElement {
const frameHandle = createHiddenIframe();
frameHandle.src = urlNavigate;
return frameHandle;
}
/**
* @hidden
* Creates a new hidden iframe or gets an existing one for silent token renewal.
* @ignore
*/
function createHiddenIframe(): HTMLIFrameElement {
const authFrame = document.createElement("iframe");
authFrame.className = "msalSilentIframe";
authFrame.style.visibility = "hidden";
authFrame.style.position = "absolute";
authFrame.style.width = authFrame.style.height = "0";
authFrame.style.border = "0";
authFrame.setAttribute(
"sandbox",
"allow-scripts allow-same-origin allow-forms"
);
document.body.appendChild(authFrame);
return authFrame;
}
/**
* @hidden
* Removes a hidden iframe from the page.
* @ignore
*/
function removeHiddenIframe(iframe: HTMLIFrameElement): void {
if (document.body === iframe.parentNode) {
document.body.removeChild(iframe);
}
}
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type AccountInfo = {
homeAccountId?: string;
environment: string;
tenantId?: string;
username: string;
localAccountId?: string;
name?: string;
idToken?: string; // idTokenClaims can be parsed from idToken in MSAL.js
platformBrokerId?: string; // Used by WAM previous called nativeAccountId
idTokenClaims?: object;
};
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type AuthBridgeResponse = string | { data: string };
export interface AuthBridge {
addEventListener: (
eventName: string,
callback: (response: AuthBridgeResponse) => void
) => void;
postMessage: (message: string) => void;
removeEventListener: (
eventName: string,
callback: (response: AuthBridgeResponse) => void
) => void;
}
+12
View File
@@ -0,0 +1,12 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountInfo } from "./AccountInfo.js";
import { TokenResponse } from "./TokenResponse.js";
export type AuthResult = {
token: TokenResponse;
account: AccountInfo;
};
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* AccountContext is used to pass account information when the bridge is initialized
*
* NAA (MetaOS) apps are created and destroyed for the same session multiple times.
* `AccountContext` helps in booting up the cached account when the bridge
* is recreated for a new NAA instance in the same auth session.
*/
export interface AccountContext {
homeAccountId: string;
environment: string;
tenantId: string;
}
+9
View File
@@ -0,0 +1,9 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
// Capabilities are intended to future proof the bridge against any feature support
export interface BridgeCapabilities {
queryAccount?: boolean;
}
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BridgeStatusCode } from "./BridgeStatusCode.js";
export type BridgeError = {
status: BridgeStatusCode;
code?: string; // auth_flow_last_error such as invalid_grant
subError?: string; // server_suberror_code such as consent_required
description?: string;
properties?: object; // additional telemetry info
};
export function isBridgeError(error: unknown): error is BridgeError {
return (error as BridgeError).status !== undefined;
}
+233
View File
@@ -0,0 +1,233 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthBridge, AuthBridgeResponse } from "./AuthBridge.js";
import { AuthResult } from "./AuthResult.js";
import { BridgeCapabilities } from "./BridgeCapabilities.js";
import { AccountContext } from "./BridgeAccountContext.js";
import { BridgeError } from "./BridgeError.js";
import { BridgeRequest } from "./BridgeRequest.js";
import {
BridgeRequestEnvelope,
BridgeMethods,
} from "./BridgeRequestEnvelope.js";
import { BridgeResponseEnvelope } from "./BridgeResponseEnvelope.js";
import { BridgeStatusCode } from "./BridgeStatusCode.js";
import { IBridgeProxy } from "./IBridgeProxy.js";
import { InitContext } from "./InitContext.js";
import { TokenRequest } from "./TokenRequest.js";
import * as BrowserCrypto from "../crypto/BrowserCrypto.js";
import { BrowserConstants } from "../utils/BrowserConstants.js";
import { version } from "../packageMetadata.js";
declare global {
interface Window {
nestedAppAuthBridge: AuthBridge;
}
}
/**
* BridgeProxy
* Provides a proxy for accessing a bridge to a host app and/or
* platform broker
*/
export class BridgeProxy implements IBridgeProxy {
static bridgeRequests: BridgeRequest[] = [];
sdkName: string;
sdkVersion: string;
capabilities?: BridgeCapabilities;
accountContext?: AccountContext;
/**
* initializeNestedAppAuthBridge - Initializes the bridge to the host app
* @returns a promise that resolves to an InitializeBridgeResponse or rejects with an Error
* @remarks This method will be called by the create factory method
* @remarks If the bridge is not available, this method will throw an error
*/
protected static async initializeNestedAppAuthBridge(): Promise<InitContext> {
if (window === undefined) {
throw new Error("window is undefined");
}
if (window.nestedAppAuthBridge === undefined) {
throw new Error("window.nestedAppAuthBridge is undefined");
}
try {
window.nestedAppAuthBridge.addEventListener(
"message",
(response: AuthBridgeResponse) => {
const responsePayload =
typeof response === "string" ? response : response.data;
const responseEnvelope: BridgeResponseEnvelope =
JSON.parse(responsePayload);
const request = BridgeProxy.bridgeRequests.find(
(element) =>
element.requestId === responseEnvelope.requestId
);
if (request !== undefined) {
BridgeProxy.bridgeRequests.splice(
BridgeProxy.bridgeRequests.indexOf(request),
1
);
if (responseEnvelope.success) {
request.resolve(responseEnvelope);
} else {
request.reject(responseEnvelope.error);
}
}
}
);
const bridgeResponse = await new Promise<BridgeResponseEnvelope>(
(resolve, reject) => {
const message = BridgeProxy.buildRequest("GetInitContext");
const request: BridgeRequest = {
requestId: message.requestId,
method: message.method,
resolve: resolve,
reject: reject,
};
BridgeProxy.bridgeRequests.push(request);
window.nestedAppAuthBridge.postMessage(
JSON.stringify(message)
);
}
);
return BridgeProxy.validateBridgeResultOrThrow(
bridgeResponse.initContext
);
} catch (error) {
window.console.log(error);
throw error;
}
}
/**
* getTokenInteractive - Attempts to get a token interactively from the bridge
* @param request A token request
* @returns a promise that resolves to an auth result or rejects with a BridgeError
*/
public getTokenInteractive(request: TokenRequest): Promise<AuthResult> {
return this.getToken("GetTokenPopup", request);
}
/**
* getTokenSilent Attempts to get a token silently from the bridge
* @param request A token request
* @returns a promise that resolves to an auth result or rejects with a BridgeError
*/
public getTokenSilent(request: TokenRequest): Promise<AuthResult> {
return this.getToken("GetToken", request);
}
private async getToken(
requestType: BridgeMethods,
request: TokenRequest
): Promise<AuthResult> {
const result = await this.sendRequest(requestType, {
tokenParams: request,
});
return {
token: BridgeProxy.validateBridgeResultOrThrow(result.token),
account: BridgeProxy.validateBridgeResultOrThrow(result.account),
};
}
public getHostCapabilities(): BridgeCapabilities | null {
return this.capabilities ?? null;
}
public getAccountContext(): AccountContext | null {
return this.accountContext ? this.accountContext : null;
}
private static buildRequest(
method: BridgeMethods,
requestParams?: Partial<BridgeRequestEnvelope>
): BridgeRequestEnvelope {
return {
messageType: "NestedAppAuthRequest",
method: method,
requestId: BrowserCrypto.createNewGuid(),
sendTime: Date.now(),
clientLibrary: BrowserConstants.MSAL_SKU,
clientLibraryVersion: version,
...requestParams,
};
}
/**
* A method used to send a request to the bridge
* @param request A token request
* @returns a promise that resolves to a response of provided type or rejects with a BridgeError
*/
private sendRequest(
method: BridgeMethods,
requestParams?: Partial<BridgeRequestEnvelope>
): Promise<BridgeResponseEnvelope> {
const message = BridgeProxy.buildRequest(method, requestParams);
const promise = new Promise<BridgeResponseEnvelope>(
(resolve, reject) => {
const request: BridgeRequest = {
requestId: message.requestId,
method: message.method,
resolve: resolve,
reject: reject,
};
BridgeProxy.bridgeRequests.push(request);
window.nestedAppAuthBridge.postMessage(JSON.stringify(message));
}
);
return promise;
}
private static validateBridgeResultOrThrow<T>(input: T | undefined): T {
if (input === undefined) {
const bridgeError: BridgeError = {
status: BridgeStatusCode.NestedAppAuthUnavailable,
};
throw bridgeError;
}
return input;
}
/**
* Private constructor for BridgeProxy
* @param sdkName The name of the SDK being used to make requests on behalf of the app
* @param sdkVersion The version of the SDK being used to make requests on behalf of the app
* @param capabilities The capabilities of the bridge / SDK / platform broker
*/
private constructor(
sdkName: string,
sdkVersion: string,
accountContext?: AccountContext,
capabilities?: BridgeCapabilities
) {
this.sdkName = sdkName;
this.sdkVersion = sdkVersion;
this.accountContext = accountContext;
this.capabilities = capabilities;
}
/**
* Factory method for creating an implementation of IBridgeProxy
* @returns A promise that resolves to a BridgeProxy implementation
*/
public static async create(): Promise<IBridgeProxy> {
const response = await BridgeProxy.initializeNestedAppAuthBridge();
return new BridgeProxy(
response.sdkName,
response.sdkVersion,
response.accountContext,
response.capabilities
);
}
}
export default BridgeProxy;
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BridgeResponseEnvelope } from "./BridgeResponseEnvelope.js";
export type BridgeRequest = {
requestId: string;
method: string;
resolve: (
value: BridgeResponseEnvelope | PromiseLike<BridgeResponseEnvelope>
) => void;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
reject: (reason?: any) => void;
};
+29
View File
@@ -0,0 +1,29 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TokenRequest } from "./TokenRequest.js";
export type BridgeMethods = "GetToken" | "GetInitContext" | "GetTokenPopup";
export type BridgeRequestEnvelope = {
messageType: "NestedAppAuthRequest";
method: BridgeMethods;
sendTime?: number; // Assume this is epoch
clientLibrary?: string;
clientLibraryVersion?: string;
requestId: string;
tokenParams?: TokenRequest;
};
export function isBridgeRequestEnvelope(
obj: unknown
): obj is BridgeRequestEnvelope {
return (
(obj as BridgeRequestEnvelope).messageType !== undefined &&
(obj as BridgeRequestEnvelope).messageType === "NestedAppAuthRequest" &&
(obj as BridgeRequestEnvelope).method !== undefined &&
(obj as BridgeRequestEnvelope).requestId !== undefined
);
}
+19
View File
@@ -0,0 +1,19 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BridgeError } from "./BridgeError.js";
import { TokenResponse } from "./TokenResponse.js";
import { AccountInfo } from "./AccountInfo.js";
import { InitContext } from "./InitContext.js";
export type BridgeResponseEnvelope = {
messageType: "NestedAppAuthResponse";
requestId: string;
success: boolean; // false if body is error
token?: TokenResponse;
error?: BridgeError;
account?: AccountInfo;
initContext?: InitContext;
};
+17
View File
@@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export const BridgeStatusCode = {
UserInteractionRequired: "USER_INTERACTION_REQUIRED",
UserCancel: "USER_CANCEL",
NoNetwork: "NO_NETWORK",
TransientError: "TRANSIENT_ERROR",
PersistentError: "PERSISTENT_ERROR",
Disabled: "DISABLED",
AccountUnavailable: "ACCOUNT_UNAVAILABLE",
NestedAppAuthUnavailable: "NESTED_APP_AUTH_UNAVAILABLE", // NAA is unavailable in the current context, can retry with standard browser based auth
} as const;
export type BridgeStatusCode =
(typeof BridgeStatusCode)[keyof typeof BridgeStatusCode];
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AuthResult } from "./AuthResult.js";
import { AccountContext } from "./BridgeAccountContext.js";
import { BridgeCapabilities } from "./BridgeCapabilities.js";
import { TokenRequest } from "./TokenRequest.js";
export interface IBridgeProxy {
getTokenInteractive(request: TokenRequest): Promise<AuthResult>;
getTokenSilent(request: TokenRequest): Promise<AuthResult>;
getHostCapabilities(): BridgeCapabilities | null;
getAccountContext(): AccountContext | null;
}
+14
View File
@@ -0,0 +1,14 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BridgeCapabilities } from "./BridgeCapabilities.js";
import { AccountContext } from "./BridgeAccountContext.js";
export interface InitContext {
capabilities?: BridgeCapabilities;
sdkName: string;
sdkVersion: string;
accountContext?: AccountContext;
}
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type TokenRequest = {
platformBrokerId?: string; // Account identifier used by OneAuth
clientId: string;
authority?: string;
scope: string;
correlationId: string;
claims?: string;
state?: string;
reqCnf?: string; // Having OneAuth own the keypair is better for hardware token binding support
keyId?: string; // Having OneAuth own the keypair is better for hardware token binding support
authenticationScheme?: string;
shrClaims?: string;
shrNonce?: string;
resourceRequestMethod?: string;
resourceRequestUri?: string;
extendedExpiryToken?: boolean;
extraParameters?: Map<string, string>;
};
+57
View File
@@ -0,0 +1,57 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
export type TokenResponse = {
access_token: string;
expires_in: number;
id_token: string;
properties: TokenResponseProperties | null;
scope?: string;
shr?: string; // token binding enabled at native layer it is the access token, not the signing keys
extendedLifetimeToken?: boolean;
authority?: string;
};
export type TokenResponseProperties = {
MATS?: string;
};
/*
* Sample response below
* {
* "body":{
* "access_token":"<token>",
* "account":{"environment":"login.microsoftonline.com",
* "homeAccountId":"2995ae49-d9dd-409d-8d62-ba969ce58a81.51178b70-16cc-41b5-bef1-ae1808139065",
* "idTokenClaims":{
* "aud":"a076930c-cfc9-4ebd-9607-7963bccbf666",
* "exp":"1680557128",
* "graph_url":"https://graph.microsoft.com",
* "iat":"1680553228",
* "iss":"https://login.microsoftonline.com/51178b70-16cc-41b5-bef1-ae1808139065/v2.0",
* "name":"Adele Vance",
* "nbf":"1680553228",
* "oid":"2995ae49-d9dd-409d-8d62-ba969ce58a81",
* "preferred_username":"AdeleV@vc6w6.onmicrosoft.com",
* "rh":"0.AX0AcIsXUcwWtUG-8a4YCBOQZQyTdqDJz71Olgd5Y7zL9maaAHs.",
* "sovereignty2":"Global",
* "sub":"wtxUI1WD2C--Bl8vN1p-P-VgadGud8QSqXD4Vp5i9sc",
* "tid":"51178b70-16cc-41b5-bef1-ae1808139065",
* "uti":"39pEKQyYDU6SXjD_phaCAA",
* "ver":"2.0"},
* "localAccountId":"2995ae49-d9dd-409d-8d62-ba969ce58a81",
* "name":"Adele Vance",
* "tenantId":"51178b70-16cc-41b5-bef1-ae1808139065",
* "username":"AdeleV@vc6w6.onmicrosoft.com"
* },
* "client_info":"",
* "expires_in":4290,
* "id_token":"",
* "properties":null,
* "scope":"User.Read",
* "state":""},
* "requestId":"8863d6e8-a539-43e3-84b6-ca1bf64943f3",
* "success":true}
*/
@@ -0,0 +1,319 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { TokenRequest } from "../TokenRequest.js";
import { AccountInfo as NaaAccountInfo } from "../AccountInfo.js";
import { RedirectRequest } from "../../request/RedirectRequest.js";
import { PopupRequest } from "../../request/PopupRequest.js";
import {
AccountInfo as MsalAccountInfo,
AuthError,
ClientAuthError,
ClientConfigurationError,
InteractionRequiredAuthError,
ServerError,
ICrypto,
Logger,
AuthToken,
TokenClaims,
ClientAuthErrorCodes,
AuthenticationScheme,
RequestParameterBuilder,
StringUtils,
createClientAuthError,
OIDC_DEFAULT_SCOPES,
AccountInfo,
IdTokenEntity,
AccessTokenEntity,
TenantProfile,
buildTenantProfile,
} from "@azure/msal-common/browser";
import { isBridgeError } from "../BridgeError.js";
import { BridgeStatusCode } from "../BridgeStatusCode.js";
import { AuthenticationResult } from "../../response/AuthenticationResult.js";
import {} from "../../error/BrowserAuthErrorCodes.js";
import { AuthResult } from "../AuthResult.js";
import { SsoSilentRequest } from "../../request/SsoSilentRequest.js";
import { SilentRequest } from "../../request/SilentRequest.js";
export class NestedAppAuthAdapter {
protected crypto: ICrypto;
protected logger: Logger;
protected clientId: string;
protected clientCapabilities: string[];
constructor(
clientId: string,
clientCapabilities: string[],
crypto: ICrypto,
logger: Logger
) {
this.clientId = clientId;
this.clientCapabilities = clientCapabilities;
this.crypto = crypto;
this.logger = logger;
}
public toNaaTokenRequest(
request:
| PopupRequest
| RedirectRequest
| SilentRequest
| SsoSilentRequest
): TokenRequest {
let extraParams: Map<string, string>;
if (request.extraQueryParameters === undefined) {
extraParams = new Map<string, string>();
} else {
extraParams = new Map<string, string>(
Object.entries(request.extraQueryParameters)
);
}
const correlationId =
request.correlationId || this.crypto.createNewGuid();
const requestBuilder = new RequestParameterBuilder(correlationId);
const claims = requestBuilder.addClientCapabilitiesToClaims(
request.claims,
this.clientCapabilities
);
const scopes = request.scopes || OIDC_DEFAULT_SCOPES;
const tokenRequest: TokenRequest = {
platformBrokerId: request.account?.homeAccountId,
clientId: this.clientId,
authority: request.authority,
scope: scopes.join(" "),
correlationId,
claims: !StringUtils.isEmptyObj(claims) ? claims : undefined,
state: request.state,
authenticationScheme:
request.authenticationScheme || AuthenticationScheme.BEARER,
extraParameters: extraParams,
};
return tokenRequest;
}
public fromNaaTokenResponse(
request: TokenRequest,
response: AuthResult,
reqTimestamp: number
): AuthenticationResult {
if (!response.token.id_token || !response.token.access_token) {
throw createClientAuthError(ClientAuthErrorCodes.nullOrEmptyToken);
}
const expiresOn = new Date(
(reqTimestamp + (response.token.expires_in || 0)) * 1000
);
const idTokenClaims = AuthToken.extractTokenClaims(
response.token.id_token,
this.crypto.base64Decode
);
const account = this.fromNaaAccountInfo(
response.account,
response.token.id_token,
idTokenClaims
);
const scopes = response.token.scope || request.scope;
const authenticationResult: AuthenticationResult = {
authority: response.token.authority || account.environment,
uniqueId: account.localAccountId,
tenantId: account.tenantId,
scopes: scopes.split(" "),
account,
idToken: response.token.id_token,
idTokenClaims,
accessToken: response.token.access_token,
fromCache: false,
expiresOn: expiresOn,
tokenType:
request.authenticationScheme || AuthenticationScheme.BEARER,
correlationId: request.correlationId,
extExpiresOn: expiresOn,
state: request.state,
};
return authenticationResult;
}
/*
* export type AccountInfo = {
* homeAccountId: string;
* environment: string;
* tenantId: string;
* username: string;
* localAccountId: string;
* name?: string;
* idToken?: string;
* idTokenClaims?: TokenClaims & {
* [key: string]:
* | string
* | number
* | string[]
* | object
* | undefined
* | unknown;
* };
* nativeAccountId?: string;
* authorityType?: string;
* };
*/
public fromNaaAccountInfo(
fromAccount: NaaAccountInfo,
idToken?: string,
idTokenClaims?: TokenClaims
): MsalAccountInfo {
const effectiveIdTokenClaims =
idTokenClaims || (fromAccount.idTokenClaims as TokenClaims);
const localAccountId =
fromAccount.localAccountId ||
effectiveIdTokenClaims?.oid ||
effectiveIdTokenClaims?.sub ||
"";
const tenantId =
fromAccount.tenantId || effectiveIdTokenClaims?.tid || "";
const homeAccountId =
fromAccount.homeAccountId || `${localAccountId}.${tenantId}`;
const username =
fromAccount.username ||
effectiveIdTokenClaims?.preferred_username ||
"";
const name = fromAccount.name || effectiveIdTokenClaims?.name;
const tenantProfiles = new Map<string, TenantProfile>();
const tenantProfile = buildTenantProfile(
homeAccountId,
localAccountId,
tenantId,
effectiveIdTokenClaims
);
tenantProfiles.set(tenantId, tenantProfile);
const account: MsalAccountInfo = {
homeAccountId,
environment: fromAccount.environment,
tenantId,
username,
localAccountId,
name,
idToken: idToken,
idTokenClaims: effectiveIdTokenClaims,
tenantProfiles,
};
return account;
}
/**
*
* @param error BridgeError
* @returns AuthError, ClientAuthError, ClientConfigurationError, ServerError, InteractionRequiredError
*/
public fromBridgeError(
error: unknown
):
| AuthError
| ClientAuthError
| ClientConfigurationError
| ServerError
| InteractionRequiredAuthError {
if (isBridgeError(error)) {
switch (error.status) {
case BridgeStatusCode.UserCancel:
return new ClientAuthError(
ClientAuthErrorCodes.userCanceled
);
case BridgeStatusCode.NoNetwork:
return new ClientAuthError(
ClientAuthErrorCodes.noNetworkConnectivity
);
case BridgeStatusCode.AccountUnavailable:
return new ClientAuthError(
ClientAuthErrorCodes.noAccountFound
);
case BridgeStatusCode.Disabled:
return new ClientAuthError(
ClientAuthErrorCodes.nestedAppAuthBridgeDisabled
);
case BridgeStatusCode.NestedAppAuthUnavailable:
return new ClientAuthError(
error.code ||
ClientAuthErrorCodes.nestedAppAuthBridgeDisabled,
error.description
);
case BridgeStatusCode.TransientError:
case BridgeStatusCode.PersistentError:
return new ServerError(error.code, error.description);
case BridgeStatusCode.UserInteractionRequired:
return new InteractionRequiredAuthError(
error.code,
error.description
);
default:
return new AuthError(error.code, error.description);
}
} else {
return new AuthError("unknown_error", "An unknown error occurred");
}
}
/**
* Returns an AuthenticationResult from the given cache items
*
* @param account
* @param idToken
* @param accessToken
* @param reqTimestamp
* @returns
*/
public toAuthenticationResultFromCache(
account: AccountInfo,
idToken: IdTokenEntity,
accessToken: AccessTokenEntity,
request: SilentRequest,
correlationId: string
): AuthenticationResult {
if (!idToken || !accessToken) {
throw createClientAuthError(ClientAuthErrorCodes.nullOrEmptyToken);
}
const idTokenClaims = AuthToken.extractTokenClaims(
idToken.secret,
this.crypto.base64Decode
);
const scopes = accessToken.target || request.scopes.join(" ");
const authenticationResult: AuthenticationResult = {
authority: accessToken.environment || account.environment,
uniqueId: account.localAccountId,
tenantId: account.tenantId,
scopes: scopes.split(" "),
account,
idToken: idToken.secret,
idTokenClaims: idTokenClaims || {},
accessToken: accessToken.secret,
fromCache: true,
expiresOn: new Date(Number(accessToken.expiresOn) * 1000),
tokenType:
request.authenticationScheme || AuthenticationScheme.BEARER,
correlationId,
extExpiresOn: new Date(
Number(accessToken.extendedExpiresOn) * 1000
),
state: request.state,
};
return authenticationResult;
}
}
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { NavigationOptions } from "./NavigationOptions.js";
export interface INavigationClient {
/**
* Navigates to other pages within the same web application
* Return false if this doesn't cause the page to reload i.e. Client-side navigation
* @param url
* @param options
*/
navigateInternal(url: string, options: NavigationOptions): Promise<boolean>;
/**
* Navigates to other pages outside the web application i.e. the Identity Provider
* @param url
* @param options
*/
navigateExternal(url: string, options: NavigationOptions): Promise<boolean>;
}
+55
View File
@@ -0,0 +1,55 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { INavigationClient } from "./INavigationClient.js";
import { NavigationOptions } from "./NavigationOptions.js";
export class NavigationClient implements INavigationClient {
/**
* Navigates to other pages within the same web application
* @param url
* @param options
*/
navigateInternal(
url: string,
options: NavigationOptions
): Promise<boolean> {
return NavigationClient.defaultNavigateWindow(url, options);
}
/**
* Navigates to other pages outside the web application i.e. the Identity Provider
* @param url
* @param options
*/
navigateExternal(
url: string,
options: NavigationOptions
): Promise<boolean> {
return NavigationClient.defaultNavigateWindow(url, options);
}
/**
* Default navigation implementation invoked by the internal and external functions
* @param url
* @param options
*/
private static defaultNavigateWindow(
url: string,
options: NavigationOptions
): Promise<boolean> {
if (options.noHistory) {
window.location.replace(url);
} else {
window.location.assign(url);
}
return new Promise((resolve) => {
setTimeout(() => {
resolve(true);
}, options.timeout);
});
}
}
+18
View File
@@ -0,0 +1,18 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { ApiId } from "../utils/BrowserConstants.js";
/**
* Additional information passed to the navigateInternal and navigateExternal functions
*/
export type NavigationOptions = {
/** The Id of the API that initiated navigation */
apiId: ApiId;
/** Suggested timeout (ms) based on the configuration provided to PublicClientApplication */
timeout: number;
/** When set to true the url should not be added to the browser history */
noHistory: boolean;
};
+157
View File
@@ -0,0 +1,157 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
INetworkModule,
NetworkRequestOptions,
NetworkResponse,
createNetworkError,
} from "@azure/msal-common/browser";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { HTTP_REQUEST_TYPE } from "../utils/BrowserConstants.js";
/**
* This class implements the Fetch API for GET and POST requests. See more here: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
*/
export class FetchClient implements INetworkModule {
/**
* Fetch Client for REST endpoints - Get request
* @param url
* @param headers
* @param body
*/
async sendGetRequestAsync<T>(
url: string,
options?: NetworkRequestOptions
): Promise<NetworkResponse<T>> {
let response: Response;
let responseHeaders: Record<string, string> = {};
let responseStatus = 0;
const reqHeaders = getFetchHeaders(options);
try {
response = await fetch(url, {
method: HTTP_REQUEST_TYPE.GET,
headers: reqHeaders,
});
} catch (e) {
throw createBrowserAuthError(
window.navigator.onLine
? BrowserAuthErrorCodes.getRequestFailed
: BrowserAuthErrorCodes.noNetworkConnectivity
);
}
responseHeaders = getHeaderDict(response.headers);
try {
responseStatus = response.status;
return {
headers: responseHeaders,
body: (await response.json()) as T,
status: responseStatus,
};
} catch (e) {
throw createNetworkError(
createBrowserAuthError(
BrowserAuthErrorCodes.failedToParseResponse
),
responseStatus,
responseHeaders
);
}
}
/**
* Fetch Client for REST endpoints - Post request
* @param url
* @param headers
* @param body
*/
async sendPostRequestAsync<T>(
url: string,
options?: NetworkRequestOptions
): Promise<NetworkResponse<T>> {
const reqBody = (options && options.body) || "";
const reqHeaders = getFetchHeaders(options);
let response: Response;
let responseStatus = 0;
let responseHeaders: Record<string, string> = {};
try {
response = await fetch(url, {
method: HTTP_REQUEST_TYPE.POST,
headers: reqHeaders,
body: reqBody,
});
} catch (e) {
throw createBrowserAuthError(
window.navigator.onLine
? BrowserAuthErrorCodes.postRequestFailed
: BrowserAuthErrorCodes.noNetworkConnectivity
);
}
responseHeaders = getHeaderDict(response.headers);
try {
responseStatus = response.status;
return {
headers: responseHeaders,
body: (await response.json()) as T,
status: responseStatus,
};
} catch (e) {
throw createNetworkError(
createBrowserAuthError(
BrowserAuthErrorCodes.failedToParseResponse
),
responseStatus,
responseHeaders
);
}
}
}
/**
* Get Fetch API Headers object from string map
* @param inputHeaders
*/
function getFetchHeaders(options?: NetworkRequestOptions): Headers {
try {
const headers = new Headers();
if (!(options && options.headers)) {
return headers;
}
const optionsHeaders = options.headers;
Object.entries(optionsHeaders).forEach(([key, value]) => {
headers.append(key, value);
});
return headers;
} catch (e) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.failedToBuildHeaders
);
}
}
/**
* Returns object representing response headers
* @param headers
* @returns
*/
function getHeaderDict(headers: Headers): Record<string, string> {
try {
const headerDict: Record<string, string> = {};
headers.forEach((value: string, key: string) => {
headerDict[key] = value;
});
return headerDict;
} catch (e) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.failedToParseHeaders
);
}
}
@@ -0,0 +1,139 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Logger, LogLevel } from "@azure/msal-common/browser";
import {
BrowserConfiguration,
buildConfiguration,
Configuration,
} from "../config/Configuration.js";
import { version, name } from "../packageMetadata.js";
import {
BrowserCacheLocation,
LOG_LEVEL_CACHE_KEY,
LOG_PII_CACHE_KEY,
} from "../utils/BrowserConstants.js";
/**
* Base class for operating context
* Operating contexts are contexts in which MSAL.js is being run
* More than one operating context may be available at a time
* It's important from a logging and telemetry point of view for us to be able to identify the operating context.
* For example: Some operating contexts will pre-cache tokens impacting performance telemetry
*/
export abstract class BaseOperatingContext {
protected logger: Logger;
protected config: BrowserConfiguration;
protected available: boolean;
protected browserEnvironment: boolean;
protected static loggerCallback(level: LogLevel, message: string): void {
switch (level) {
case LogLevel.Error:
// eslint-disable-next-line no-console
console.error(message);
return;
case LogLevel.Info:
// eslint-disable-next-line no-console
console.info(message);
return;
case LogLevel.Verbose:
// eslint-disable-next-line no-console
console.debug(message);
return;
case LogLevel.Warning:
// eslint-disable-next-line no-console
console.warn(message);
return;
default:
// eslint-disable-next-line no-console
console.log(message);
return;
}
}
constructor(config: Configuration) {
/*
* If loaded in an environment where window is not available,
* set internal flag to false so that further requests fail.
* This is to support server-side rendering environments.
*/
this.browserEnvironment = typeof window !== "undefined";
this.config = buildConfiguration(config, this.browserEnvironment);
let sessionStorage: Storage | undefined;
try {
sessionStorage = window[BrowserCacheLocation.SessionStorage];
// Mute errors if it's a non-browser environment or cookies are blocked.
} catch (e) {}
const logLevelKey = sessionStorage?.getItem(LOG_LEVEL_CACHE_KEY);
const piiLoggingKey = sessionStorage
?.getItem(LOG_PII_CACHE_KEY)
?.toLowerCase();
const piiLoggingEnabled =
piiLoggingKey === "true"
? true
: piiLoggingKey === "false"
? false
: undefined;
const loggerOptions = { ...this.config.system.loggerOptions };
const logLevel =
logLevelKey && Object.keys(LogLevel).includes(logLevelKey)
? LogLevel[logLevelKey]
: undefined;
if (logLevel) {
loggerOptions.loggerCallback = BaseOperatingContext.loggerCallback;
loggerOptions.logLevel = logLevel;
}
if (piiLoggingEnabled !== undefined) {
loggerOptions.piiLoggingEnabled = piiLoggingEnabled;
}
this.logger = new Logger(loggerOptions, name, version);
this.available = false;
}
/**
* returns the name of the module containing the API controller associated with this operating context
*/
abstract getModuleName(): string;
/**
* returns the string identifier of this operating context
*/
abstract getId(): string;
/**
* returns a boolean indicating whether this operating context is present
*/
abstract initialize(): Promise<boolean>;
/**
* Return the MSAL config
* @returns BrowserConfiguration
*/
getConfig(): BrowserConfiguration {
return this.config;
}
/**
* Returns the MSAL Logger
* @returns Logger
*/
getLogger(): Logger {
return this.logger;
}
isAvailable(): boolean {
return this.available;
}
isBrowserEnvironment(): boolean {
return this.browserEnvironment;
}
}
@@ -0,0 +1,88 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseOperatingContext } from "./BaseOperatingContext.js";
import { IBridgeProxy } from "../naa/IBridgeProxy.js";
import { BridgeProxy } from "../naa/BridgeProxy.js";
import { AccountContext } from "../naa/BridgeAccountContext.js";
declare global {
interface Window {
__initializeNestedAppAuth?(): Promise<void>;
}
}
export class NestedAppOperatingContext extends BaseOperatingContext {
protected bridgeProxy: IBridgeProxy | undefined = undefined;
protected accountContext: AccountContext | null = null;
/*
* TODO: Once we have determine the bundling code return here to specify the name of the bundle
* containing the implementation for this operating context
*/
static readonly MODULE_NAME: string = "";
/**
* Unique identifier for the operating context
*/
static readonly ID: string = "NestedAppOperatingContext";
/**
* Return the module name. Intended for use with import() to enable dynamic import
* of the implementation associated with this operating context
* @returns
*/
getModuleName(): string {
return NestedAppOperatingContext.MODULE_NAME;
}
/**
* Returns the unique identifier for this operating context
* @returns string
*/
getId(): string {
return NestedAppOperatingContext.ID;
}
/**
* Returns the current BridgeProxy
* @returns IBridgeProxy | undefined
*/
getBridgeProxy(): IBridgeProxy | undefined {
return this.bridgeProxy;
}
/**
* Checks whether the operating context is available.
* Confirms that the code is running a browser rather. This is required.
* @returns Promise<boolean> indicating whether this operating context is currently available.
*/
async initialize(): Promise<boolean> {
try {
if (typeof window !== "undefined") {
if (typeof window.__initializeNestedAppAuth === "function") {
await window.__initializeNestedAppAuth();
}
const bridgeProxy: IBridgeProxy = await BridgeProxy.create();
/*
* Because we want single sign on we expect the host app to provide the account context
* with a min set of params that can be used to identify the account
* this.account = nestedApp.getAccountByFilter(bridgeProxy.getAccountContext());
*/
this.accountContext = bridgeProxy.getAccountContext();
this.bridgeProxy = bridgeProxy;
this.available = bridgeProxy !== undefined;
}
} catch (ex) {
this.logger.infoPii(
`Could not initialize Nested App Auth bridge (${ex})`
);
}
this.logger.info(`Nested App Auth Bridge available: ${this.available}`);
return this.available;
}
}
@@ -0,0 +1,50 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseOperatingContext } from "./BaseOperatingContext.js";
export class StandardOperatingContext extends BaseOperatingContext {
/*
* TODO: Once we have determine the bundling code return here to specify the name of the bundle
* containing the implementation for this operating context
*/
static readonly MODULE_NAME: string = "";
/**
* Unique identifier for the operating context
*/
static readonly ID: string = "StandardOperatingContext";
/**
* Return the module name. Intended for use with import() to enable dynamic import
* of the implementation associated with this operating context
* @returns
*/
getModuleName(): string {
return StandardOperatingContext.MODULE_NAME;
}
/**
* Returns the unique identifier for this operating context
* @returns string
*/
getId(): string {
return StandardOperatingContext.ID;
}
/**
* Checks whether the operating context is available.
* Confirms that the code is running a browser rather. This is required.
* @returns Promise<boolean> indicating whether this operating context is currently available.
*/
async initialize(): Promise<boolean> {
this.available = typeof window !== "undefined";
return this.available;
/*
* NOTE: The standard context is available as long as there is a window. If/when we split out WAM from Browser
* We can move the current contents of the initialize method to here and verify that the WAM extension is available
*/
}
}
@@ -0,0 +1,49 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { BaseOperatingContext } from "./BaseOperatingContext.js";
export class UnknownOperatingContext extends BaseOperatingContext {
/*
* TODO: Once we have determine the bundling code return here to specify the name of the bundle
* containing the implementation for this operating context
*/
static readonly MODULE_NAME: string = "";
/**
* Unique identifier for the operating context
*/
static readonly ID: string = "UnknownOperatingContext";
/**
* Returns the unique identifier for this operating context
* @returns string
*/
getId(): string {
return UnknownOperatingContext.ID;
}
/**
* Return the module name. Intended for use with import() to enable dynamic import
* of the implementation associated with this operating context
* @returns
*/
getModuleName(): string {
return UnknownOperatingContext.MODULE_NAME;
}
/**
* Checks whether the operating context is available.
* Confirms that the code is running a browser rather. This is required.
* @returns Promise<boolean> indicating whether this operating context is currently available.
*/
async initialize(): Promise<boolean> {
/**
* This operating context is in use when we have not checked for what the operating context is.
* The context is unknown until we check it.
*/
return true;
}
}
+3
View File
@@ -0,0 +1,3 @@
/* eslint-disable header/header */
export const name = "@azure/msal-browser";
export const version = "4.5.1";
@@ -0,0 +1,19 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CommonAuthorizationCodeRequest } from "@azure/msal-common/browser";
export type AuthorizationCodeRequest = Partial<
Omit<
CommonAuthorizationCodeRequest,
"code" | "enableSpaAuthorizationCode" | "requestedClaimsHash"
>
> & {
code?: string;
nativeAccountId?: string;
cloudGraphHostName?: string;
msGraphHost?: string;
cloudInstanceHostName?: string;
};
@@ -0,0 +1,17 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CommonAuthorizationUrlRequest } from "@azure/msal-common/browser";
/**
* This type is deprecated and will be removed on the next major version update
*/
export type AuthorizationUrlRequest = Omit<
CommonAuthorizationUrlRequest,
"state" | "nonce" | "requestedClaimsHash" | "platformBroker"
> & {
state: string;
nonce: string;
};
+16
View File
@@ -0,0 +1,16 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { AccountInfo } from "@azure/msal-common/browser";
/**
* ClearCacheRequest
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - account - Account object that will be logged out of. All tokens tied to this account will be cleared.
*/
export type ClearCacheRequest = {
correlationId?: string;
account?: AccountInfo | null;
};
+28
View File
@@ -0,0 +1,28 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CommonEndSessionRequest } from "@azure/msal-common/browser";
import { PopupWindowAttributes } from "./PopupWindowAttributes.js";
/**
* EndSessionPopupRequest
* - account - Account object that will be logged out of. All tokens tied to this account will be cleared.
* - postLogoutRedirectUri - URI to navigate to after logout page inside the popup. Required to ensure popup can be closed.
* - authority - Authority to send logout request to.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - idTokenHint - ID Token used by B2C to validate logout if required by the policy
* - mainWindowRedirectUri - URI to navigate the main window to after logout is complete
* - popupWindowAttributes - Optional popup window attributes. popupSize with height and width, and popupPosition with top and left can be set.
* - logoutHint - A string that specifies the account that is being logged out in order to skip the server account picker on logout
* - popupWindowParent - Optional window object to use as the parent when opening popup windows. Uses global `window` if not given.
*/
export type EndSessionPopupRequest = Partial<
Omit<CommonEndSessionRequest, "tokenQueryParameters">
> & {
authority?: string;
mainWindowRedirectUri?: string;
popupWindowAttributes?: PopupWindowAttributes;
popupWindowParent?: Window;
};
+23
View File
@@ -0,0 +1,23 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { CommonEndSessionRequest } from "@azure/msal-common/browser";
/**
* EndSessionRequest
* - account - Account object that will be logged out of. All tokens tied to this account will be cleared.
* - postLogoutRedirectUri - URI to navigate to after logout page.
* - authority - Authority to send logout request to.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - idTokenHint - ID Token used by B2C to validate logout if required by the policy
* - onRedirectNavigate - Callback that will be passed the url that MSAL will navigate to. Returning false in the callback will stop navigation.
* - logoutHint - A string that specifies the account that is being logged out in order to skip the server account picker on logout
*/
export type EndSessionRequest = Partial<
Omit<CommonEndSessionRequest, "tokenQueryParameters">
> & {
authority?: string;
onRedirectNavigate?: (url: string) => boolean | void;
};
@@ -0,0 +1,13 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* InitializeApplicationRequest: Request object passed by user to initialize application
*
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
*/
export type InitializeApplicationRequest = {
correlationId?: string;
};
+56
View File
@@ -0,0 +1,56 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
CommonAuthorizationUrlRequest,
StringDict,
} from "@azure/msal-common/browser";
import { PopupWindowAttributes } from "./PopupWindowAttributes.js";
/**
* PopupRequest: Request object passed by user to retrieve a Code from the
* server (first leg of authorization code grant flow) with a popup window.
*
* - scopes - Array of scopes the application is requesting access to.
* - authority - Url of the authority which the application acquires tokens from.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - redirectUri - The redirect URI where authentication responses can be received by your application. It must exactly match one of the redirect URIs registered in the Azure portal.
* - extraScopesToConsent - Scopes for a different resource when the user needs consent upfront.
* - state - A value included in the request that is also returned in the token response. A randomly generated unique value is typically used for preventing cross site request forgery attacks. The state is also used to encode information about the user's state in the app before the authentication request occurred.
* - prompt - Indicates the type of user interaction that is required.
* login: will force the user to enter their credentials on that request, negating single-sign on
* none: will ensure that the user isn't presented with any interactive prompt. if request can't be completed via single-sign on, the endpoint will return an interaction_required error
* consent: will the trigger the OAuth consent dialog after the user signs in, asking the user to grant permissions to the app
* select_account: will interrupt single sign-=on providing account selection experience listing all the accounts in session or any remembered accounts or an option to choose to use a different account
* create: will direct the user to the account creation experience instead of the log in experience
* no_session: will not read existing session token when authenticating the user. Upon user being successfully authenticated, EVO wont create a new session for the user. FOR INTERNAL USE ONLY.
* - loginHint - Can be used to pre-fill the username/email address field of the sign-in page for the user, if you know the username/email address ahead of time. Often apps use this parameter during re-authentication, having already extracted the username from a previous sign-in using the login_hint or preferred_username claim.
* - sid - Session ID, unique identifier for the session. Available as an optional claim on ID tokens.
* - domainHint - Provides a hint about the tenant or domain that the user should use to sign in. The value of the domain hint is a registered domain for the tenant.
* - extraQueryParameters - String to string map of custom query parameters added to the /authorize call
* - tokenBodyParameters - String to string map of custom token request body parameters added to the /token call. Only used when renewing access tokens.
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call
* - claims - In cases where Azure AD tenant admin has enabled conditional access policies, and the policy has not been met, exceptions will contain claims that need to be consented to.
* - nonce - A value included in the request that is returned in the id token. A randomly generated unique value is typically used to mitigate replay attacks.
* - popupWindowAttributes - Optional popup window attributes. popupSize with height and width, and popupPosition with top and left can be set.
* - popupWindowParent - Optional window object to use as the parent when opening popup windows. Uses global `window` if not given.
*/
export type PopupRequest = Partial<
Omit<
CommonAuthorizationUrlRequest,
| "responseMode"
| "scopes"
| "codeChallenge"
| "codeChallengeMethod"
| "requestedClaimsHash"
| "platformBroker"
>
> & {
scopes: Array<string>;
popupWindowAttributes?: PopupWindowAttributes;
tokenBodyParameters?: StringDict;
popupWindowParent?: Window;
};
+22
View File
@@ -0,0 +1,22 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
/**
* Popup configurations for setting dimensions and position of popup window
*/
export type PopupWindowAttributes = {
popupSize?: PopupSize;
popupPosition?: PopupPosition;
};
export type PopupSize = {
height: number;
width: number;
};
export type PopupPosition = {
top: number;
left: number;
};
+59
View File
@@ -0,0 +1,59 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
CommonAuthorizationUrlRequest,
StringDict,
} from "@azure/msal-common/browser";
/**
* RedirectRequest: Request object passed by user to retrieve a Code from the
* server (first leg of authorization code grant flow) with a full page redirect.
*
* - scopes - Array of scopes the application is requesting access to.
* - authority - Url of the authority which the application acquires tokens from.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - redirectUri - The redirect URI where authentication responses can be received by your application. It must exactly match one of the redirect URIs registered in the Azure portal.
* - extraScopesToConsent - Scopes for a different resource when the user needs consent upfront.
* - state - A value included in the request that is also returned in the token response. A randomly generated unique value is typically used for preventing cross site request forgery attacks. The state is also used to encode information about the user's state in the app before the authentication request occurred.
* - prompt - Indicates the type of user interaction that is required.
* login: will force the user to enter their credentials on that request, negating single-sign on
* none: will ensure that the user isn't presented with any interactive prompt. if request can't be completed via single-sign on, the endpoint will return an interaction_required error
* consent: will the trigger the OAuth consent dialog after the user signs in, asking the user to grant permissions to the app
* select_account: will interrupt single sign-=on providing account selection experience listing all the accounts in session or any remembered accounts or an option to choose to use a different account
* create: will direct the user to the account creation experience instead of the log in experience
* no_session: will not read existing session token when authenticating the user. Upon user being successfully authenticated, EVO wont create a new session for the user. FOR INTERNAL USE ONLY.
* - loginHint - Can be used to pre-fill the username/email address field of the sign-in page for the user, if you know the username/email address ahead of time. Often apps use this parameter during re-authentication, having already extracted the username from a previous sign-in using the login_hint or preferred_username claim.
* - sid - Session ID, unique identifier for the session. Available as an optional claim on ID tokens.
* - domainHint - Provides a hint about the tenant or domain that the user should use to sign in. The value of the domain hint is a registered domain for the tenant.
* - extraQueryParameters - String to string map of custom query parameters added to the /authorize call
* - tokenBodyParameters - String to string map of custom token request body parameters added to the /token call. Only used when renewing access tokens.
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call
* - claims - In cases where Azure AD tenant admin has enabled conditional access policies, and the policy has not been met, exceptions will contain claims that need to be consented to.
* - nonce - A value included in the request that is returned in the id token. A randomly generated unique value is typically used to mitigate replay attacks.
* - redirectStartPage - The page that should be returned to after loginRedirect or acquireTokenRedirect. This should only be used if this is different from the redirectUri and will default to the page that initiates the request. When the navigateToLoginRequestUrl config option is set to false this parameter will be ignored.
* - onRedirectNavigate - Callback that will be passed the url that MSAL will navigate to. Returning false in the callback will stop navigation.
*/
export type RedirectRequest = Partial<
Omit<
CommonAuthorizationUrlRequest,
| "responseMode"
| "scopes"
| "codeChallenge"
| "codeChallengeMethod"
| "requestedClaimsHash"
| "platformBroker"
>
> & {
scopes: Array<string>;
redirectStartPage?: string;
/**
* @deprecated
* onRedirectNavigate is deprecated and will be removed in the next major version.
* Set onRedirectNavigate in Configuration instead.
*/
onRedirectNavigate?: (url: string) => boolean | void;
tokenBodyParameters?: StringDict;
};
+112
View File
@@ -0,0 +1,112 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AccountInfo,
AuthenticationScheme,
BaseAuthRequest,
ClientConfigurationErrorCodes,
CommonSilentFlowRequest,
IPerformanceClient,
Logger,
PerformanceEvents,
StringUtils,
createClientConfigurationError,
invokeAsync,
} from "@azure/msal-common/browser";
import { BrowserConfiguration } from "../config/Configuration.js";
import { SilentRequest } from "./SilentRequest.js";
import { hashString } from "../crypto/BrowserCrypto.js";
/**
* Initializer function for all request APIs
* @param request
*/
export async function initializeBaseRequest(
request: Partial<BaseAuthRequest> & { correlationId: string },
config: BrowserConfiguration,
performanceClient: IPerformanceClient,
logger: Logger
): Promise<BaseAuthRequest> {
performanceClient.addQueueMeasurement(
PerformanceEvents.InitializeBaseRequest,
request.correlationId
);
const authority = request.authority || config.auth.authority;
const scopes = [...((request && request.scopes) || [])];
const validatedRequest: BaseAuthRequest = {
...request,
correlationId: request.correlationId,
authority,
scopes,
};
// Set authenticationScheme to BEARER if not explicitly set in the request
if (!validatedRequest.authenticationScheme) {
validatedRequest.authenticationScheme = AuthenticationScheme.BEARER;
logger.verbose(
'Authentication Scheme wasn\'t explicitly set in request, defaulting to "Bearer" request'
);
} else {
if (
validatedRequest.authenticationScheme === AuthenticationScheme.SSH
) {
if (!request.sshJwk) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.missingSshJwk
);
}
if (!request.sshKid) {
throw createClientConfigurationError(
ClientConfigurationErrorCodes.missingSshKid
);
}
}
logger.verbose(
`Authentication Scheme set to "${validatedRequest.authenticationScheme}" as configured in Auth request`
);
}
// Set requested claims hash if claims-based caching is enabled and claims were requested
if (
config.cache.claimsBasedCachingEnabled &&
request.claims &&
// Checks for empty stringified object "{}" which doesn't qualify as requested claims
!StringUtils.isEmptyObj(request.claims)
) {
validatedRequest.requestedClaimsHash = await hashString(request.claims);
}
return validatedRequest;
}
export async function initializeSilentRequest(
request: SilentRequest & { correlationId: string },
account: AccountInfo,
config: BrowserConfiguration,
performanceClient: IPerformanceClient,
logger: Logger
): Promise<CommonSilentFlowRequest> {
performanceClient.addQueueMeasurement(
PerformanceEvents.InitializeSilentRequest,
request.correlationId
);
const baseRequest = await invokeAsync(
initializeBaseRequest,
PerformanceEvents.InitializeBaseRequest,
logger,
performanceClient,
request.correlationId
)(request, config, performanceClient, logger);
return {
...request,
...baseRequest,
account: account,
forceRefresh: request.forceRefresh || false,
};
}
+50
View File
@@ -0,0 +1,50 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AccountInfo,
CommonSilentFlowRequest,
StringDict,
} from "@azure/msal-common/browser";
import { CacheLookupPolicy } from "../utils/BrowserConstants.js";
/**
* SilentRequest: Request object passed by user to retrieve tokens from the
* cache, renew an expired token with a refresh token, or retrieve a code (first leg of authorization code grant flow)
* in a hidden iframe.
*
* - scopes - Array of scopes the application is requesting access to.
* - authority - Url of the authority which the application acquires tokens from.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - account - Account entity to lookup the credentials.
* - forceRefresh - Forces silent requests to make network calls if true.
* - extraQueryParameters - String to string map of custom query parameters added to the /authorize call. Only used when renewing the refresh token.
* - tokenBodyParameters - String to string map of custom token request body parameters added to the /token call. Only used when renewing access tokens.
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call. Only used when renewing access tokens.
* - redirectUri - The redirect URI where authentication responses can be received by your application. It must exactly match one of the redirect URIs registered in the Azure portal. Only used for cases where refresh token is expired.
* - cacheLookupPolicy - Enum of different ways the silent token can be retrieved.
* - prompt - Indicates the type of user interaction that is required.
* none: will ensure that the user isn't presented with any interactive prompt. if request can't be completed via single-sign on, the endpoint will return an interaction_required error
* no_session: will not read existing session token when authenticating the user. Upon user being successfully authenticated, EVO wont create a new session for the user. FOR INTERNAL USE ONLY.
*/
export type SilentRequest = Omit<
CommonSilentFlowRequest,
| "authority"
| "correlationId"
| "forceRefresh"
| "account"
| "requestedClaimsHash"
> & {
redirectUri?: string;
extraQueryParameters?: StringDict;
authority?: string;
account?: AccountInfo;
correlationId?: string;
forceRefresh?: boolean;
cacheLookupPolicy?: CacheLookupPolicy;
prompt?: string;
state?: string;
tokenBodyParameters?: StringDict;
};
+47
View File
@@ -0,0 +1,47 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
CommonAuthorizationUrlRequest,
StringDict,
} from "@azure/msal-common/browser";
/**
* Request object passed by user to ssoSilent to retrieve a Code from the server (first leg of authorization code grant flow)
*
* - scopes - Array of scopes the application is requesting access to (optional for ssoSilent calls)
* - claims - A stringified claims request which will be added to all /authorize and /token calls
* - authority - Url of the authority which the application acquires tokens from.
* - correlationId - Unique GUID set per request to trace a request end-to-end for telemetry purposes.
* - redirectUri - The redirect URI where authentication responses can be received by your application. It must exactly match one of the redirect URIs registered in the Azure portal.
* - extraScopesToConsent - Scopes for a different resource when the user needs consent upfront.
* - state - A value included in the request that is also returned in the token response. A randomly generated unique value is typically used for preventing cross site request forgery attacks. The state is also used to encode information about the user's state in the app before the authentication request occurred.
* - prompt - Indicates the type of user interaction that is required.
* login: will force the user to enter their credentials on that request, negating single-sign on
* none: will ensure that the user isn't presented with any interactive prompt. if request can't be completed via single-sign on, the endpoint will return an interaction_required error
* consent: will trigger the OAuth consent dialog after the user signs in, asking the user to grant permissions to the app
* select_account: will interrupt single sign-=on providing account selection experience listing all the accounts in session or any remembered accounts or an option to choose to use a different account
* create: will direct the user to the account creation experience instead of the log in experience
* no_session: will not read existing session token when authenticating the user. Upon user being successfully authenticated, EVO wont create a new session for the user. FOR INTERNAL USE ONLY.
* - loginHint - Can be used to pre-fill the username/email address field of the sign-in page for the user, if you know the username/email address ahead of time. Often apps use this parameter during re-authentication, having already extracted the username from a previous sign-in using the login_hint or preferred_username claim.
* - sid - Session ID, unique identifier for the session. Available as an optional claim on ID tokens.
* - domainHint - Provides a hint about the tenant or domain that the user should use to sign in. The value of the domain hint is a registered domain for the tenant.
* - extraQueryParameters - String to string map of custom query parameters added to the /authorize call
* - tokenBodyParameters - String to string map of custom token request body parameters added to the /token call. Only used when renewing access tokens.
* - tokenQueryParameters - String to string map of custom query parameters added to the /token call
* - nonce - A value included in the request that is returned in the id token. A randomly generated unique value is typically used to mitigate replay attacks.
*/
export type SsoSilentRequest = Partial<
Omit<
CommonAuthorizationUrlRequest,
| "responseMode"
| "codeChallenge"
| "codeChallengeMethod"
| "requestedClaimsHash"
| "platformBroker"
>
> & {
tokenBodyParameters?: StringDict;
};
+13
View File
@@ -0,0 +1,13 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
AccountInfo,
AuthenticationResult as CommonAuthenticationResult,
} from "@azure/msal-common/browser";
export type AuthenticationResult = CommonAuthenticationResult & {
account: AccountInfo;
};
+73
View File
@@ -0,0 +1,73 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
ICrypto,
Logger,
ServerAuthorizationCodeResponse,
UrlUtils,
} from "@azure/msal-common/browser";
import {
BrowserAuthErrorCodes,
createBrowserAuthError,
} from "../error/BrowserAuthError.js";
import { extractBrowserRequestState } from "../utils/BrowserProtocolUtils.js";
import { InteractionType } from "../utils/BrowserConstants.js";
export function deserializeResponse(
responseString: string,
responseLocation: string,
logger: Logger
): ServerAuthorizationCodeResponse {
// Deserialize hash fragment response parameters.
const serverParams = UrlUtils.getDeserializedResponse(responseString);
if (!serverParams) {
if (!UrlUtils.stripLeadingHashOrQuery(responseString)) {
// Hash or Query string is empty
logger.error(
`The request has returned to the redirectUri but a ${responseLocation} is not present. It's likely that the ${responseLocation} has been removed or the page has been redirected by code running on the redirectUri page.`
);
throw createBrowserAuthError(BrowserAuthErrorCodes.hashEmptyError);
} else {
logger.error(
`A ${responseLocation} is present in the iframe but it does not contain known properties. It's likely that the ${responseLocation} has been replaced by code running on the redirectUri page.`
);
logger.errorPii(
`The ${responseLocation} detected is: ${responseString}`
);
throw createBrowserAuthError(
BrowserAuthErrorCodes.hashDoesNotContainKnownProperties
);
}
}
return serverParams;
}
/**
* Returns the interaction type that the response object belongs to
*/
export function validateInteractionType(
response: ServerAuthorizationCodeResponse,
browserCrypto: ICrypto,
interactionType: InteractionType
): void {
if (!response.state) {
throw createBrowserAuthError(BrowserAuthErrorCodes.noStateInHash);
}
const platformStateObj = extractBrowserRequestState(
browserCrypto,
response.state
);
if (!platformStateObj) {
throw createBrowserAuthError(BrowserAuthErrorCodes.unableToParseState);
}
if (platformStateObj.interactionType !== interactionType) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.stateInteractionTypeMismatch
);
}
}
@@ -0,0 +1,289 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
Constants,
InProgressPerformanceEvent,
IPerformanceClient,
Logger,
PerformanceClient,
PerformanceEvent,
PerformanceEvents,
PreQueueEvent,
SubMeasurement,
} from "@azure/msal-common/browser";
import { Configuration } from "../config/Configuration.js";
import { name, version } from "../packageMetadata.js";
import {
BROWSER_PERF_ENABLED_KEY,
BrowserCacheLocation,
} from "../utils/BrowserConstants.js";
import * as BrowserCrypto from "../crypto/BrowserCrypto.js";
/**
* Returns browser performance measurement module if session flag is enabled. Returns undefined otherwise.
*/
function getPerfMeasurementModule() {
let sessionStorage: Storage | undefined;
try {
sessionStorage = window[BrowserCacheLocation.SessionStorage];
const perfEnabled = sessionStorage?.getItem(BROWSER_PERF_ENABLED_KEY);
if (Number(perfEnabled) === 1) {
return import("./BrowserPerformanceMeasurement.js");
}
// Mute errors if it's a non-browser environment or cookies are blocked.
} catch (e) {}
return undefined;
}
/**
* Returns boolean, indicating whether browser supports window.performance.now() function.
*/
function supportsBrowserPerformanceNow(): boolean {
return (
typeof window !== "undefined" &&
typeof window.performance !== "undefined" &&
typeof window.performance.now === "function"
);
}
/**
* Returns event duration in milliseconds using window performance API if available. Returns undefined otherwise.
* @param startTime {DOMHighResTimeStamp | undefined}
* @returns {number | undefined}
*/
function getPerfDurationMs(
startTime: DOMHighResTimeStamp | undefined
): number | undefined {
if (!startTime || !supportsBrowserPerformanceNow()) {
return undefined;
}
return Math.round(window.performance.now() - startTime);
}
export class BrowserPerformanceClient
extends PerformanceClient
implements IPerformanceClient
{
constructor(
configuration: Configuration,
intFields?: Set<string>,
abbreviations?: Map<string, string>
) {
super(
configuration.auth.clientId,
configuration.auth.authority || `${Constants.DEFAULT_AUTHORITY}`,
new Logger(
configuration.system?.loggerOptions || {},
name,
version
),
name,
version,
configuration.telemetry?.application || {
appName: "",
appVersion: "",
},
intFields,
abbreviations
);
}
generateId(): string {
return BrowserCrypto.createNewGuid();
}
private getPageVisibility(): string | null {
return document.visibilityState?.toString() || null;
}
private deleteIncompleteSubMeasurements(
inProgressEvent: InProgressPerformanceEvent
): void {
void getPerfMeasurementModule()?.then((module) => {
const rootEvent = this.eventsByCorrelationId.get(
inProgressEvent.event.correlationId
);
const isRootEvent =
rootEvent &&
rootEvent.eventId === inProgressEvent.event.eventId;
const incompleteMeasurements: SubMeasurement[] = [];
if (isRootEvent && rootEvent?.incompleteSubMeasurements) {
rootEvent.incompleteSubMeasurements.forEach(
(subMeasurement: SubMeasurement) => {
incompleteMeasurements.push({ ...subMeasurement });
}
);
}
// Clean up remaining marks for incomplete sub-measurements
module.BrowserPerformanceMeasurement.flushMeasurements(
inProgressEvent.event.correlationId,
incompleteMeasurements
);
});
}
/**
* Starts measuring performance for a given operation. Returns a function that should be used to end the measurement.
* Also captures browser page visibilityState.
*
* @param {PerformanceEvents} measureName
* @param {?string} [correlationId]
* @returns {((event?: Partial<PerformanceEvent>) => PerformanceEvent| null)}
*/
startMeasurement(
measureName: string,
correlationId?: string
): InProgressPerformanceEvent {
// Capture page visibilityState and then invoke start/end measurement
const startPageVisibility = this.getPageVisibility();
const inProgressEvent = super.startMeasurement(
measureName,
correlationId
);
const startTime: number | undefined = supportsBrowserPerformanceNow()
? window.performance.now()
: undefined;
const browserMeasurement = getPerfMeasurementModule()?.then(
(module) => {
return new module.BrowserPerformanceMeasurement(
measureName,
inProgressEvent.event.correlationId
);
}
);
void browserMeasurement?.then((measurement) =>
measurement.startMeasurement()
);
return {
...inProgressEvent,
end: (
event?: Partial<PerformanceEvent>,
error?: unknown
): PerformanceEvent | null => {
const res = inProgressEvent.end(
{
...event,
startPageVisibility,
endPageVisibility: this.getPageVisibility(),
durationMs: getPerfDurationMs(startTime),
},
error
);
void browserMeasurement?.then((measurement) =>
measurement.endMeasurement()
);
this.deleteIncompleteSubMeasurements(inProgressEvent);
return res;
},
discard: () => {
inProgressEvent.discard();
void browserMeasurement?.then((measurement) =>
measurement.flushMeasurement()
);
this.deleteIncompleteSubMeasurements(inProgressEvent);
},
};
}
/**
* Adds pre-queue time to preQueueTimeByCorrelationId map.
* @param {PerformanceEvents} eventName
* @param {?string} correlationId
* @returns
*/
setPreQueueTime(
eventName: PerformanceEvents,
correlationId?: string
): void {
if (!supportsBrowserPerformanceNow()) {
this.logger.trace(
`BrowserPerformanceClient: window performance API not available, unable to set telemetry queue time for ${eventName}`
);
return;
}
if (!correlationId) {
this.logger.trace(
`BrowserPerformanceClient: correlationId for ${eventName} not provided, unable to set telemetry queue time`
);
return;
}
const preQueueEvent: PreQueueEvent | undefined =
this.preQueueTimeByCorrelationId.get(correlationId);
/**
* Manually complete queue measurement if there is an incomplete pre-queue event.
* Incomplete pre-queue events are instrumentation bugs that should be fixed.
*/
if (preQueueEvent) {
this.logger.trace(
`BrowserPerformanceClient: Incomplete pre-queue ${preQueueEvent.name} found`,
correlationId
);
this.addQueueMeasurement(
preQueueEvent.name,
correlationId,
undefined,
true
);
}
this.preQueueTimeByCorrelationId.set(correlationId, {
name: eventName,
time: window.performance.now(),
});
}
/**
* Calculates and adds queue time measurement for given performance event.
*
* @param {PerformanceEvents} eventName
* @param {?string} correlationId
* @param {?number} queueTime
* @param {?boolean} manuallyCompleted - indicator for manually completed queue measurements
* @returns
*/
addQueueMeasurement(
eventName: string,
correlationId?: string,
queueTime?: number,
manuallyCompleted?: boolean
): void {
if (!supportsBrowserPerformanceNow()) {
this.logger.trace(
`BrowserPerformanceClient: window performance API not available, unable to add queue measurement for ${eventName}`
);
return;
}
if (!correlationId) {
this.logger.trace(
`BrowserPerformanceClient: correlationId for ${eventName} not provided, unable to add queue measurement`
);
return;
}
const preQueueTime = super.getPreQueueTime(eventName, correlationId);
if (!preQueueTime) {
return;
}
const currentTime = window.performance.now();
const resQueueTime =
queueTime || super.calculateQueuedTime(preQueueTime, currentTime);
return super.addQueueMeasurement(
eventName,
correlationId,
resQueueTime,
manuallyCompleted
);
}
}
@@ -0,0 +1,147 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import {
IPerformanceMeasurement,
SubMeasurement,
} from "@azure/msal-common/browser";
export class BrowserPerformanceMeasurement implements IPerformanceMeasurement {
private readonly measureName: string;
private readonly correlationId: string;
private readonly startMark: string;
private readonly endMark: string;
constructor(name: string, correlationId: string) {
this.correlationId = correlationId;
this.measureName = BrowserPerformanceMeasurement.makeMeasureName(
name,
correlationId
);
this.startMark = BrowserPerformanceMeasurement.makeStartMark(
name,
correlationId
);
this.endMark = BrowserPerformanceMeasurement.makeEndMark(
name,
correlationId
);
}
private static makeMeasureName(name: string, correlationId: string) {
return `msal.measure.${name}.${correlationId}`;
}
private static makeStartMark(name: string, correlationId: string) {
return `msal.start.${name}.${correlationId}`;
}
private static makeEndMark(name: string, correlationId: string) {
return `msal.end.${name}.${correlationId}`;
}
static supportsBrowserPerformance(): boolean {
return (
typeof window !== "undefined" &&
typeof window.performance !== "undefined" &&
typeof window.performance.mark === "function" &&
typeof window.performance.measure === "function" &&
typeof window.performance.clearMarks === "function" &&
typeof window.performance.clearMeasures === "function" &&
typeof window.performance.getEntriesByName === "function"
);
}
/**
* Flush browser marks and measurements.
* @param {string} correlationId
* @param {SubMeasurement} measurements
*/
public static flushMeasurements(
correlationId: string,
measurements: SubMeasurement[]
): void {
if (BrowserPerformanceMeasurement.supportsBrowserPerformance()) {
try {
measurements.forEach((measurement) => {
const measureName =
BrowserPerformanceMeasurement.makeMeasureName(
measurement.name,
correlationId
);
const entriesForMeasurement =
window.performance.getEntriesByName(
measureName,
"measure"
);
if (entriesForMeasurement.length > 0) {
window.performance.clearMeasures(measureName);
window.performance.clearMarks(
BrowserPerformanceMeasurement.makeStartMark(
measureName,
correlationId
)
);
window.performance.clearMarks(
BrowserPerformanceMeasurement.makeEndMark(
measureName,
correlationId
)
);
}
});
} catch (e) {
// Silently catch and return null
}
}
}
startMeasurement(): void {
if (BrowserPerformanceMeasurement.supportsBrowserPerformance()) {
try {
window.performance.mark(this.startMark);
} catch (e) {
// Silently catch
}
}
}
endMeasurement(): void {
if (BrowserPerformanceMeasurement.supportsBrowserPerformance()) {
try {
window.performance.mark(this.endMark);
window.performance.measure(
this.measureName,
this.startMark,
this.endMark
);
} catch (e) {
// Silently catch
}
}
}
flushMeasurement(): number | null {
if (BrowserPerformanceMeasurement.supportsBrowserPerformance()) {
try {
const entriesForMeasurement =
window.performance.getEntriesByName(
this.measureName,
"measure"
);
if (entriesForMeasurement.length > 0) {
const durationMs = entriesForMeasurement[0].duration;
window.performance.clearMeasures(this.measureName);
window.performance.clearMarks(this.startMark);
window.performance.clearMarks(this.endMark);
return durationMs;
}
} catch (e) {
// Silently catch and return null
}
}
return null;
}
}
+252
View File
@@ -0,0 +1,252 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { OIDC_DEFAULT_SCOPES } from "@azure/msal-common/browser";
import { PopupRequest } from "../request/PopupRequest.js";
import { RedirectRequest } from "../request/RedirectRequest.js";
/**
* Constants
*/
export const BrowserConstants = {
/**
* Interaction in progress cache value
*/
INTERACTION_IN_PROGRESS_VALUE: "interaction_in_progress",
/**
* Invalid grant error code
*/
INVALID_GRANT_ERROR: "invalid_grant",
/**
* Default popup window width
*/
POPUP_WIDTH: 483,
/**
* Default popup window height
*/
POPUP_HEIGHT: 600,
/**
* Name of the popup window starts with
*/
POPUP_NAME_PREFIX: "msal",
/**
* Default popup monitor poll interval in milliseconds
*/
DEFAULT_POLL_INTERVAL_MS: 30,
/**
* Msal-browser SKU
*/
MSAL_SKU: "msal.js.browser",
};
export const NativeConstants = {
CHANNEL_ID: "53ee284d-920a-4b59-9d30-a60315b26836",
PREFERRED_EXTENSION_ID: "ppnbnpeolgkicgegkbkbjmhlideopiji",
MATS_TELEMETRY: "MATS",
};
export const NativeExtensionMethod = {
HandshakeRequest: "Handshake",
HandshakeResponse: "HandshakeResponse",
GetToken: "GetToken",
Response: "Response",
} as const;
export type NativeExtensionMethod =
(typeof NativeExtensionMethod)[keyof typeof NativeExtensionMethod];
export const BrowserCacheLocation = {
LocalStorage: "localStorage",
SessionStorage: "sessionStorage",
MemoryStorage: "memoryStorage",
} as const;
export type BrowserCacheLocation =
(typeof BrowserCacheLocation)[keyof typeof BrowserCacheLocation];
/**
* HTTP Request types supported by MSAL.
*/
export const HTTP_REQUEST_TYPE = {
GET: "GET",
POST: "POST",
} as const;
export type HTTP_REQUEST_TYPE =
(typeof HTTP_REQUEST_TYPE)[keyof typeof HTTP_REQUEST_TYPE];
/**
* Temporary cache keys for MSAL, deleted after any request.
*/
export const TemporaryCacheKeys = {
AUTHORITY: "authority",
ACQUIRE_TOKEN_ACCOUNT: "acquireToken.account",
SESSION_STATE: "session.state",
REQUEST_STATE: "request.state",
NONCE_IDTOKEN: "nonce.id_token",
ORIGIN_URI: "request.origin",
RENEW_STATUS: "token.renew.status",
URL_HASH: "urlHash",
REQUEST_PARAMS: "request.params",
SCOPES: "scopes",
INTERACTION_STATUS_KEY: "interaction.status",
CCS_CREDENTIAL: "ccs.credential",
CORRELATION_ID: "request.correlationId",
NATIVE_REQUEST: "request.native",
REDIRECT_CONTEXT: "request.redirect.context",
} as const;
export type TemporaryCacheKeys =
(typeof TemporaryCacheKeys)[keyof typeof TemporaryCacheKeys];
export const StaticCacheKeys = {
ACCOUNT_KEYS: "msal.account.keys",
TOKEN_KEYS: "msal.token.keys",
} as const;
export type StaticCacheKeys =
(typeof StaticCacheKeys)[keyof typeof StaticCacheKeys];
/**
* Cache keys stored in-memory
*/
export const InMemoryCacheKeys = {
WRAPPER_SKU: "wrapper.sku",
WRAPPER_VER: "wrapper.version",
} as const;
export type InMemoryCacheKeys =
(typeof InMemoryCacheKeys)[keyof typeof InMemoryCacheKeys];
/**
* API Codes for Telemetry purposes.
* Before adding a new code you must claim it in the MSAL Telemetry tracker as these number spaces are shared across all MSALs
* 0-99 Silent Flow
* 800-899 Auth Code Flow
*/
export const ApiId = {
acquireTokenRedirect: 861,
acquireTokenPopup: 862,
ssoSilent: 863,
acquireTokenSilent_authCode: 864,
handleRedirectPromise: 865,
acquireTokenByCode: 866,
acquireTokenSilent_silentFlow: 61,
logout: 961,
logoutPopup: 962,
} as const;
export type ApiId = (typeof ApiId)[keyof typeof ApiId];
/*
* Interaction type of the API - used for state and telemetry
*/
export enum InteractionType {
Redirect = "redirect",
Popup = "popup",
Silent = "silent",
None = "none",
}
/**
* Types of interaction currently in progress.
* Used in events in wrapper libraries to invoke functions when certain interaction is in progress or all interactions are complete.
*/
export const InteractionStatus = {
/**
* Initial status before interaction occurs
*/
Startup: "startup",
/**
* Status set when all login calls occuring
*/
Login: "login",
/**
* Status set when logout call occuring
*/
Logout: "logout",
/**
* Status set for acquireToken calls
*/
AcquireToken: "acquireToken",
/**
* Status set for ssoSilent calls
*/
SsoSilent: "ssoSilent",
/**
* Status set when handleRedirect in progress
*/
HandleRedirect: "handleRedirect",
/**
* Status set when interaction is complete
*/
None: "none",
} as const;
export type InteractionStatus =
(typeof InteractionStatus)[keyof typeof InteractionStatus];
export const DEFAULT_REQUEST: RedirectRequest | PopupRequest = {
scopes: OIDC_DEFAULT_SCOPES,
};
/**
* JWK Key Format string (Type MUST be defined for window crypto APIs)
*/
export const KEY_FORMAT_JWK = "jwk";
// Supported wrapper SKUs
export const WrapperSKU = {
React: "@azure/msal-react",
Angular: "@azure/msal-angular",
} as const;
export type WrapperSKU = (typeof WrapperSKU)[keyof typeof WrapperSKU];
// DatabaseStorage Constants
export const DB_NAME = "msal.db";
export const DB_VERSION = 1;
export const DB_TABLE_NAME = `${DB_NAME}.keys`;
export const CacheLookupPolicy = {
/*
* acquireTokenSilent will attempt to retrieve an access token from the cache. If the access token is expired
* or cannot be found the refresh token will be used to acquire a new one. Finally, if the refresh token
* is expired acquireTokenSilent will attempt to acquire new access and refresh tokens.
*/
Default: 0, // 0 is falsy, is equivalent to not passing in a CacheLookupPolicy
/*
* acquireTokenSilent will only look for access tokens in the cache. It will not attempt to renew access or
* refresh tokens.
*/
AccessToken: 1,
/*
* acquireTokenSilent will attempt to retrieve an access token from the cache. If the access token is expired or
* cannot be found, the refresh token will be used to acquire a new one. If the refresh token is expired, it
* will not be renewed and acquireTokenSilent will fail.
*/
AccessTokenAndRefreshToken: 2,
/*
* acquireTokenSilent will not attempt to retrieve access tokens from the cache and will instead attempt to
* exchange the cached refresh token for a new access token. If the refresh token is expired, it will not be
* renewed and acquireTokenSilent will fail.
*/
RefreshToken: 3,
/*
* acquireTokenSilent will not look in the cache for the access token. It will go directly to network with the
* cached refresh token. If the refresh token is expired an attempt will be made to renew it. This is equivalent to
* setting "forceRefresh: true".
*/
RefreshTokenAndNetwork: 4,
/*
* acquireTokenSilent will attempt to renew both access and refresh tokens. It will not look in the cache. This will
* always fail if 3rd party cookies are blocked by the browser.
*/
Skip: 5,
} as const;
export type CacheLookupPolicy =
(typeof CacheLookupPolicy)[keyof typeof CacheLookupPolicy];
export const iFrameRenewalPolicies: CacheLookupPolicy[] = [
CacheLookupPolicy.Default,
CacheLookupPolicy.Skip,
CacheLookupPolicy.RefreshTokenAndNetwork,
];
export const LOG_LEVEL_CACHE_KEY = "msal.browser.log.level";
export const LOG_PII_CACHE_KEY = "msal.browser.log.pii";
export const BROWSER_PERF_ENABLED_KEY = "msal.browser.performance.enabled";
+39
View File
@@ -0,0 +1,39 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { InteractionType } from "./BrowserConstants.js";
import {
ICrypto,
RequestStateObject,
ProtocolUtils,
createClientAuthError,
ClientAuthErrorCodes,
} from "@azure/msal-common/browser";
export type BrowserStateObject = {
interactionType: InteractionType;
};
/**
* Extracts the BrowserStateObject from the state string.
* @param browserCrypto
* @param state
*/
export function extractBrowserRequestState(
browserCrypto: ICrypto,
state: string
): BrowserStateObject | null {
if (!state) {
return null;
}
try {
const requestStateObj: RequestStateObject =
ProtocolUtils.parseRequestState(browserCrypto, state);
return requestStateObj.libraryState.meta as BrowserStateObject;
} catch (e) {
throw createClientAuthError(ClientAuthErrorCodes.invalidState);
}
}
+213
View File
@@ -0,0 +1,213 @@
/*
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { UrlString, invoke, invokeAsync } from "@azure/msal-common/browser";
import {
createBrowserAuthError,
BrowserAuthErrorCodes,
} from "../error/BrowserAuthError.js";
import { BrowserConstants, BrowserCacheLocation } from "./BrowserConstants.js";
import * as BrowserCrypto from "../crypto/BrowserCrypto.js";
import {
BrowserConfigurationAuthErrorCodes,
createBrowserConfigurationAuthError,
} from "../error/BrowserConfigurationAuthError.js";
import { BrowserConfiguration } from "../config/Configuration.js";
/**
* Clears hash from window url.
*/
export function clearHash(contentWindow: Window): void {
// Office.js sets history.replaceState to null
contentWindow.location.hash = "";
if (typeof contentWindow.history.replaceState === "function") {
// Full removes "#" from url
contentWindow.history.replaceState(
null,
"",
`${contentWindow.location.origin}${contentWindow.location.pathname}${contentWindow.location.search}`
);
}
}
/**
* Replaces current hash with hash from provided url
*/
export function replaceHash(url: string): void {
const urlParts = url.split("#");
urlParts.shift(); // Remove part before the hash
window.location.hash = urlParts.length > 0 ? urlParts.join("#") : "";
}
/**
* Returns boolean of whether the current window is in an iframe or not.
*/
export function isInIframe(): boolean {
return window.parent !== window;
}
/**
* Returns boolean of whether or not the current window is a popup opened by msal
*/
export function isInPopup(): boolean {
return (
typeof window !== "undefined" &&
!!window.opener &&
window.opener !== window &&
typeof window.name === "string" &&
window.name.indexOf(`${BrowserConstants.POPUP_NAME_PREFIX}.`) === 0
);
}
// #endregion
/**
* Returns current window URL as redirect uri
*/
export function getCurrentUri(): string {
return typeof window !== "undefined" && window.location
? window.location.href.split("?")[0].split("#")[0]
: "";
}
/**
* Gets the homepage url for the current window location.
*/
export function getHomepage(): string {
const currentUrl = new UrlString(window.location.href);
const urlComponents = currentUrl.getUrlComponents();
return `${urlComponents.Protocol}//${urlComponents.HostNameAndPort}/`;
}
/**
* Throws error if we have completed an auth and are
* attempting another auth request inside an iframe.
*/
export function blockReloadInHiddenIframes(): void {
const isResponseHash = UrlString.hashContainsKnownProperties(
window.location.hash
);
// return an error if called from the hidden iframe created by the msal js silent calls
if (isResponseHash && isInIframe()) {
throw createBrowserAuthError(BrowserAuthErrorCodes.blockIframeReload);
}
}
/**
* Block redirect operations in iframes unless explicitly allowed
* @param interactionType Interaction type for the request
* @param allowRedirectInIframe Config value to allow redirects when app is inside an iframe
*/
export function blockRedirectInIframe(allowRedirectInIframe: boolean): void {
if (isInIframe() && !allowRedirectInIframe) {
// If we are not in top frame, we shouldn't redirect. This is also handled by the service.
throw createBrowserAuthError(BrowserAuthErrorCodes.redirectInIframe);
}
}
/**
* Block redirectUri loaded in popup from calling AcquireToken APIs
*/
export function blockAcquireTokenInPopups(): void {
// Popups opened by msal popup APIs are given a name that starts with "msal."
if (isInPopup()) {
throw createBrowserAuthError(BrowserAuthErrorCodes.blockNestedPopups);
}
}
/**
* Throws error if token requests are made in non-browser environment
* @param isBrowserEnvironment Flag indicating if environment is a browser.
*/
export function blockNonBrowserEnvironment(): void {
if (typeof window === "undefined") {
throw createBrowserAuthError(
BrowserAuthErrorCodes.nonBrowserEnvironment
);
}
}
/**
* Throws error if initialize hasn't been called
* @param initialized
*/
export function blockAPICallsBeforeInitialize(initialized: boolean): void {
if (!initialized) {
throw createBrowserAuthError(
BrowserAuthErrorCodes.uninitializedPublicClientApplication
);
}
}
/**
* Helper to validate app environment before making an auth request
* @param initialized
*/
export function preflightCheck(initialized: boolean): void {
// Block request if not in browser environment
blockNonBrowserEnvironment();
// Block auth requests inside a hidden iframe
blockReloadInHiddenIframes();
// Block redirectUri opened in a popup from calling MSAL APIs
blockAcquireTokenInPopups();
// Block token acquisition before initialize has been called
blockAPICallsBeforeInitialize(initialized);
}
/**
* Helper to validate app enviornment before making redirect request
* @param initialized
* @param config
*/
export function redirectPreflightCheck(
initialized: boolean,
config: BrowserConfiguration
): void {
preflightCheck(initialized);
blockRedirectInIframe(config.system.allowRedirectInIframe);
// Block redirects if memory storage is enabled but storeAuthStateInCookie is not
if (
config.cache.cacheLocation === BrowserCacheLocation.MemoryStorage &&
!config.cache.storeAuthStateInCookie
) {
throw createBrowserConfigurationAuthError(
BrowserConfigurationAuthErrorCodes.inMemRedirectUnavailable
);
}
}
/**
* Adds a preconnect link element to the header which begins DNS resolution and SSL connection in anticipation of the /token request
* @param loginDomain Authority domain, including https protocol e.g. https://login.microsoftonline.com
* @returns
*/
export function preconnect(authority: string): void {
const link = document.createElement("link");
link.rel = "preconnect";
link.href = new URL(authority).origin;
link.crossOrigin = "anonymous";
document.head.appendChild(link);
// The browser will close connection if not used within a few seconds, remove element from the header after 10s
window.setTimeout(() => {
try {
document.head.removeChild(link);
} catch {}
}, 10000); // 10s Timeout
}
/**
* Wrapper function that creates a UUID v7 from the current timestamp.
* @returns {string}
*/
export function createGuid(): string {
return BrowserCrypto.createNewGuid();
}
export { invoke };
export { invokeAsync };