Skip to content
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

Authorization Code Flow for Single Page Applications: Silent Flow and Logout #1208

Merged
53 changes: 10 additions & 43 deletions lib/msal-browser/src/app/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
*/

// import { Logger } from "./Logger";
import { AuthOptions, INetworkModule, ILoggerCallback, LogLevel } from "msal-common";
import { AuthOptions, SystemOptions, LoggerOptions, INetworkModule, LogLevel } from "msal-common";
import { BrowserUtils } from "../utils/BrowserUtils";
import { BrowserConstants } from "../utils/BrowserConstants";
// import { TelemetryEmitter } from "./telemetry/TelemetryTypes";
Expand All @@ -13,8 +13,6 @@ import { BrowserConstants } from "../utils/BrowserConstants";
* Defaults for the Configuration Options
*/
const FRAME_TIMEOUT = 6000;
const OFFSET = 300;
const NAVIGATE_FRAME_WAIT = 500;

export type BrowserAuthOptions = AuthOptions & {
navigateToLoginRequestUrl?: boolean;
Expand All @@ -31,18 +29,6 @@ export type CacheOptions = {
storeAuthStateInCookie?: boolean;
};

/**
* Telemetry Config Options
* - applicationName - Name of the consuming apps application
* - applicationVersion - Verison of the consuming application
* - telemetryEmitter - Function where telemetry events are flushed to
*/
export type TelemetryOptions = {
applicationName: string;
applicationVersion: string;
// TODO, add onlyAddFailureTelemetry option
};

/**
* Library Specific Options
*
Expand All @@ -51,44 +37,28 @@ export type TelemetryOptions = {
* - tokenRenewalOffsetSeconds - sets the window of offset needed to renew the token before expiry
* - navigateFrameWait - sets the wait time for hidden iFrame navigation
*/
export type SystemOptions = {
loggerOptions?: BrowserLoggerOptions;
export type BrowserSystemOptions = SystemOptions & {
loggerOptions?: LoggerOptions;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nitpick: Overwrites a generic loggerOptions object? If it is browser specific, the name is very generic. I will check the code as a whole later, this is for traction.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Logger isn't browser specific, only the implementation is, so I reverted back to generic logger options.

networkClient?: INetworkModule;
loadFrameTimeout?: number;
tokenRenewalOffsetSeconds?: number;
navigateFrameWait?: number;
telemetry?: TelemetryOptions
};

/**
* Logger options
*
* - piiLoggingEnabled - Used to configure whether piiLogging is enabled. Defaults to false.
* - loggerCallback - Callback for logger that determines how message is broadcast. Defaults to console.
*/
export type BrowserLoggerOptions = {
piiLoggingEnabled?: boolean,
loggerCallback?: ILoggerCallback
popupWindowTimeout?: number;
jasonnutter marked this conversation as resolved.
Show resolved Hide resolved
};

/**
* Use the configuration object to configure MSAL and initialize the UserAgentApplication.
*
* This object allows you to configure important elements of MSAL functionality:
* - auth: this is where you configure auth elements like clientID, authority used for authenticating against the Microsoft Identity Platform
* - auth: this is where you configure auth elements like clientID, authority used for authenticating against the Microsoft Identity Platform
* - cache: this is where you configure cache location and whether to store cache in cookies
* - system: this is where you can configure the logger, frame timeout etc.
* - framework: this is where you can configure the running mode of angular. More to come here soon.
* - system: this is where you can configure the network client, logger, token renewal offset, and telemetry
jasonnutter marked this conversation as resolved.
Show resolved Hide resolved
*/
export type Configuration = {
auth?: BrowserAuthOptions,
cache?: CacheOptions,
system?: SystemOptions
system?: BrowserSystemOptions
};

const DEFAULT_AUTH_OPTIONS: BrowserAuthOptions = {
clientId: "",
clientSecret: "",
authority: null,
validateAuthority: true,
redirectUri: () => BrowserUtils.getDefaultRedirectUri(),
Expand All @@ -101,7 +71,7 @@ const DEFAULT_CACHE_OPTIONS: CacheOptions = {
storeAuthStateInCookie: false
};

const DEFAULT_LOGGER_OPTIONS: BrowserLoggerOptions = {
const DEFAULT_LOGGER_OPTIONS: LoggerOptions = {
loggerCallback: (level: LogLevel, message: string, containsPii: boolean): void => {
if (containsPii) {
return;
Expand All @@ -124,13 +94,10 @@ const DEFAULT_LOGGER_OPTIONS: BrowserLoggerOptions = {
piiLoggingEnabled: false
};

const DEFAULT_SYSTEM_OPTIONS: SystemOptions = {
const DEFAULT_SYSTEM_OPTIONS: BrowserSystemOptions = {
loggerOptions: DEFAULT_LOGGER_OPTIONS,
networkClient: BrowserUtils.getBrowserNetworkClient(),
loadFrameTimeout: FRAME_TIMEOUT,
tokenRenewalOffsetSeconds: OFFSET,
navigateFrameWait: NAVIGATE_FRAME_WAIT,
telemetry: null
popupWindowTimeout: FRAME_TIMEOUT
};

/**
Expand Down
67 changes: 54 additions & 13 deletions lib/msal-browser/src/app/PublicClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,7 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/

import { AuthError, AuthResponse, AuthorizationCodeModule, AuthenticationParameters, INetworkModule, TokenResponse, UrlString, TemporaryCacheKeys } from "msal-common";
import { AuthError, Account, AuthResponse, AuthorizationCodeModule, AuthenticationParameters, INetworkModule, TokenResponse, UrlString, TemporaryCacheKeys, TokenRenewParameters } from "msal-common";
import { BrowserStorage } from "../cache/BrowserStorage";
import { Configuration, buildConfiguration } from "./Configuration";
import { CryptoOps } from "../crypto/CryptoOps";
Expand Down Expand Up @@ -86,8 +85,12 @@ export class PublicClientApplication {
// Create auth module
this.authModule = new AuthorizationCodeModule({
auth: this.config.auth,
systemOptions: {
tokenRenewalOffsetSeconds: this.config.system.tokenRenewalOffsetSeconds,
telemetry: this.config.system.telemetry
},
loggerOptions: {
loggerCallbackInterface: this.config.system.loggerOptions.loggerCallback,
loggerCallback: this.config.system.loggerOptions.loggerCallback,
piiLoggingEnabled: this.config.system.loggerOptions.piiLoggingEnabled
},
cryptoInterface: this.browserCrypto,
Expand All @@ -114,7 +117,7 @@ export class PublicClientApplication {
const { location: { hash } } = window;
const cachedHash = this.browserStorage.getItem(TemporaryCacheKeys.URL_HASH);
try {
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.authCallback, this.config.auth.navigateToLoginRequestUrl);
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.config.auth.navigateToLoginRequestUrl);
const responseHash = UrlString.hashContainsKnownProperties(hash) ? hash : cachedHash;
if (responseHash) {
this.authCallback(null, await interactionHandler.handleCodeResponse(responseHash));
Expand All @@ -130,10 +133,16 @@ export class PublicClientApplication {
* @param {@link (AuthenticationParameters:type)}
*/
loginRedirect(request: AuthenticationParameters): void {
if (!this.authCallback) {
throw BrowserConfigurationAuthError.createRedirectCallbacksNotSetError();
}

if (this.interactionInProgress()) {
throw BrowserAuthError.createInteractionInProgressError();
this.authCallback(BrowserAuthError.createInteractionInProgressError());
return;
}
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.authCallback, this.config.auth.navigateToLoginRequestUrl);

const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.config.auth.navigateToLoginRequestUrl);
this.authModule.createLoginUrl(request).then((navigateUrl) => {
interactionHandler.showUI(navigateUrl);
});
Expand All @@ -147,10 +156,16 @@ export class PublicClientApplication {
* To acquire only idToken, please pass clientId as the only scope in the Authentication Parameters
*/
acquireTokenRedirect(request: AuthenticationParameters): void {
if (!this.authCallback) {
throw BrowserConfigurationAuthError.createRedirectCallbacksNotSetError();
}

if (this.interactionInProgress()) {
throw BrowserAuthError.createInteractionInProgressError();
this.authCallback(BrowserAuthError.createInteractionInProgressError());
return;
}
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.authCallback, this.config.auth.navigateToLoginRequestUrl);

const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.config.auth.navigateToLoginRequestUrl);
this.authModule.createAcquireTokenUrl(request).then((navigateUrl) => {
interactionHandler.showUI(navigateUrl);
});
Expand All @@ -174,7 +189,7 @@ export class PublicClientApplication {
const interactionHandler = new PopupHandler(this.authModule, this.browserStorage);
const navigateUrl = await this.authModule.createLoginUrl(request);
const popupWindow = interactionHandler.showUI(navigateUrl);
const hash = await interactionHandler.monitorWindowForHash(popupWindow, this.config.system.loadFrameTimeout, navigateUrl);
const hash = await interactionHandler.monitorWindowForHash(popupWindow, this.config.system.popupWindowTimeout, navigateUrl);
return interactionHandler.handleCodeResponse(hash);
}

Expand All @@ -192,7 +207,7 @@ export class PublicClientApplication {
const interactionHandler = new PopupHandler(this.authModule, this.browserStorage);
const navigateUrl = await this.authModule.createAcquireTokenUrl(request);
const popupWindow = interactionHandler.showUI(navigateUrl);
const hash = await interactionHandler.monitorWindowForHash(popupWindow, this.config.system.loadFrameTimeout, navigateUrl);
const hash = await interactionHandler.monitorWindowForHash(popupWindow, this.config.system.popupWindowTimeout, navigateUrl);
return interactionHandler.handleCodeResponse(hash);
}

Expand All @@ -202,16 +217,32 @@ export class PublicClientApplication {
* Use this function to obtain a token before every call to the API / resource provider
*
* MSAL return's a cached token when available
* Or it send's a request to the STS to obtain a new token using a hidden iframe.
* Or it send's a request to the STS to obtain a new token using a refresh token.
*
* @param {@link AuthenticationParameters}
*
* To renew idToken, please pass clientId as the only scope in the Authentication Parameters
* @returns {Promise.<TokenResponse>} - a promise that is fulfilled when this function has completed, or rejected if an error was raised. Returns the {@link AuthResponse} object
*
*/
acquireTokenSilent(request: AuthenticationParameters): Promise<TokenResponse> {
throw new Error("Method not implemented.");
async acquireTokenSilent(tokenRequest: TokenRenewParameters): Promise<TokenResponse> {
return this.authModule.renewToken(tokenRequest);
}

// #endregion

// #region Logout

/**
* 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`.
*/
logout(): void {
this.authModule.logout().then(logoutUri => {
window.location.assign(logoutUri);
jasonnutter marked this conversation as resolved.
Show resolved Hide resolved
}).catch(e => {
throw e;
jasonnutter marked this conversation as resolved.
Show resolved Hide resolved
});
}

// #endregion
Expand Down Expand Up @@ -239,6 +270,16 @@ export class PublicClientApplication {
return this.authModule.getPostLogoutRedirectUri();
}

/**
* Returns the signed in account
* (the account object is created at the time of successful login)
* or null when no state is found
* @returns {@link Account} - the account object stored in MSAL
*/
public getAccount(): Account {
return this.authModule.getAccount();
}

// #endregion

// #region Helpers
Expand Down
5 changes: 2 additions & 3 deletions lib/msal-browser/src/interaction_handler/PopupHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,8 @@
*/
import { InteractionHandler } from "./InteractionHandler";
import { BrowserAuthError } from "../error/BrowserAuthError";
import { TemporaryCacheKeys, UrlString, StringUtils, Constants, AuthorizationCodeModule, TokenResponse } from "msal-common";
import { UrlString, StringUtils, Constants, TokenResponse } from "msal-common";
import { BrowserConstants } from "../utils/BrowserConstants";
import { BrowserStorage } from "../cache/BrowserStorage";

export class PopupHandler extends InteractionHandler {

Expand All @@ -33,7 +32,7 @@ export class PopupHandler extends InteractionHandler {
this.browserStorage.removeItem(BrowserConstants.INTERACTION_STATUS_KEY);
const codeResponse = this.authModule.handleFragmentResponse(locationHash);
this.currentWindow.close();
return this.authModule.acquireToken(null, codeResponse);
return this.authModule.acquireToken(codeResponse);
}

/**
Expand Down
9 changes: 2 additions & 7 deletions lib/msal-browser/src/interaction_handler/RedirectHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,15 +13,10 @@ import { BrowserUtils } from "../utils/BrowserUtils";

export class RedirectHandler extends InteractionHandler {

private authCallback: AuthCallback;
private navigateToLoginRequestUrl: boolean;

constructor(authCodeModule: AuthorizationCodeModule, storageImpl: BrowserStorage, redirectCallback: AuthCallback, navigateToLoginRequestUrl: boolean) {
constructor(authCodeModule: AuthorizationCodeModule, storageImpl: BrowserStorage, navigateToLoginRequestUrl: boolean) {
super(authCodeModule, storageImpl);
if (!redirectCallback) {
throw BrowserConfigurationAuthError.createRedirectCallbacksNotSetError();
}
this.authCallback = redirectCallback;
this.navigateToLoginRequestUrl = navigateToLoginRequestUrl;
}

Expand Down Expand Up @@ -72,6 +67,6 @@ export class RedirectHandler extends InteractionHandler {

this.browserStorage.removeItem(BrowserConstants.INTERACTION_STATUS_KEY);
const codeResponse = this.authModule.handleFragmentResponse(locationHash);
return this.authModule.acquireToken(null, codeResponse);
return this.authModule.acquireToken(codeResponse);
}
}
41 changes: 38 additions & 3 deletions lib/msal-common/src/app/config/ModuleConfiguration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,11 @@ import { ICrypto, PkceCodes } from "../../crypto/ICrypto";
import { AuthError } from "../../error/AuthError";
import { ILoggerCallback, LogLevel } from "../../logger/Logger";

/**
* Defaults for the Module Configuration Options
*/
const OFFSET = 300;
jasonnutter marked this conversation as resolved.
Show resolved Hide resolved

/**
* Use the configuration object to configure MSAL Modules and initialize the base interfaces for MSAL.
*
Expand All @@ -18,22 +23,51 @@ import { ILoggerCallback, LogLevel } from "../../logger/Logger";
* - crypto: implementation of crypto functions
*/
export type ModuleConfiguration = {
systemOptions?: SystemOptions,
loggerOptions?: LoggerOptions,
storageInterface?: ICacheStorage,
networkInterface?: INetworkModule,
cryptoInterface?: ICrypto
};

/**
* Telemetry Config Options
* - applicationName - Name of the consuming apps application
* - applicationVersion - Verison of the consuming application
* - telemetryEmitter - Function where telemetry events are flushed to
*/
export type TelemetryOptions = {
applicationName: string;
applicationVersion: string;
// TODO, add onlyAddFailureTelemetry option
};

/**
* Library Specific Options
*
* - tokenRenewalOffsetSeconds - sets the window of offset needed to renew the token before expiry
* - telemetry - Telemetry options for library network requests
*/
export type SystemOptions = {
tokenRenewalOffsetSeconds?: number;
telemetry?: TelemetryOptions
};

/**
* Logger options to configure the logging that MSAL does.
*/
export type LoggerOptions = {
loggerCallbackInterface?: ILoggerCallback,
loggerCallback?: ILoggerCallback,
piiLoggingEnabled?: boolean
};

const DEFAULT_SYSTEM_OPTIONS: SystemOptions = {
tokenRenewalOffsetSeconds: OFFSET,
telemetry: null
};

const DEFAULT_LOGGER_IMPLEMENTATION: LoggerOptions = {
loggerCallbackInterface: () => {
loggerCallback: () => {
const notImplErr = "Logger - loggerCallbackInterface() has not been implemented.";
throw AuthError.createUnexpectedError(notImplErr);
},
Expand Down Expand Up @@ -106,8 +140,9 @@ const DEFAULT_CRYPTO_IMPLEMENTATION: ICrypto = {
*
* @returns MsalConfiguration object
*/
export function buildModuleConfiguration({ loggerOptions: userLoggerOption, storageInterface: storageImplementation, networkInterface: networkImplementation, cryptoInterface: cryptoImplementation }: ModuleConfiguration): ModuleConfiguration {
export function buildModuleConfiguration({ systemOptions: userSystemOptions, loggerOptions: userLoggerOption, storageInterface: storageImplementation, networkInterface: networkImplementation, cryptoInterface: cryptoImplementation }: ModuleConfiguration): ModuleConfiguration {
const overlayedConfig: ModuleConfiguration = {
systemOptions: userSystemOptions || DEFAULT_SYSTEM_OPTIONS,
loggerOptions: userLoggerOption || DEFAULT_LOGGER_IMPLEMENTATION,
storageInterface: storageImplementation || DEFAULT_STORAGE_IMPLEMENTATION,
networkInterface: networkImplementation || DEFAULT_NETWORK_IMPLEMENTATION,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ import { ModuleConfiguration, buildModuleConfiguration } from "./ModuleConfigura
*/
export type AuthOptions = {
clientId: string;
clientSecret: string;
authority?: string;
validateAuthority?: boolean;
redirectUri?: string | (() => string);
Expand All @@ -36,7 +35,6 @@ export type PublicClientSPAConfiguration = ModuleConfiguration & {

const DEFAULT_AUTH_OPTIONS: AuthOptions = {
clientId: "",
clientSecret: "",
authority: null,
validateAuthority: true,
redirectUri: "",
Expand Down