diff --git a/lib/msal-browser/src/app/PublicClientApplication.ts b/lib/msal-browser/src/app/PublicClientApplication.ts index 18c988d1fe..f976ec360b 100644 --- a/lib/msal-browser/src/app/PublicClientApplication.ts +++ b/lib/msal-browser/src/app/PublicClientApplication.ts @@ -29,6 +29,8 @@ import { EndSessionRequest, BaseAuthRequest, Logger, + ServerTelemetryManager, + ServerTelemetryRequest, ServerAuthorizationCodeResponse } from "@azure/msal-common"; import { buildConfiguration, Configuration } from "../config/Configuration"; @@ -38,7 +40,7 @@ import { RedirectHandler } from "../interaction_handler/RedirectHandler"; import { PopupHandler } from "../interaction_handler/PopupHandler"; import { SilentHandler } from "../interaction_handler/SilentHandler"; import { BrowserAuthError } from "../error/BrowserAuthError"; -import { BrowserConstants, TemporaryCacheKeys, DEFAULT_REQUEST, InteractionType } from "../utils/BrowserConstants"; +import { BrowserConstants, TemporaryCacheKeys, ApiId, DEFAULT_REQUEST, InteractionType } from "../utils/BrowserConstants"; import { BrowserUtils } from "../utils/BrowserUtils"; import { version } from "../../package.json"; import { IPublicClientApplication } from "./IPublicClientApplication"; @@ -216,11 +218,21 @@ export class PublicClientApplication implements IPublicClientApplication { return null; } - // Hash contains known properties - handle and return in callback - const currentAuthority = this.browserStorage.getCachedAuthority(); - const authClient = await this.createAuthCodeClient(currentAuthority); - const interactionHandler = new RedirectHandler(authClient, this.browserStorage); - return interactionHandler.handleCodeResponse(responseHash, this.browserCrypto); + const encodedTokenRequest = this.browserStorage.getItem(this.browserStorage.generateCacheKey(TemporaryCacheKeys.REQUEST_PARAMS), CacheSchemaType.TEMPORARY) as string; + const cachedRequest = JSON.parse(this.browserCrypto.base64Decode(encodedTokenRequest)) as AuthorizationCodeRequest; + const serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.handleRedirectPromise, cachedRequest.correlationId); + + try { + // Hash contains known properties - handle and return in callback + const currentAuthority = this.browserStorage.getCachedAuthority(); + const authClient = await this.createAuthCodeClient(serverTelemetryManager, currentAuthority); + const interactionHandler = new RedirectHandler(authClient, this.browserStorage); + return interactionHandler.handleCodeResponse(responseHash, this.browserCrypto); + } catch (e) { + serverTelemetryManager.cacheFailedRequest(e); + this.browserStorage.cleanRequest(); + throw e; + } } /** @@ -246,15 +258,16 @@ export class PublicClientApplication implements IPublicClientApplication { * @param {@link (RedirectRequest:type)} */ async acquireTokenRedirect(request: RedirectRequest): Promise { + // Preflight request + const validRequest: AuthorizationUrlRequest = this.preflightInteractiveRequest(request, InteractionType.REDIRECT); + const serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.acquireTokenRedirect, validRequest.correlationId); + try { - // Preflight request - const validRequest: AuthorizationUrlRequest = this.preflightInteractiveRequest(request, InteractionType.REDIRECT); - // Create auth code request and generate PKCE params const authCodeRequest: AuthorizationCodeRequest = await this.initializeAuthorizationCodeRequest(validRequest); // Initialize the client - const authClient: AuthorizationCodeClient = await this.createAuthCodeClient(validRequest.authority); + const authClient: AuthorizationCodeClient = await this.createAuthCodeClient(serverTelemetryManager, validRequest.authority); // Create redirect interaction handler. const interactionHandler = new RedirectHandler(authClient, this.browserStorage); @@ -266,6 +279,7 @@ export class PublicClientApplication implements IPublicClientApplication { // Show the UI once the url has been created. Response will come back in the hash, which will be handled in the handleRedirectCallback function. interactionHandler.initiateAuthRequest(navigateUrl, authCodeRequest, redirectStartPage, this.browserCrypto); } catch (e) { + serverTelemetryManager.cacheFailedRequest(e); this.browserStorage.cleanRequest(); throw e; } @@ -293,15 +307,16 @@ export class PublicClientApplication implements IPublicClientApplication { * @returns {Promise.} - a promise that is fulfilled when this function has completed, or rejected if an error was raised. Returns the {@link AuthResponse} object */ async acquireTokenPopup(request: PopupRequest): Promise { + // Preflight request + const validRequest: AuthorizationUrlRequest = this.preflightInteractiveRequest(request, InteractionType.POPUP); + const serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.acquireTokenPopup, validRequest.correlationId); + try { - // Preflight request - const validRequest: AuthorizationUrlRequest = this.preflightInteractiveRequest(request, InteractionType.POPUP); - // Create auth code request and generate PKCE params const authCodeRequest: AuthorizationCodeRequest = await this.initializeAuthorizationCodeRequest(validRequest); // Initialize the client - const authClient: AuthorizationCodeClient = await this.createAuthCodeClient(validRequest.authority); + const authClient: AuthorizationCodeClient = await this.createAuthCodeClient(serverTelemetryManager, validRequest.authority); // Create acquire token url. const navigateUrl = await authClient.getAuthCodeUrl(validRequest); @@ -309,6 +324,7 @@ export class PublicClientApplication implements IPublicClientApplication { // Acquire token with popup return await this.popupTokenHelper(navigateUrl, authCodeRequest, authClient); } catch (e) { + serverTelemetryManager.cacheFailedRequest(e); this.browserStorage.cleanRequest(); throw e; } @@ -368,19 +384,27 @@ export class PublicClientApplication implements IPublicClientApplication { prompt: PromptValue.NONE }, InteractionType.SILENT); - // Create auth code request and generate PKCE params - const authCodeRequest: AuthorizationCodeRequest = await this.initializeAuthorizationCodeRequest(silentRequest); + const serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.ssoSilent, silentRequest.correlationId); + + try { + // Create auth code request and generate PKCE params + const authCodeRequest: AuthorizationCodeRequest = await this.initializeAuthorizationCodeRequest(silentRequest); - // Get scopeString for iframe ID - const scopeString = silentRequest.scopes ? silentRequest.scopes.join(" ") : ""; + // Get scopeString for iframe ID + const scopeString = silentRequest.scopes ? silentRequest.scopes.join(" ") : ""; - // Initialize the client - const authClient: AuthorizationCodeClient = await this.createAuthCodeClient(silentRequest.authority); + // Initialize the client + const authClient: AuthorizationCodeClient = await this.createAuthCodeClient(serverTelemetryManager, silentRequest.authority); - // Create authorize request url - const navigateUrl = await authClient.getAuthCodeUrl(silentRequest); + // Create authorize request url + const navigateUrl = await authClient.getAuthCodeUrl(silentRequest); - return this.silentTokenHelper(navigateUrl, authCodeRequest, authClient, scopeString); + return this.silentTokenHelper(navigateUrl, authCodeRequest, authClient, scopeString); + } catch (e) { + serverTelemetryManager.cacheFailedRequest(e); + this.browserStorage.cleanRequest(); + throw e; + } } /** @@ -402,12 +426,13 @@ export class PublicClientApplication implements IPublicClientApplication { ...request, ...this.initializeBaseRequest(request) }; - + let serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.acquireTokenSilent_silentFlow, silentRequest.correlationId); try { - const silentAuthClient = await this.createSilentFlowClient(silentRequest.authority); + const silentAuthClient = await this.createSilentFlowClient(serverTelemetryManager, silentRequest.authority); // Send request to renew token. Auth module will throw errors if token cannot be renewed. return await silentAuthClient.acquireToken(silentRequest); } catch (e) { + serverTelemetryManager.cacheFailedRequest(e); const isServerError = e instanceof ServerError; const isInteractionRequiredError = e instanceof InteractionRequiredAuthError; const isInvalidGrantError = (e.errorCode === BrowserConstants.INVALID_GRANT_ERROR); @@ -417,20 +442,27 @@ export class PublicClientApplication implements IPublicClientApplication { redirectUri: request.redirectUri, prompt: PromptValue.NONE }, InteractionType.SILENT); + serverTelemetryManager = this.initializeServerTelemetryManager(ApiId.acquireTokenSilent_authCode, silentAuthUrlRequest.correlationId); - // Create auth code request and generate PKCE params - const authCodeRequest: AuthorizationCodeRequest = await this.initializeAuthorizationCodeRequest(silentAuthUrlRequest); + try { + // Create auth code request and generate PKCE params + const authCodeRequest: AuthorizationCodeRequest = await this.initializeAuthorizationCodeRequest(silentAuthUrlRequest); - // Initialize the client - const authClient: AuthorizationCodeClient = await this.createAuthCodeClient(silentAuthUrlRequest.authority); + // Initialize the client + const authClient: AuthorizationCodeClient = await this.createAuthCodeClient(serverTelemetryManager, silentAuthUrlRequest.authority); - // Create authorize request url - const navigateUrl = await authClient.getAuthCodeUrl(silentAuthUrlRequest); + // Create authorize request url + const navigateUrl = await authClient.getAuthCodeUrl(silentAuthUrlRequest); - // Get scopeString for iframe ID - const scopeString = silentAuthUrlRequest.scopes ? silentAuthUrlRequest.scopes.join(" ") : ""; + // Get scopeString for iframe ID + const scopeString = silentAuthUrlRequest.scopes ? silentAuthUrlRequest.scopes.join(" ") : ""; - return this.silentTokenHelper(navigateUrl, authCodeRequest, authClient, scopeString); + return this.silentTokenHelper(navigateUrl, authCodeRequest, authClient, scopeString); + } catch (e) { + serverTelemetryManager.cacheFailedRequest(e); + this.browserStorage.cleanRequest(); + throw e; + } } throw e; @@ -444,19 +476,14 @@ export class PublicClientApplication implements IPublicClientApplication { * @param userRequestScopes */ private async silentTokenHelper(navigateUrl: string, authCodeRequest: AuthorizationCodeRequest, authClient: AuthorizationCodeClient, userRequestScopes: string): Promise { - try { - // Create silent handler - const silentHandler = new SilentHandler(authClient, this.browserStorage, this.config.system.loadFrameTimeout); - // Get the frame handle for the silent request - const msalFrame = await silentHandler.initiateAuthRequest(navigateUrl, authCodeRequest, userRequestScopes); - // Monitor the window for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds. - const hash = await silentHandler.monitorIframeForHash(msalFrame, this.config.system.iframeHashTimeout); - // Handle response from hash string. - return await silentHandler.handleCodeResponse(hash); - } catch (e) { - this.browserStorage.cleanRequest(); - throw e; - } + // Create silent handler + const silentHandler = new SilentHandler(authClient, this.browserStorage, this.config.system.loadFrameTimeout); + // Get the frame handle for the silent request + const msalFrame = await silentHandler.initiateAuthRequest(navigateUrl, authCodeRequest, userRequestScopes); + // Monitor the window for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds. + const hash = await silentHandler.monitorIframeForHash(msalFrame, this.config.system.iframeHashTimeout); + // Handle response from hash string. + return await silentHandler.handleCodeResponse(hash); } // #endregion @@ -470,7 +497,7 @@ export class PublicClientApplication implements IPublicClientApplication { */ async logout(logoutRequest?: EndSessionRequest): Promise { const validLogoutRequest = this.initializeLogoutRequest(logoutRequest); - const authClient = await this.createAuthCodeClient(validLogoutRequest && validLogoutRequest.authority); + const authClient = await this.createAuthCodeClient(null, validLogoutRequest && validLogoutRequest.authority); // create logout string and navigate user window to logout. Auth module will clear cache. const logoutUri: string = authClient.getLogoutUri(validLogoutRequest); BrowserUtils.navigateWindow(logoutUri); @@ -546,9 +573,9 @@ export class PublicClientApplication implements IPublicClientApplication { * Creates an Authorization Code Client with the given authority, or the default authority. * @param authorityUrl */ - private async createAuthCodeClient(authorityUrl?: string): Promise { + private async createAuthCodeClient(serverTelemetryManager: ServerTelemetryManager, authorityUrl?: string): Promise { // Create auth module. - const clientConfig = await this.getClientConfiguration(authorityUrl); + const clientConfig = await this.getClientConfiguration(serverTelemetryManager, authorityUrl); return new AuthorizationCodeClient(clientConfig); } @@ -556,9 +583,9 @@ export class PublicClientApplication implements IPublicClientApplication { * Creates an Silent Flow Client with the given authority, or the default authority. * @param authorityUrl */ - private async createSilentFlowClient(authorityUrl?: string): Promise { + private async createSilentFlowClient(serverTelemetryManager: ServerTelemetryManager, authorityUrl?: string): Promise { // Create auth module. - const clientConfig = await this.getClientConfiguration(authorityUrl); + const clientConfig = await this.getClientConfiguration(serverTelemetryManager, authorityUrl); return new SilentFlowClient(clientConfig); } @@ -566,7 +593,7 @@ export class PublicClientApplication implements IPublicClientApplication { * Creates a Client Configuration object with the given request authority, or the default authority. * @param requestAuthority */ - private async getClientConfiguration(requestAuthority?: string): Promise { + private async getClientConfiguration(serverTelemetryManager: ServerTelemetryManager, requestAuthority?: string): Promise { // If the requestAuthority is passed and is not equivalent to the default configured authority, create new authority and discover endpoints. Return default authority otherwise. const discoveredAuthority = (!StringUtils.isEmpty(requestAuthority) && requestAuthority !== this.config.auth.authority) ? await AuthorityFactory.createDiscoveredInstance(requestAuthority, this.config.system.networkClient) : await this.getDiscoveredDefaultAuthority(); @@ -587,6 +614,7 @@ export class PublicClientApplication implements IPublicClientApplication { cryptoInterface: this.browserCrypto, networkInterface: this.networkClient, storageInterface: this.browserStorage, + serverTelemetryManager: serverTelemetryManager, libraryInfo: { sku: BrowserConstants.MSAL_SKU, version: version, @@ -629,6 +657,17 @@ export class PublicClientApplication implements IPublicClientApplication { return validatedRequest; } + private initializeServerTelemetryManager(apiId: number, correlationId: string, forceRefresh?: boolean): ServerTelemetryManager { + const telemetryPayload: ServerTelemetryRequest = { + clientId: this.config.auth.clientId, + correlationId: correlationId, + apiId: apiId, + forceRefresh: forceRefresh || false + }; + + return new ServerTelemetryManager(telemetryPayload, this.browserStorage); + } + /** * Generates a request that will contain the openid and profile scopes. * @param request diff --git a/lib/msal-browser/src/cache/BrowserStorage.ts b/lib/msal-browser/src/cache/BrowserStorage.ts index 0b1bed3900..06d65039ea 100644 --- a/lib/msal-browser/src/cache/BrowserStorage.ts +++ b/lib/msal-browser/src/cache/BrowserStorage.ts @@ -2,7 +2,7 @@ * Copyright (c) Microsoft Corporation. All rights reserved. * Licensed under the MIT License. */ -import { Constants, PersistentCacheKeys, StringUtils, AuthorizationCodeRequest, ICrypto, CacheSchemaType, AccountEntity, IdTokenEntity, CredentialType, AccessTokenEntity, RefreshTokenEntity, AppMetadataEntity, CacheManager, CredentialEntity } from "@azure/msal-common"; +import { Constants, PersistentCacheKeys, StringUtils, AuthorizationCodeRequest, ICrypto, CacheSchemaType, AccountEntity, IdTokenEntity, CredentialType, AccessTokenEntity, RefreshTokenEntity, AppMetadataEntity, CacheManager, CredentialEntity, ServerTelemetryCacheValue } from "@azure/msal-common"; import { CacheOptions } from "../config/Configuration"; import { BrowserAuthError } from "../error/BrowserAuthError"; import { BrowserConfigurationAuthError } from "../error/BrowserConfigurationAuthError"; @@ -124,6 +124,10 @@ export class BrowserStorage extends CacheManager { } break; } + case CacheSchemaType.TELEMETRY: { + this.windowStorage.setItem(key, JSON.stringify(value)); + break; + } default: { throw BrowserAuthError.createInvalidCacheTypeError(); } @@ -172,6 +176,9 @@ export class BrowserStorage extends CacheManager { } return value; } + case CacheSchemaType.TELEMETRY: { + return JSON.parse(value) as ServerTelemetryCacheValue; + } default: { throw BrowserAuthError.createInvalidCacheTypeError(); } diff --git a/lib/msal-browser/src/utils/BrowserConstants.ts b/lib/msal-browser/src/utils/BrowserConstants.ts index 1dcb426187..54d78160b3 100644 --- a/lib/msal-browser/src/utils/BrowserConstants.ts +++ b/lib/msal-browser/src/utils/BrowserConstants.ts @@ -54,6 +54,21 @@ export enum TemporaryCacheKeys { } /** + * 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 enum ApiId { + acquireTokenRedirect = 861, + acquireTokenPopup = 862, + ssoSilent = 863, + acquireTokenSilent_authCode = 864, + handleRedirectPromise = 865, + acquireTokenSilent_silentFlow = 61 +} + +/* * Interaction type of the API - used for state and telemetry */ export enum InteractionType { diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index d6287cbdb6..bf548dbd92 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -7,9 +7,9 @@ const expect = chai.expect; import sinon from "sinon"; import { PublicClientApplication } from "../../src/app/PublicClientApplication"; import { TEST_CONFIG, TEST_URIS, TEST_HASHES, TEST_TOKENS, TEST_DATA_CLIENT_INFO, TEST_TOKEN_LIFETIMES, RANDOM_TEST_GUID, DEFAULT_OPENID_CONFIG_RESPONSE, testNavUrl, testLogoutUrl, TEST_STATE_VALUES, testNavUrlNoRequest } from "../utils/StringConstants"; -import { ServerError, Constants, AccountInfo, IdTokenClaims, PromptValue, AuthenticationResult, AuthorizationCodeRequest, AuthorizationUrlRequest, IdToken, PersistentCacheKeys, SilentFlowRequest, CacheSchemaType, TimeUtils, AuthorizationCodeClient, ResponseMode, SilentFlowClient, TrustedAuthority, EndSessionRequest, CloudDiscoveryMetadata, AccountEntity, ProtocolUtils } from "@azure/msal-common"; +import { ServerError, Constants, AccountInfo, IdTokenClaims, PromptValue, AuthenticationResult, AuthorizationCodeRequest, AuthorizationUrlRequest, IdToken, PersistentCacheKeys, SilentFlowRequest, CacheSchemaType, TimeUtils, AuthorizationCodeClient, ResponseMode, SilentFlowClient, TrustedAuthority, EndSessionRequest, CloudDiscoveryMetadata, AccountEntity, ProtocolUtils, ServerTelemetryCacheValue } from "@azure/msal-common"; import { BrowserUtils } from "../../src/utils/BrowserUtils"; -import { BrowserConstants, TemporaryCacheKeys } from "../../src/utils/BrowserConstants"; +import { BrowserConstants, TemporaryCacheKeys, ApiId } from "../../src/utils/BrowserConstants"; import { Base64Encode } from "../../src/encode/Base64Encode"; import { XhrClient } from "../../src/network/XhrClient"; import { BrowserAuthErrorMessage, BrowserAuthError } from "../../src/error/BrowserAuthError"; @@ -567,11 +567,24 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { challenge: TEST_CONFIG.TEST_CHALLENGE, verifier: TEST_CONFIG.TEST_VERIFIER }); - const loginUrlErr = "loginUrlErr"; - sinon.stub(AuthorizationCodeClient.prototype, "getAuthCodeUrl").throws(new BrowserAuthError(loginUrlErr)); - await expect(pca.loginRedirect(emptyRequest)).to.be.rejectedWith(loginUrlErr); - await expect(pca.loginRedirect(emptyRequest)).to.be.rejectedWith(BrowserAuthError); - expect(browserStorage.getKeys()).to.be.empty; + + const testError = { + errorCode: "create_login_url_error", + errorMessage: "Error in creating a login url" + } + sinon.stub(AuthorizationCodeClient.prototype, "getAuthCodeUrl").throws(testError); + try { + await pca.loginRedirect(emptyRequest); + } catch (e) { + // Test that error was cached for telemetry purposes and then thrown + expect(window.sessionStorage).to.be.length(1); + const failures = window.sessionStorage.getItem(`server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}`); + const failureObj = JSON.parse(failures) as ServerTelemetryCacheValue; + expect(failureObj.failedRequests).to.be.length(2); + expect(failureObj.failedRequests[0]).to.eq(ApiId.acquireTokenRedirect); + expect(failureObj.errors[0]).to.eq(testError.errorCode); + expect(e).to.be.eq(testError); + } }); it("Uses adal token from cache if it is present.", async () => { @@ -758,12 +771,25 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { sinon.stub(CryptoOps.prototype, "generatePkceCodes").resolves({ challenge: TEST_CONFIG.TEST_CHALLENGE, verifier: TEST_CONFIG.TEST_VERIFIER - }); - const loginUrlErr = "loginUrlErr"; - sinon.stub(AuthorizationCodeClient.prototype, "getAuthCodeUrl").throws(new BrowserAuthError(loginUrlErr)); - await expect(pca.acquireTokenRedirect(emptyRequest)).to.be.rejectedWith(loginUrlErr); - await expect(pca.acquireTokenRedirect(emptyRequest)).to.be.rejectedWith(BrowserAuthError); - expect(browserStorage.getKeys()).to.be.empty; + }); + + const testError = { + errorCode: "create_login_url_error", + errorMessage: "Error in creating a login url" + } + sinon.stub(AuthorizationCodeClient.prototype, "getAuthCodeUrl").throws(testError); + try { + await pca.acquireTokenRedirect(emptyRequest); + } catch (e) { + // Test that error was cached for telemetry purposes and then thrown + expect(window.sessionStorage).to.be.length(1); + const failures = window.sessionStorage.getItem(`server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}`); + const failureObj = JSON.parse(failures) as ServerTelemetryCacheValue; + expect(failureObj.failedRequests).to.be.length(2); + expect(failureObj.failedRequests[0]).to.eq(ApiId.acquireTokenRedirect); + expect(failureObj.errors[0]).to.eq(testError.errorCode); + expect(e).to.be.eq(testError); + } }); it("Uses adal token from cache if it is present.", async () => { @@ -939,7 +965,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("catches error and cleans cache before rethrowing", async () => { - const testError = "Error in creating a login url"; + const testError = { + errorCode: "create_login_url_error", + errorMessage: "Error in creating a login url" + } sinon.stub(AuthorizationCodeClient.prototype, "getAuthCodeUrl").resolves(testNavUrl); sinon.stub(PopupHandler.prototype, "initiateAuthRequest").throws(testError); sinon.stub(CryptoOps.prototype, "generatePkceCodes").resolves({ @@ -950,8 +979,14 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { try { const tokenResp = await pca.loginPopup(null); } catch (e) { - expect(window.sessionStorage).to.be.empty; - expect(`${e}`).to.be.eq(testError); + // Test that error was cached for telemetry purposes and then thrown + expect(window.sessionStorage).to.be.length(1); + const failures = window.sessionStorage.getItem(`server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}`); + const failureObj = JSON.parse(failures) as ServerTelemetryCacheValue; + expect(failureObj.failedRequests).to.be.length(2); + expect(failureObj.failedRequests[0]).to.eq(ApiId.acquireTokenPopup); + expect(failureObj.errors[0]).to.eq(testError.errorCode); + expect(e).to.be.eq(testError); } }); }); @@ -1027,7 +1062,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("catches error and cleans cache before rethrowing", async () => { - const testError = "Error in creating a login url"; + const testError = { + errorCode: "create_login_url_error", + errorMessage: "Error in creating a login url" + } sinon.stub(AuthorizationCodeClient.prototype, "getAuthCodeUrl").resolves(testNavUrl); sinon.stub(PopupHandler.prototype, "initiateAuthRequest").throws(testError); sinon.stub(CryptoOps.prototype, "generatePkceCodes").resolves({ @@ -1041,8 +1079,14 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { scopes: TEST_CONFIG.DEFAULT_SCOPES }); } catch (e) { - expect(window.sessionStorage).to.be.empty; - expect(`${e}`).to.be.eq(testError); + // Test that error was cached for telemetry purposes and then thrown + expect(window.sessionStorage).to.be.length(1); + const failures = window.sessionStorage.getItem(`server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}`); + const failureObj = JSON.parse(failures) as ServerTelemetryCacheValue; + expect(failureObj.failedRequests).to.be.length(2); + expect(failureObj.failedRequests[0]).to.eq(ApiId.acquireTokenPopup); + expect(failureObj.errors[0]).to.eq(testError.errorCode); + expect(e).to.be.eq(testError); } }); }); @@ -1243,7 +1287,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); it("throws error that SilentFlowClient.acquireToken() throws", async () => { - const testError = "Error in creating a login url"; + const testError = { + errorCode: "create_login_url_error", + errorMessage: "Error in creating a login url" + } const testAccount: AccountInfo = { homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, environment: "login.windows.net", @@ -1257,8 +1304,14 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { account: testAccount }); } catch (e) { - expect(`${e}`).to.contain(testError); - expect(window.sessionStorage).to.be.empty; + // Test that error was cached for telemetry purposes and then thrown + expect(window.sessionStorage).to.be.length(1); + const failures = window.sessionStorage.getItem(`server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}`); + const failureObj = JSON.parse(failures) as ServerTelemetryCacheValue; + expect(failureObj.failedRequests).to.be.length(2); + expect(failureObj.failedRequests[0]).to.eq(ApiId.acquireTokenSilent_silentFlow); + expect(failureObj.errors[0]).to.eq(testError.errorCode); + expect(e).to.be.eq(testError); } }); diff --git a/lib/msal-common/src/index.ts b/lib/msal-common/src/index.ts index 375fce6e93..f2578cb2dc 100644 --- a/lib/msal-common/src/index.ts +++ b/lib/msal-common/src/index.ts @@ -57,5 +57,6 @@ export { StringDict } from "./utils/MsalTypes"; export { ProtocolUtils, RequestStateObject, LibraryStateObject } from "./utils/ProtocolUtils"; export { TimeUtils } from "./utils/TimeUtils"; // Telemetry +export { ServerTelemetryCacheValue } from "./telemetry/server/ServerTelemetryCacheValue"; export { ServerTelemetryManager } from "./telemetry/server/ServerTelemetryManager"; export { ServerTelemetryRequest } from "./telemetry/server/ServerTelemetryRequest";