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: Cleanup Popup Flows and Window Nav #1236

Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
77 changes: 53 additions & 24 deletions lib/msal-browser/src/app/PublicClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,9 @@ export class PublicClientApplication {

/**
* Set the callback functions for the redirect flow to send back the success or error object.
* IMPORTANT: Please do not use this function when using the popup APIs, as it will break the response handling
* in the main window.
*
* @param {@link (AuthCallback:type)} authCallback - Callback which contains
* an AuthError object, containing error data from either the server
* or the library, depending on the origin of the error, or the AuthResponse object
Expand All @@ -110,12 +113,16 @@ export class PublicClientApplication {
const { location: { hash } } = window;
const cachedHash = this.browserStorage.getItem(TemporaryCacheKeys.URL_HASH);
try {
// If hash exists, handle in window. Otherwise, continue execution.
// If hash exists, handle in window. Otherwise, cancel any current requests and continue.
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.config.auth.navigateToLoginRequestUrl);
const responseHash = UrlString.hashContainsKnownProperties(hash) ? hash : cachedHash;
if (responseHash) {
const tokenResponse = await interactionHandler.handleCodeResponse(responseHash);
this.authCallback(null, tokenResponse);
} else {
// Interaction is completed - remove interaction status.
this.browserStorage.removeItem(BrowserConstants.INTERACTION_STATUS_KEY);
this.authModule.cancelRequest();
}
} catch (err) {
this.authCallback(err);
Expand All @@ -139,14 +146,21 @@ export class PublicClientApplication {
return;
}

// Create redirect interaction handler.
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.config.auth.navigateToLoginRequestUrl);
try {
// Create redirect interaction handler.
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.config.auth.navigateToLoginRequestUrl);

// Create login url, which will by default append the client id scope to the call.
this.authModule.createLoginUrl(request).then((navigateUrl) => {
// 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.showUI(navigateUrl);
});
// Create login url, which will by default append the client id scope to the call.
this.authModule.createLoginUrl(request).then((navigateUrl) => {
// 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.showUI(navigateUrl);
});
} catch (e) {
// Interaction is completed - remove interaction status.
this.browserStorage.removeItem(BrowserConstants.INTERACTION_STATUS_KEY);
this.authModule.cancelRequest();
throw e;
}
}

/**
Expand All @@ -168,14 +182,22 @@ export class PublicClientApplication {
return;
}

// Create redirect interaction handler.
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.config.auth.navigateToLoginRequestUrl);
// Create acquire token url.
this.authModule.createAcquireTokenUrl(request).then((navigateUrl) => {
// 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.showUI(navigateUrl);
});
}
try {
// Create redirect interaction handler.
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage, this.config.auth.navigateToLoginRequestUrl);

// Create acquire token url.
this.authModule.createAcquireTokenUrl(request).then((navigateUrl) => {
// 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.showUI(navigateUrl);
});
} catch (e) {
// Interaction is completed - remove interaction status.
this.browserStorage.removeItem(BrowserConstants.INTERACTION_STATUS_KEY);
this.authModule.cancelRequest();
throw e;
}
}

// #endregion

Expand Down Expand Up @@ -226,14 +248,21 @@ export class PublicClientApplication {
* @param navigateUrl
*/
private async popupTokenHelper(navigateUrl: string): Promise<TokenResponse> {
// Create popup interaction handler.
const interactionHandler = new PopupHandler(this.authModule, this.browserStorage);
// Show the UI once the url has been created. Get the window handle for the popup.
const popupWindow = interactionHandler.showUI(navigateUrl);
// 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 interactionHandler.monitorWindowForHash(popupWindow, this.config.system.windowHashTimeout, navigateUrl);
// Handle response from hash string.
return interactionHandler.handleCodeResponse(hash);
try {
// Create popup interaction handler.
const interactionHandler = new PopupHandler(this.authModule, this.browserStorage);
// Show the UI once the url has been created. Get the window handle for the popup.
const popupWindow = interactionHandler.showUI(navigateUrl);
// 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 interactionHandler.monitorWindowForHash(popupWindow, this.config.system.windowHashTimeout, navigateUrl);
// Handle response from hash string.
return interactionHandler.handleCodeResponse(hash);
} catch (e) {
// Interaction is completed - remove interaction status.
this.browserStorage.removeItem(BrowserConstants.INTERACTION_STATUS_KEY);
this.authModule.cancelRequest();
throw e;
}
}

// #region Silent Flow
Expand Down
43 changes: 27 additions & 16 deletions lib/msal-browser/src/interaction_handler/PopupHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,6 @@ import { BrowserConstants } from "../utils/BrowserConstants";
*/
export class PopupHandler extends InteractionHandler {

// Currently opened window handle.
private currentWindow: Window;

/**
* Opens a popup window with given request Url.
* @param requestUrl
Expand All @@ -25,13 +22,9 @@ export class PopupHandler extends InteractionHandler {
if (!StringUtils.isEmpty(requestUrl)) {
// Set interaction status in the library.
this.browserStorage.setItem(BrowserConstants.INTERACTION_STATUS_KEY, BrowserConstants.INTERACTION_IN_PROGRESS_VALUE);
// Open the popup window to requestUrl.
const popupWindow = this.openPopup(requestUrl, Constants.LIBRARY_NAME, BrowserConstants.POPUP_WIDTH, BrowserConstants.POPUP_HEIGHT);
// Save the window handle.
this.currentWindow = popupWindow;
this.authModule.logger.infoPii("Navigate to:" + requestUrl);
// Return popup window handle.
return popupWindow;
// Open the popup window to requestUrl.
return this.openPopup(requestUrl, Constants.LIBRARY_NAME, BrowserConstants.POPUP_WIDTH, BrowserConstants.POPUP_HEIGHT);
} else {
// Throw error if request URL is empty.
this.authModule.logger.error("Navigate url is empty");
Expand All @@ -49,12 +42,9 @@ export class PopupHandler extends InteractionHandler {
throw BrowserAuthError.createEmptyHashError(locationHash);
}

// Interaction is completed - remove interaction status.
this.browserStorage.removeItem(BrowserConstants.INTERACTION_STATUS_KEY);
// Handle code response.
const codeResponse = this.authModule.handleFragmentResponse(locationHash);
// Close window.
this.currentWindow.close();

// Acquire token with retrieved code.
return this.authModule.acquireToken(codeResponse);
}
Expand All @@ -72,6 +62,7 @@ export class PopupHandler extends InteractionHandler {

const intervalId = setInterval(() => {
if (contentWindow.closed) {
this.cleanPopup();
clearInterval(intervalId);
reject(BrowserAuthError.createUserCancelledError());
return;
Expand All @@ -96,12 +87,16 @@ export class PopupHandler extends InteractionHandler {
ticks++;

if (UrlString.hashContainsKnownProperties(href)) {
const contentHash = contentWindow.location.hash;
this.cleanPopup(contentWindow);
clearInterval(intervalId);
resolve(contentWindow.location.hash);
resolve(contentHash);
return;
} else if (ticks > maxTicks) {
this.cleanPopup(contentWindow);
clearInterval(intervalId);
contentWindow.close();
reject(BrowserAuthError.createPopupWindowTimeoutError(urlNavigate)); // better error?
reject(BrowserAuthError.createPopupWindowTimeoutError(urlNavigate));
return;
}
}, BrowserConstants.POPUP_POLL_INTERVAL_MS);
});
Expand Down Expand Up @@ -144,6 +139,11 @@ export class PopupHandler extends InteractionHandler {
if (popupWindow.focus) {
popupWindow.focus();
}
window.onbeforeunload = (): void => {
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
this.authModule.cancelRequest();
this.browserStorage.removeItem(BrowserConstants.INTERACTION_STATUS_KEY);
popupWindow.close();
};

return popupWindow;
} catch (e) {
Expand All @@ -152,4 +152,15 @@ export class PopupHandler extends InteractionHandler {
throw BrowserAuthError.createPopupWindowError(e.toString());
}
}

private cleanPopup(popupWindow?: Window): void {
if (popupWindow) {
// Close window.
popupWindow.close();
}
// Remove window unload function
window.onbeforeunload = null;
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved
// Interaction is completed - remove interaction status.
this.browserStorage.removeItem(BrowserConstants.INTERACTION_STATUS_KEY);
}
}
8 changes: 8 additions & 0 deletions lib/msal-common/src/app/module/AuthorizationCodeModule.ts
Original file line number Diff line number Diff line change
Expand Up @@ -272,6 +272,14 @@ export class AuthorizationCodeModule extends AuthModule {
}
}

/**
* Clears cache of items related to current request.
*/
public cancelRequest(): void {
const cachedState = this.cacheStorage.getItem(TemporaryCacheKeys.REQUEST_STATE);
this.cacheManager.resetTempCacheItems(cachedState || "");
}

// #region Logout

/**
Expand Down
1 change: 1 addition & 0 deletions lib/msal-common/src/cache/CacheHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,7 @@ export class CacheHelpers {
});
// delete generic interactive request parameters
this.cacheStorage.removeItem(TemporaryCacheKeys.REQUEST_STATE);
this.cacheStorage.removeItem(TemporaryCacheKeys.REQUEST_PARAMS);
this.cacheStorage.removeItem(TemporaryCacheKeys.ORIGIN_URI);
this.cacheStorage.removeItem(TemporaryCacheKeys.REDIRECT_REQUEST);
}
Expand Down
10 changes: 7 additions & 3 deletions samples/VanillaJSTestApp/index_authCode.html
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@
};

const tokenRequest = {
scopes: ["Mail.Read"],
scopes: ["User.Read"],
forceRefresh: true
};

Expand All @@ -53,11 +53,15 @@
// instantiate MSAL
const myMSALObj = new msal.PublicClientApplication(msalConfig);
// register callback for redirect usecases
myMSALObj.handleRedirectCallback(authRedirectCallBack);
// myMSALObj.handleRedirectCallback(authRedirectCallBack);
// signin and acquire a token silently with POPUP flow. Fall back in case of failure with silent acquisition to popup
function signIn() {
try {
myMSALObj.loginRedirect(loginRequest);
myMSALObj.loginPopup(loginRequest).then(tokenResponse => {
console.log("Response: " + JSON.stringify(tokenResponse));
}).catch(error => {
console.error("Auth Error: " + error);
});
} catch (e) {
jmckennon marked this conversation as resolved.
Show resolved Hide resolved
console.log(e);
}
Expand Down