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

Adding Silent Iframe Flow in Authorization Code Flow #1451

Merged
merged 40 commits into from
Apr 23, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
40 commits
Select commit Hold shift + click to select a range
8c870f1
Updating package deps
pkanher617 Apr 1, 2020
fce9e67
Adding SilentHandler class
pkanher617 Apr 1, 2020
5da6c2f
Adding SSO Silent changes
pkanher617 Apr 1, 2020
fa271cf
Adding updates to make acquireTokenSilent and getTokens working, addi…
pkanher617 Apr 2, 2020
1a4a8db
Merge branch 'dev' into iframe-handler-2.0
pkanher617 Apr 2, 2020
de93afa
Update authConfig.js
pkanher617 Apr 3, 2020
50ff00e
Updating code
pkanher617 Apr 6, 2020
10a283d
Removing refresh token error handling
pkanher617 Apr 7, 2020
2bd91c7
Merge branch 'dev' into iframe-handler-2.0
pkanher617 Apr 7, 2020
c63b999
Adding browser detection for default value
pkanher617 Apr 7, 2020
ab34358
Fixing npm ci
pkanher617 Apr 7, 2020
7c7fe3d
attempting to fix unit tests
pkanher617 Apr 8, 2020
cd2a315
Merge branch 'dev' into iframe-handler-2.0
pkanher617 Apr 8, 2020
0c0aa91
reverting msal-angular changes
pkanher617 Apr 8, 2020
0a1ec5a
Adding new sample for ssoSilent
pkanher617 Apr 9, 2020
c137023
Merge branch 'dev' into iframe-handler-2.0
pkanher617 Apr 9, 2020
669f6ab
Adding test spec template for ssoSilent
pkanher617 Apr 9, 2020
ac622b1
Move hash processing to constructor for redirect cases
pkanher617 Apr 10, 2020
7766264
Revert "Move hash processing to constructor for redirect cases"
pkanher617 Apr 10, 2020
e5bf784
Adding updates from PR
pkanher617 Apr 10, 2020
0c78f60
Adding fixes for silent iframe and ssoSilent flows
pkanher617 Apr 17, 2020
19a8c6b
Adding comments
pkanher617 Apr 17, 2020
8d84b59
Merge branch 'dev' into iframe-handler-2.0
pkanher617 Apr 20, 2020
2679fbe
Adding additional tests, fixing linting
pkanher617 Apr 20, 2020
96adca4
Merge branch 'dev' into iframe-handler-2.0
pkanher617 Apr 21, 2020
a604c56
Merge branch 'dev' into iframe-handler-2.0
pkanher617 Apr 21, 2020
e4f27b8
Update credScan
pkanher617 Apr 21, 2020
fba8e5a
Adding silent handler code
pkanher617 Apr 21, 2020
c98cc9b
Update SilentHandler.spec.ts
pkanher617 Apr 22, 2020
29ef76a
Fixing interaction handler tests
pkanher617 Apr 22, 2020
b41b7fa
Update SilentHandler.ts
pkanher617 Apr 22, 2020
cfc85e8
Adding tests for silent handler in PublicClientApp
pkanher617 Apr 22, 2020
21a2e73
Merge branch 'dev' into iframe-handler-2.0
pkanher617 Apr 22, 2020
6021db3
Update BrowserAuthError.spec.ts
pkanher617 Apr 22, 2020
cfd8e00
Update PublicClientApplication.ts
pkanher617 Apr 22, 2020
ea2e219
Update PublicClientApplication.ts
pkanher617 Apr 22, 2020
35caa7a
Adding prompt error instead of warning log
pkanher617 Apr 22, 2020
5439bd2
Update BrowserAuthError.spec.ts
pkanher617 Apr 22, 2020
5bc34a5
Merge branch 'dev' into iframe-handler-2.0
pkanher617 Apr 23, 2020
f181187
Merge branch 'dev' into iframe-handler-2.0
pkanher617 Apr 23, 2020
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
5 changes: 2 additions & 3 deletions build/sdl-tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,9 @@ steps:
optionsXS: 1
optionsHMENABLE: 0

- task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@2
displayName: 'Run CredScan2'
- task: securedevelopmentteam.vss-secure-development-tools.build-task-credscan.CredScan@3
displayName: 'Run CredScan3'
inputs:
toolMajorVersion: 'V2'
scanFolder: './'
debugMode: false

Expand Down
1,791 changes: 1,003 additions & 788 deletions lib/msal-angular/package-lock.json

Large diffs are not rendered by default.

68 changes: 40 additions & 28 deletions lib/msal-browser/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 0 additions & 1 deletion lib/msal-browser/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -40,7 +40,6 @@
"doc": "npm run doc:generate && npm run doc:deploy",
"doc:generate": "typedoc --mode modules --excludePrivate --excludeProtected --out ./ref ./src/ --gitRevision dev",
"doc:deploy": "gh-pages -d ref -a -e ref/msal-browser",
"pretest": "npm link @azure/msal-common",
"test": "mocha",
"test:coverage": "nyc --reporter=text mocha",
"test:coverage:only": "npm run clean:coverage && npm run test:coverage",
Expand Down
9 changes: 7 additions & 2 deletions lib/msal-browser/src/app/Configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,9 @@ import { AuthOptions, SystemOptions, LoggerOptions, INetworkModule, LogLevel } f
import { BrowserUtils } from "../utils/BrowserUtils";
import { BrowserConstants } from "../utils/BrowserConstants";

// Default timeout for popup windows in milliseconds
// Default timeout for popup windows and iframes in milliseconds
const DEFAULT_POPUP_TIMEOUT_MS = 60000;
const DEFAULT_IFRAME_TIMEOUT_MS = 6000;
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved

export type BrowserAuthOptions = AuthOptions & {
navigateToLoginRequestUrl?: boolean;
Expand Down Expand Up @@ -37,6 +38,8 @@ export type BrowserSystemOptions = SystemOptions & {
loggerOptions?: LoggerOptions;
networkClient?: INetworkModule;
windowHashTimeout?: number;
iframeHashTimeout?: number;
loadFrameTimeout?: number;
};

/**
Expand Down Expand Up @@ -96,7 +99,9 @@ const DEFAULT_LOGGER_OPTIONS: LoggerOptions = {
const DEFAULT_SYSTEM_OPTIONS: BrowserSystemOptions = {
loggerOptions: DEFAULT_LOGGER_OPTIONS,
networkClient: BrowserUtils.getBrowserNetworkClient(),
windowHashTimeout: DEFAULT_POPUP_TIMEOUT_MS
windowHashTimeout: DEFAULT_POPUP_TIMEOUT_MS,
iframeHashTimeout: DEFAULT_IFRAME_TIMEOUT_MS,
loadFrameTimeout: BrowserUtils.detectIEOrEdge() ? 500 : 0
};

/**
Expand Down
123 changes: 113 additions & 10 deletions lib/msal-browser/src/app/PublicClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@
* Copyright (c) Microsoft Corporation. All rights reserved.
* Licensed under the MIT License.
*/
import { Account, AuthorizationCodeModule, AuthenticationParameters, INetworkModule, TokenResponse, UrlString, TemporaryCacheKeys, TokenRenewParameters, StringUtils } from "@azure/msal-common";
import { Account, AuthorizationCodeModule, AuthenticationParameters, INetworkModule, TokenResponse, UrlString, TemporaryCacheKeys, TokenRenewParameters, StringUtils, PromptValue, ServerError } from "@azure/msal-common";
import { Configuration, buildConfiguration } from "./Configuration";
import { BrowserStorage } from "../cache/BrowserStorage";
import { CryptoOps } from "../crypto/CryptoOps";
import { RedirectHandler } from "../interaction_handler/RedirectHandler";
import { PopupHandler } from "../interaction_handler/PopupHandler";
import { SilentHandler } from "../interaction_handler/SilentHandler";
import { BrowserAuthError } from "../error/BrowserAuthError";
import { BrowserConfigurationAuthError } from "../error/BrowserConfigurationAuthError";
import { BrowserConstants } from "../utils/BrowserConstants";
Expand Down Expand Up @@ -178,6 +179,9 @@ export class PublicClientApplication {
* @param {@link (AuthenticationParameters:type)}
*/
loginRedirect(request: AuthenticationParameters): void {
// block the reload if it occurred inside a hidden iframe
BrowserUtils.blockReloadInHiddenIframes();

// Check if callback has been set. If not, handleRedirectCallbacks wasn't called correctly.
if (!this.authCallback) {
throw BrowserConfigurationAuthError.createRedirectCallbacksNotSetError();
Expand All @@ -194,9 +198,9 @@ export class PublicClientApplication {
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage);

// Create login url, which will by default append the client id scope to the call.
this.authModule.createLoginUrl(request).then((navigateUrl) => {
this.authModule.createLoginUrl(request).then((navigateUrl: string) => {
// 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);
interactionHandler.initiateAuthRequest(navigateUrl);
});
} catch (e) {
this.cleanRequest();
Expand All @@ -212,6 +216,9 @@ export class PublicClientApplication {
* To acquire only idToken, please pass clientId as the only scope in the Authentication Parameters
*/
acquireTokenRedirect(request: AuthenticationParameters): void {
// block the reload if it occurred inside a hidden iframe
BrowserUtils.blockReloadInHiddenIframes();

// Check if callback has been set. If not, handleRedirectCallbacks wasn't called correctly.
if (!this.authCallback) {
throw BrowserConfigurationAuthError.createRedirectCallbacksNotSetError();
Expand All @@ -228,9 +235,9 @@ export class PublicClientApplication {
const interactionHandler = new RedirectHandler(this.authModule, this.browserStorage);

// Create acquire token url.
this.authModule.createAcquireTokenUrl(request).then((navigateUrl) => {
this.authModule.createAcquireTokenUrl(request).then((navigateUrl: string) => {
// 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);
interactionHandler.initiateAuthRequest(navigateUrl);
});
} catch (e) {
this.cleanRequest();
Expand All @@ -250,6 +257,9 @@ export class PublicClientApplication {
* @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
*/
async loginPopup(request: AuthenticationParameters): Promise<TokenResponse> {
// block the reload if it occurred inside a hidden iframe
BrowserUtils.blockReloadInHiddenIframes();

// Check if interaction is in progress. Throw error if true.
if (this.interactionInProgress()) {
throw BrowserAuthError.createInteractionInProgressError();
Expand All @@ -270,6 +280,9 @@ export class PublicClientApplication {
* @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
*/
async acquireTokenPopup(request: AuthenticationParameters): Promise<TokenResponse> {
// block the reload if it occurred inside a hidden iframe
BrowserUtils.blockReloadInHiddenIframes();

// Check if interaction is in progress. Throw error if true.
if (this.interactionInProgress()) {
throw BrowserAuthError.createInteractionInProgressError();
Expand All @@ -291,7 +304,7 @@ export class PublicClientApplication {
// 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);
const popupWindow: Window = interactionHandler.initiateAuthRequest(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.
Expand All @@ -305,6 +318,51 @@ export class PublicClientApplication {
// #endregion

// #region Silent Flow

/**
* 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 {@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
*/
async ssoSilent(request: AuthenticationParameters): Promise<TokenResponse> {
// block the reload if it occurred inside a hidden iframe
BrowserUtils.blockReloadInHiddenIframes();
DarylThayil marked this conversation as resolved.
Show resolved Hide resolved

// Check that we have some SSO data
if (StringUtils.isEmpty(request.loginHint) && StringUtils.isEmpty(request.sid) && !request.account) {
jasonnutter marked this conversation as resolved.
Show resolved Hide resolved
throw BrowserAuthError.createSilentSSOInsufficientInfoError();
}

// Check that prompt is set to none, throw error if it is set to anything else.
if (request.prompt && request.prompt !== PromptValue.NONE) {
jasonnutter marked this conversation as resolved.
Show resolved Hide resolved
throw BrowserAuthError.createSilentPromptValueError(request.prompt);
}

// Create silent request
const silentRequest: AuthenticationParameters = {
...request,
prompt: PromptValue.NONE
jasonnutter marked this conversation as resolved.
Show resolved Hide resolved
};

// Get scopeString for iframe ID
const scopeString = silentRequest.scopes ? silentRequest.scopes.join(" ") : "";

// Create authorize request url
const navigateUrl = await this.authModule.createLoginUrl(silentRequest);

return this.silentTokenHelper(navigateUrl, scopeString);
}

/**
* Use this function to obtain a token before every call to the API / resource provider
Expand All @@ -318,9 +376,54 @@ export class PublicClientApplication {
* @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
*
*/
async acquireTokenSilent(tokenRequest: TokenRenewParameters): Promise<TokenResponse> {
// Send request to renew token. Auth module will throw errors if token cannot be renewed.
return this.authModule.renewToken(tokenRequest);
async acquireTokenSilent(silentRequest: TokenRenewParameters): Promise<TokenResponse> {
// block the reload if it occurred inside a hidden iframe
BrowserUtils.blockReloadInHiddenIframes();

try {
// Send request to renew token. Auth module will throw errors if token cannot be renewed.
return await this.authModule.getValidToken(silentRequest);
} catch (e) {
const isServerError = e instanceof ServerError;
const isInvalidGrantError = (e.errorCode === BrowserConstants.INVALID_GRANT_ERROR);
if (isServerError && isInvalidGrantError) {
jasonnutter marked this conversation as resolved.
Show resolved Hide resolved
const tokenRequest: AuthenticationParameters = {
...silentRequest,
prompt: PromptValue.NONE
jasonnutter marked this conversation as resolved.
Show resolved Hide resolved
};
pkanher617 marked this conversation as resolved.
Show resolved Hide resolved

// Create authorize request url
const navigateUrl = await this.authModule.createAcquireTokenUrl(tokenRequest);

// Get scopeString for iframe ID
const scopeString = silentRequest.scopes ? silentRequest.scopes.join(" ") : "";

return this.silentTokenHelper(navigateUrl, scopeString);
}

throw e;
}
}

/**
* 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
*/
private async silentTokenHelper(navigateUrl: string, userRequestScopes: string): Promise<TokenResponse> {
try {
// Create silent handler
const silentHandler = new SilentHandler(this.authModule, this.browserStorage, this.config.system.loadFrameTimeout);
// Get the frame handle for the silent request
const msalFrame = await silentHandler.initiateAuthRequest(navigateUrl, 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.monitorFrameForHash(msalFrame, this.config.system.iframeHashTimeout, navigateUrl);
// Handle response from hash string.
return await silentHandler.handleCodeResponse(hash);
} catch(e) {
throw e;
}
}

// #endregion
Expand All @@ -333,7 +436,7 @@ export class PublicClientApplication {
*/
logout(): void {
// create logout string and navigate user window to logout. Auth module will clear cache.
this.authModule.logout().then(logoutUri => {
this.authModule.logout().then((logoutUri: string) => {
BrowserUtils.navigateWindow(logoutUri);
});
}
Expand Down