Skip to content

Telemetry #2: Implement Telemetry in msal-browser #1918

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 32 commits into from
Aug 4, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
1b49c36
Implement Telemetry in browser
tnorling Jul 9, 2020
dc6518a
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 9, 2020
0838065
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 9, 2020
f53718a
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 9, 2020
e7c25e4
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 9, 2020
54dde19
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 10, 2020
666fa9c
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 13, 2020
b6598c6
Always generateCacheKey
tnorling Jul 13, 2020
455f3e5
Add comment to API codes
tnorling Jul 13, 2020
24846c7
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 14, 2020
f73ed7d
Tests passing
tnorling Jul 14, 2020
43e4039
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 17, 2020
da8b6c1
Update name to ServerTelemetryManager
tnorling Jul 17, 2020
8723988
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 21, 2020
a20f4b9
Update BrowserStorage to return object
tnorling Jul 22, 2020
4d6d285
Fix tests
tnorling Jul 22, 2020
9954710
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 22, 2020
eed0241
Pass telemetryManager to authCodeClient
tnorling Jul 23, 2020
4558bcf
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 25, 2020
3ff3b0c
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 29, 2020
c1e755d
Get correlationId from cached request
tnorling Jul 30, 2020
fb90da9
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Jul 31, 2020
f80adbc
Merge branch 'msal-common-MSER-Telemetry' of https://github.com/Azure…
tnorling Aug 3, 2020
76c3b73
Remove leftover references to correlationId cache entry
tnorling Aug 3, 2020
84a83d7
Merge branch 'dev' into msal-browser-MSER-Telemetry
tnorling Aug 3, 2020
83b7c1e
Merge branch 'dev' of https://github.com/AzureAD/microsoft-authentica…
tnorling Aug 3, 2020
9a4e4f1
Update cachedRequest lookup
tnorling Aug 4, 2020
ca14879
Merge branch 'msal-browser-MSER-Telemetry' of https://github.com/Azur…
tnorling Aug 4, 2020
0e8f72b
Merge branch 'dev' of https://github.com/AzureAD/microsoft-authentica…
tnorling Aug 4, 2020
2297fd7
Merge branch 'dev' into msal-browser-MSER-Telemetry
tnorling Aug 4, 2020
4ab428c
Merge branch 'dev' into msal-browser-MSER-Telemetry
tnorling Aug 4, 2020
0cfa1bb
Merge branch 'dev' into msal-browser-MSER-Telemetry
tnorling Aug 4, 2020
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
145 changes: 92 additions & 53 deletions lib/msal-browser/src/app/PublicClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ import {
EndSessionRequest,
BaseAuthRequest,
Logger,
ServerTelemetryManager,
ServerTelemetryRequest,
ServerAuthorizationCodeResponse
} from "@azure/msal-common";
import { buildConfiguration, Configuration } from "../config/Configuration";
Expand All @@ -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";
Expand Down Expand Up @@ -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;
}
}

/**
Expand All @@ -246,15 +258,16 @@ export class PublicClientApplication implements IPublicClientApplication {
* @param {@link (RedirectRequest:type)}
*/
async acquireTokenRedirect(request: RedirectRequest): Promise<void> {
// 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);
Expand All @@ -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;
}
Expand Down Expand Up @@ -293,22 +307,24 @@ export class PublicClientApplication implements IPublicClientApplication {
* @returns {Promise.<AuthenticationResult>} - 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<AuthenticationResult> {
// 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);

// Acquire token with popup
return await this.popupTokenHelper(navigateUrl, authCodeRequest, authClient);
} catch (e) {
serverTelemetryManager.cacheFailedRequest(e);
this.browserStorage.cleanRequest();
throw e;
}
Expand Down Expand Up @@ -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;
}
}

/**
Expand All @@ -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);
Expand All @@ -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;
Expand All @@ -444,19 +476,14 @@ export class PublicClientApplication implements IPublicClientApplication {
* @param userRequestScopes
*/
private async silentTokenHelper(navigateUrl: string, authCodeRequest: AuthorizationCodeRequest, authClient: AuthorizationCodeClient, userRequestScopes: string): Promise<AuthenticationResult> {
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
Expand All @@ -470,7 +497,7 @@ export class PublicClientApplication implements IPublicClientApplication {
*/
async logout(logoutRequest?: EndSessionRequest): Promise<void> {
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);
Expand Down Expand Up @@ -546,27 +573,27 @@ 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<AuthorizationCodeClient> {
private async createAuthCodeClient(serverTelemetryManager: ServerTelemetryManager, authorityUrl?: string): Promise<AuthorizationCodeClient> {
// Create auth module.
const clientConfig = await this.getClientConfiguration(authorityUrl);
const clientConfig = await this.getClientConfiguration(serverTelemetryManager, authorityUrl);
return new AuthorizationCodeClient(clientConfig);
}

/**
* Creates an Silent Flow Client with the given authority, or the default authority.
* @param authorityUrl
*/
private async createSilentFlowClient(authorityUrl?: string): Promise<SilentFlowClient> {
private async createSilentFlowClient(serverTelemetryManager: ServerTelemetryManager, authorityUrl?: string): Promise<SilentFlowClient> {
// Create auth module.
const clientConfig = await this.getClientConfiguration(authorityUrl);
const clientConfig = await this.getClientConfiguration(serverTelemetryManager, authorityUrl);
return new SilentFlowClient(clientConfig);
}

/**
* Creates a Client Configuration object with the given request authority, or the default authority.
* @param requestAuthority
*/
private async getClientConfiguration(requestAuthority?: string): Promise<ClientConfiguration> {
private async getClientConfiguration(serverTelemetryManager: ServerTelemetryManager, requestAuthority?: string): Promise<ClientConfiguration> {
// 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();
Expand All @@ -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,
Expand Down Expand Up @@ -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
Expand Down
9 changes: 8 additions & 1 deletion lib/msal-browser/src/cache/BrowserStorage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Constants, PersistentCacheKeys, StringUtils, AuthorizationCodeRequest, ICrypto, CacheSchemaType, AccountEntity, IdTokenEntity, CredentialType, AccessTokenEntity, RefreshTokenEntity, AppMetadataEntity, CacheManager, CredentialEntity } 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";
Expand Down Expand Up @@ -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();
}
Expand Down Expand Up @@ -172,6 +176,9 @@ export class BrowserStorage extends CacheManager {
}
return value;
}
case CacheSchemaType.TELEMETRY: {
return JSON.parse(value) as ServerTelemetryCacheValue;
}
default: {
throw BrowserAuthError.createInvalidCacheTypeError();
}
Expand Down
15 changes: 15 additions & 0 deletions lib/msal-browser/src/utils/BrowserConstants.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Loading