Skip to content

Commit

Permalink
Merge pull request #1543 from AzureAD/onPageLoadFix-msal-v2
Browse files Browse the repository at this point in the history
[msal-browser] Add pattern for login on page-load
  • Loading branch information
Prithvi Kanherkar authored Apr 27, 2020
2 parents dd5d2e0 + 12ffdf2 commit 4e0a4c6
Show file tree
Hide file tree
Showing 7 changed files with 396 additions and 0 deletions.
12 changes: 12 additions & 0 deletions lib/msal-browser/src/app/PublicClientApplication.ts
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ export class PublicClientApplication {
// #region Redirect Flow

/**
* WARNING: This function will be deprecated soon.
* Process any redirect-related data and send back the success or error object.
* IMPORTANT: Please do not use this function when using the popup APIs, as it may break the response handling
* in the main window.
Expand All @@ -106,6 +107,7 @@ export class PublicClientApplication {
* containing data from the server (returned with a null or non-blocking error).
*/
async handleRedirectCallback(authCallback: AuthCallback): Promise<void> {
console.warn("handleRedirectCallback will be deprecated upon release of msal-browser@v2.0.0. Please transition to using onRedirectAppLoad().");
// Check whether callback object was passed.
if (!authCallback) {
throw BrowserConfigurationAuthError.createInvalidCallbackObjectError(authCallback);
Expand All @@ -122,6 +124,16 @@ export class PublicClientApplication {
}
}

/**
* Event handler function which allows users to fire events after the PublicClientApplication object
* has loaded during redirect flows. This should be invoked on all page loads involved in redirect
* auth flows.
* @returns token response or null. If the return value is null, then no auth redirect was detected.
*/
async handleRedirectPromise(): Promise<TokenResponse|null> {
return this.tokenExchangePromise;
}

/**
* Checks if navigateToLoginRequestUrl is set, and:
* - if true, performs logic to cache and navigate
Expand Down
108 changes: 108 additions & 0 deletions samples/VanillaJSTestApp2.0/app/onPageLoad/auth.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
// Browser check variables
// If you support IE, our recommendation is that you sign-in using Redirect APIs
// If you as a developer are testing using Edge InPrivate mode, please add "isEdge" to the if check
const ua = window.navigator.userAgent;
const msie = ua.indexOf("MSIE ");
const msie11 = ua.indexOf("Trident/");
const msedge = ua.indexOf("Edge/");
const isIE = msie > 0 || msie11 > 0;
const isEdge = msedge > 0;

let signInType;

// Create the main myMSALObj instance
// configuration parameters are located at authConfig.js
const myMSALObj = new msal.PublicClientApplication(msalConfig);

// Register Callbacks for Redirect flow
myMSALObj.onRedirectAppLoad().then((tokenResponse) => {
const accountObj = tokenResponse ? tokenResponse.account : myMSALObj.getAccount();
if (accountObj) {
// Account object was retrieved, continue with app progress
console.log('id_token acquired at: ' + new Date().toString());
showWelcomeMessage(accountObj);
seeProfileRedirect();
} else if (tokenResponse && tokenResponse.tokenType === "Bearer") {
// No account object available, but access token was retrieved
console.log('access_token acquired at: ' + new Date().toString());
} else if (tokenResponse === null) {
// tokenResponse was null, attempt sign in or enter unauthenticated state for app
signIn("loginRedirect");
} else {
console.log("tokenResponse was not null but did not have any tokens: " + tokenResponse);
}
}).catch((error) => {
console.log(error);
});

async function signIn(method) {
signInType = isIE ? "loginRedirect" : method;
if (signInType === "loginPopup") {
const loginResponse = await myMSALObj.loginPopup(loginRequest).catch(function (error) {
console.log(error);
});
console.log(loginResponse);
if (myMSALObj.getAccount()) {
showWelcomeMessage(myMSALObj.getAccount());
}
} else if (signInType === "loginRedirect") {
myMSALObj.loginRedirect(loginRequest)
}
}

function signOut() {
myMSALObj.logout();
}

async function getTokenPopup(request) {
return await myMSALObj.acquireTokenSilent(request).catch(async (error) => {
console.log("silent token acquisition fails.");
if (error instanceof msal.AuthenticationRequiredError) {
if (msal.AuthenticationRequiredError.isInteractionRequiredError(error.errorCode, error.errorDesc)) {
// fallback to interaction when silent call fails
console.log("acquiring token using popup");
return myMSALObj.acquireTokenPopup(request).catch(error => {
console.error(error);
});
}
} else {
console.error(error);
}
});
}

// This function can be removed if you do not need to support IE
async function getTokenRedirect(request) {
return await myMSALObj.acquireTokenSilent(request).catch(async (error) => {
console.log("silent token acquisition fails.");
if (error instanceof AuthenticationRequiredError) {
if (AuthenticationRequiredError.isInteractionRequiredError(error.errorCode, error.errorDesc)) {
// fallback to interaction when silent call fails
console.log("acquiring token using redirect");
myMSALObj.acquireTokenRedirect(request);
}
} else {
console.error(error);
}
});
}

async function seeProfileRedirect() {
if (myMSALObj.getAccount()) {
const response = await getTokenRedirect(loginRequest).catch(error => {
console.log(error);
});
callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI);
profileButton.style.display = 'none';
}
}

async function readMailRedirect() {
if (myMSALObj.getAccount()) {
const response = await getTokenRedirect(tokenRequest).catch(error => {
console.log(error);
});
callMSGraph(graphConfig.graphMailEndpoint, response.accessToken, updateUI);
mailButton.style.display = 'none';
}
}
32 changes: 32 additions & 0 deletions samples/VanillaJSTestApp2.0/app/onPageLoad/authConfig.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
// Config object to be passed to Msal on creation
const msalConfig = {
auth: {
clientId: "3fba556e-5d4a-48e3-8e1a-fd57c12cb82e",
authority: "https://login.windows-ppe.net/common/"
},
cache: {
cacheLocation: "sessionStorage", // This configures where your cache will be stored
storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge
}
};

// Add here scopes for id token to be used at MS Identity Platform endpoints.
const loginRequest = {
scopes: ["User.Read"]
};

// Add here the endpoints for MS Graph API services you would like to use.
const graphConfig = {
graphMeEndpoint: "https://graph.microsoft-ppe.com/v1.0/me",
graphMailEndpoint: "https://graph.microsoft-ppe.com/v1.0/me/messages"
};

// Add here scopes for access token to be used at MS Graph API endpoints.
const tokenRequest = {
scopes: ["Mail.Read"],
forceRefresh: false // Set this to "true" to skip a cached token and go to the server to get a new token
};

const silentRequest = {
scopes: ["3fba556e-5d4a-48e3-8e1a-fd57c12cb82e", "User.Read", "Mail.Read"]
};
40 changes: 40 additions & 0 deletions samples/VanillaJSTestApp2.0/app/onPageLoad/graph.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Helper function to call MS Graph API endpoint
// using authorization bearer token scheme
function callMSGraph(endpoint, accessToken, callback) {
const headers = new Headers();
const bearer = `Bearer ${accessToken}`;

headers.append("Authorization", bearer);

const options = {
method: "GET",
headers: headers
};

console.log('request made to Graph API at: ' + new Date().toString());

fetch(endpoint, options)
.then(response => response.json())
.then(response => callback(response, endpoint))
.catch(error => console.log(error));
}

async function seeProfile() {
if (myMSALObj.getAccount()) {
const response = await getTokenPopup(loginRequest).catch(error => {
console.log(error);
});
callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI);
profileButton.style.display = 'none';
}
}

async function readMail() {
if (myMSALObj.getAccount()) {
const response = await getTokenPopup(tokenRequest).catch(error => {
console.log(error);
});
callMSGraph(graphConfig.graphMailEndpoint, response.accessToken, updateUI);
mailButton.style.display = 'none';
}
}
72 changes: 72 additions & 0 deletions samples/VanillaJSTestApp2.0/app/onPageLoad/index.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, shrink-to-fit=no">
<title>Quickstart | MSAL.JS Vanilla JavaScript SPA</title>

<!-- IE support: add promises polyfill before msal.js -->
<script type="text/javascript" src="//cdn.jsdelivr.net/npm/bluebird@3.7.2/js/browser/bluebird.min.js"></script>
<script src="../lib/msal-browser.js"></script>

<!-- adding Bootstrap 4 for UI components -->
<link rel="stylesheet" href="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
<link rel="SHORTCUT ICON" href="https://c.s-microsoft.com/favicon.ico?v2" type="image/x-icon">
</head>
<body>
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
<a class="navbar-brand" href="/">MS Identity Platform</a>
<div class="btn-group ml-auto dropleft">
<button type="button" id="SignIn" class="btn btn-secondary dropdown-toggle" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
Sign In
</button>
<div class="dropdown-menu">
<button class="dropdown-item" id="loginPopup" onclick="signIn(this.id)">Sign in using Popup</button>
<button class="dropdown-item" id="loginRedirect" onclick="signIn(this.id)">Sign in using Redirect</button>
</div>
</div>
</nav>
<br>
<h5 class="card-header text-center">Vanilla JavaScript SPA calling MS Graph API with MSAL.JS</h5>
<br>
<div class="row" style="margin:auto" >
<div id="card-div" class="col-md-3" style="display:none">
<div class="card text-center">
<div class="card-body">
<h5 class="card-title" id="WelcomeMessage">Please sign-in to see your profile and read your mails</h5>
<div id="profile-div"></div>
<br>
<br>
<button class="btn btn-primary" id="seeProfile" onclick="seeProfile()">See Profile</button>
<br>
<br>
<button class="btn btn-primary" id="readMail" onclick="readMail()">Read Mails</button>
</div>
</div>
</div>
<br>
<br>
<div class="col-md-4">
<div class="list-group" id="list-tab" role="tablist">
</div>
</div>
<div class="col-md-5">
<div class="tab-content" id="nav-tabContent">
</div>
</div>
</div>
<br>
<br>

<!-- importing bootstrap.js and supporting js libraries -->
<script src="https://code.jquery.com/jquery-3.4.1.slim.min.js" integrity="sha384-J6qa4849blE2+poT4WnyKhv5vZF5SrPo0iEjwBvKU7imGFAV0wwj1yYfoRSJoZ+n" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/npm/popper.js@1.16.0/dist/umd/popper.min.js" integrity="sha384-Q6E9RHvbIyZFJoft+2mJbHaEWldlvI9IOYy5n3zV9zzTtmI3UksdQRVvoxMfooAo" crossorigin="anonymous"></script>
<script src="https://stackpath.bootstrapcdn.com/bootstrap/4.4.1/js/bootstrap.min.js" integrity="sha384-wfSDF2E50Y2D1uUdj0O3uMBJnjuUD4Ih7YwaYd1iqfktj0Uod8GCExl3Og8ifwB6" crossorigin="anonymous"></script>

<!-- importing app scripts | load order is important -->
<script type="text/javascript" src="./authConfig.js"></script>
<script type="text/javascript" src="./ui.js"></script>
<script type="text/javascript" src="./auth.js"></script>
<script type="text/javascript" src="./graph.js"></script>
</body>
</html>
66 changes: 66 additions & 0 deletions samples/VanillaJSTestApp2.0/app/onPageLoad/test/template.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import "mocha";
import puppeteer from "puppeteer";
import { expect } from "chai";
import fs from "fs";

const SCREENSHOT_BASE_FOLDER_NAME = `${__dirname}/screenshots`;
let SCREENSHOT_NUM = 0;

function setupScreenshotDir() {
if (!fs.existsSync(`${SCREENSHOT_BASE_FOLDER_NAME}`)) {
fs.mkdirSync(SCREENSHOT_BASE_FOLDER_NAME);
}
}

async function takeScreenshot(page: puppeteer.Page, testName: string, screenshotName: string): Promise<void> {
const screenshotFolderName = `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}`
if (!fs.existsSync(`${screenshotFolderName}`)) {
fs.mkdirSync(screenshotFolderName);
}
await page.screenshot({ path: `${screenshotFolderName}/${++SCREENSHOT_NUM}_${screenshotName}.png` });
}

async function enterCredentials(page: puppeteer.Page, testName: string): Promise<void> {
await page.waitForNavigation({ waitUntil: "networkidle0"});
await takeScreenshot(page, testName, `loginPage`);
await page.type("#i0116", "IDLAB@msidlab0.ccsctp.net");
await page.click("#idSIButton9");
await page.waitForNavigation({ waitUntil: "networkidle0"});
await takeScreenshot(page, testName, `pwdInputPage`);
await page.type("#i0118", "");
await page.click("#idSIButton9");
}

describe("Browser tests", function () {
this.timeout(8000);
this.retries(1);

let browser: puppeteer.Browser;
before(async () => {
setupScreenshotDir();
browser = await puppeteer.launch({
headless: true,
ignoreDefaultArgs: ['--no-sandbox', '–disable-setuid-sandbox']
});
});

let context: puppeteer.BrowserContext;
let page: puppeteer.Page;
beforeEach(async () => {
SCREENSHOT_NUM = 0;
context = await browser.createIncognitoBrowserContext();
page = await context.newPage();
await page.goto('http://localhost:30662/');
});

afterEach(async () => {
await page.close();
});

after(async () => {
await context.close();
await browser.close();
});

// TODO: Add browser tests for sample here
});
Loading

0 comments on commit 4e0a4c6

Please sign in to comment.