diff --git a/package-lock.json b/package-lock.json
index e30dce65e0..1a245de25a 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -31364,6 +31364,10 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
+ "node_modules/msal-browser-popup-coop": {
+ "resolved": "samples/msal-browser-samples/popup-coop",
+ "link": true
+ },
"node_modules/msal-browser-testing-sample": {
"resolved": "samples/msal-browser-samples/TestingSample",
"link": true
diff --git a/samples/msal-browser-samples/popup-coop/app/auth.js b/samples/msal-browser-samples/popup-coop/app/auth.js
new file mode 100644
index 0000000000..c21fee1024
--- /dev/null
+++ b/samples/msal-browser-samples/popup-coop/app/auth.js
@@ -0,0 +1,124 @@
+// 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;
+let accountId = "";
+
+// Create the main myMSALObj instance
+// configuration parameters are located at authConfig.js
+const myMSALObj = new msal.PublicClientApplication(msalConfig);
+
+myMSALObj.initialize().then(() => {
+ // Redirect: once login is successful and redirects with tokens, call Graph API
+ myMSALObj.handleRedirectPromise().then(handleResponse).catch(err => {
+ console.error(err);
+ });
+})
+
+function handleResponse(resp) {
+ if (resp !== null) {
+ accountId = resp.account.homeAccountId;
+ myMSALObj.setActiveAccount(resp.account);
+ showWelcomeMessage(resp.account);
+ } else {
+ // need to call getAccount here?
+ const currentAccounts = myMSALObj.getAllAccounts();
+ if (!currentAccounts || currentAccounts.length < 1) {
+ return;
+ } else if (currentAccounts.length > 1) {
+ // Add choose account code here
+ } else if (currentAccounts.length === 1) {
+ const activeAccount = currentAccounts[0];
+ myMSALObj.setActiveAccount(activeAccount);
+ accountId = activeAccount.homeAccountId;
+ showWelcomeMessage(activeAccount);
+ }
+ }
+}
+
+async function signIn(method) {
+ signInType = isIE ? "redirect" : method;
+ if (signInType === "popup") {
+ return myMSALObj.loginPopup({
+ ...loginRequest,
+ redirectUri: "http://localhost:30662"
+ }).then(handleResponse).catch(function (error) {
+ console.log(error);
+ });
+ } else if (signInType === "redirect") {
+ return myMSALObj.loginRedirect(loginRequest)
+ }
+}
+
+function signOut(interactionType) {
+ const logoutRequest = {
+ account: myMSALObj.getAccountByHomeId(accountId)
+ };
+
+ if (interactionType === "popup") {
+ myMSALObj.logoutPopup(logoutRequest).then(() => {
+ window.location.reload();
+ });
+ } else {
+ myMSALObj.logoutRedirect(logoutRequest);
+ }
+}
+
+async function getTokenPopup(request, account) {
+ request.redirectUri = "http://localhost:30662"
+ return await myMSALObj
+ .acquireTokenSilent(request)
+ .catch(async (error) => {
+ console.log("silent token acquisition fails.");
+ if (error instanceof msal.InteractionRequiredAuthError) {
+ 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, account) {
+ return await myMSALObj.acquireTokenSilent(request).catch(async (error) => {
+ console.log("silent token acquisition fails.");
+ if (error instanceof msal.InteractionRequiredAuthError) {
+ // fallback to interaction when silent call fails
+ console.log("acquiring token using redirect");
+ myMSALObj.acquireTokenRedirect(request);
+ } else {
+ console.error(error);
+ }
+ });
+}
+
+function openPopupLmso(){
+ // const popupWindow = window.open("http://localhost:30662", "popup", "width=600,height=600");
+ const popupWindow = window.open("https://login.microsoftonline.com/common/v2.0/.well-known/openid-configuration", "popup", "width=600,height=600");
+ monitorLmsoPopup(popupWindow);
+}
+
+function monitorLmsoPopup(popupWindow){
+ console.log("PopupHandler.monitorPopupLmso - monitoring popup");
+ const intervalId = setInterval(() => {
+ // Window is closed
+ if (popupWindow.closed) {
+ console.log(
+ "PopupHandler.monitorPopupLmso - window closed"
+ );
+ clearInterval(intervalId);
+ }
+ }, 100000);
+ popupWindow.location = "http://localhost:30662";
+ popupWindow.close();
+};
diff --git a/samples/msal-browser-samples/popup-coop/app/authConfig.js b/samples/msal-browser-samples/popup-coop/app/authConfig.js
new file mode 100644
index 0000000000..07fecb7d68
--- /dev/null
+++ b/samples/msal-browser-samples/popup-coop/app/authConfig.js
@@ -0,0 +1,69 @@
+// Config object to be passed to Msal on creation
+const msalConfig = {
+ auth: {
+ clientId: "8015f5e0-0370-427c-9b0d-d834189ffdd0",
+ authority:
+ "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47",
+ },
+ 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
+ },
+ system: {
+ loggerOptions: {
+ logLevel: msal.LogLevel.Trace,
+ loggerCallback: (level, message, containsPii) => {
+ if (containsPii) {
+ return;
+ }
+ switch (level) {
+ case msal.LogLevel.Error:
+ console.error(message);
+ return;
+ case msal.LogLevel.Info:
+ console.info(message);
+ return;
+ case msal.LogLevel.Verbose:
+ console.debug(message);
+ return;
+ case msal.LogLevel.Warning:
+ console.warn(message);
+ return;
+ default:
+ console.log(message);
+ return;
+ }
+ },
+ },
+ pollIntervalMilliseconds: 0
+ },
+ telemetry: {
+ application: {
+ appName: "MSAL Browser V2 Default Sample",
+ appVersion: "1.0.0",
+ },
+ },
+};
+
+// 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.com/v1.0/me",
+ graphMailEndpoint: "https://graph.microsoft.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: ["openid", "profile", "User.Read", "Mail.Read"],
+};
+
+const logoutRequest = {};
diff --git a/samples/msal-browser-samples/popup-coop/app/graph.js b/samples/msal-browser-samples/popup-coop/app/graph.js
new file mode 100644
index 0000000000..0c5f6ba224
--- /dev/null
+++ b/samples/msal-browser-samples/popup-coop/app/graph.js
@@ -0,0 +1,64 @@
+// 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() {
+ const currentAcc = myMSALObj.getAccountByHomeId(accountId);
+ if (currentAcc) {
+ const response = await getTokenPopup(loginRequest, currentAcc).catch(error => {
+ console.log(error);
+ });
+ callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI);
+ profileButton.style.display = 'none';
+ }
+}
+
+async function readMail() {
+ const currentAcc = myMSALObj.getAccountByHomeId(accountId);
+ if (currentAcc) {
+ const response = await getTokenPopup(tokenRequest, currentAcc).catch(error => {
+ console.log(error);
+ });
+ callMSGraph(graphConfig.graphMailEndpoint, response.accessToken, updateUI);
+ mailButton.style.display = 'none';
+ }
+}
+
+async function seeProfileRedirect() {
+ const currentAcc = myMSALObj.getAccountByHomeId(accountId);
+ if (currentAcc) {
+ const response = await getTokenRedirect(loginRequest, currentAcc).catch(error => {
+ console.log(error);
+ });
+ callMSGraph(graphConfig.graphMeEndpoint, response.accessToken, updateUI);
+ profileButton.style.display = 'none';
+ }
+}
+
+async function readMailRedirect() {
+ const currentAcc = myMSALObj.getAccountByHomeId(accountId);
+ if (currentAcc) {
+ const response = await getTokenRedirect(tokenRequest, currentAcc).catch(error => {
+ console.log(error);
+ });
+ callMSGraph(graphConfig.graphMailEndpoint, response.accessToken, updateUI);
+ mailButton.style.display = 'none';
+ }
+}
diff --git a/samples/msal-browser-samples/popup-coop/app/index.html b/samples/msal-browser-samples/popup-coop/app/index.html
new file mode 100644
index 0000000000..9e97b7b949
--- /dev/null
+++ b/samples/msal-browser-samples/popup-coop/app/index.html
@@ -0,0 +1,82 @@
+
+
+
+
+
+
+ Quickstart | MSAL.JS Vanilla JavaScript SPA
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Please sign-in to see your profile and read your mails
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/samples/msal-browser-samples/popup-coop/app/redirect.html b/samples/msal-browser-samples/popup-coop/app/redirect.html
new file mode 100644
index 0000000000..dcc8b0ed7d
--- /dev/null
+++ b/samples/msal-browser-samples/popup-coop/app/redirect.html
@@ -0,0 +1,6 @@
+
\ No newline at end of file
diff --git a/samples/msal-browser-samples/popup-coop/app/ui.js b/samples/msal-browser-samples/popup-coop/app/ui.js
new file mode 100644
index 0000000000..e248605008
--- /dev/null
+++ b/samples/msal-browser-samples/popup-coop/app/ui.js
@@ -0,0 +1,70 @@
+// Select DOM elements to work with
+const welcomeDiv = document.getElementById("WelcomeMessage");
+const signInButton = document.getElementById("SignIn");
+const popupButton = document.getElementById("popup");
+const redirectButton = document.getElementById("redirect");
+const cardDiv = document.getElementById("card-div");
+const mailButton = document.getElementById("readMail");
+const profileButton = document.getElementById("seeProfile");
+const profileDiv = document.getElementById("profile-div");
+
+function showWelcomeMessage(account) {
+ // Reconfiguring DOM elements
+ cardDiv.style.display = 'initial';
+ welcomeDiv.innerHTML = `Welcome ${account.username}`;
+ signInButton.setAttribute('class', "btn btn-success dropdown-toggle");
+ signInButton.innerHTML = "Sign Out";
+ popupButton.setAttribute('onClick', "signOut(this.id)");
+ popupButton.innerHTML = "Sign Out with Popup";
+ redirectButton.setAttribute('onClick', "signOut(this.id)");
+ redirectButton.innerHTML = "Sign Out with Redirect";
+}
+
+function updateUI(data, endpoint) {
+ console.log('Graph API responded at: ' + new Date().toString());
+
+ if (endpoint === graphConfig.graphMeEndpoint) {
+ const title = document.createElement('p');
+ title.innerHTML = "Title: " + data.jobTitle;
+ const email = document.createElement('p');
+ email.innerHTML = "Mail: " + data.mail;
+ const phone = document.createElement('p');
+ phone.innerHTML = "Phone: " + data.businessPhones[0];
+ const address = document.createElement('p');
+ address.innerHTML = "Location: " + data.officeLocation;
+ profileDiv.appendChild(title);
+ profileDiv.appendChild(email);
+ profileDiv.appendChild(phone);
+ profileDiv.appendChild(address);
+ } else if (endpoint === graphConfig.graphMailEndpoint) {
+ if (data.value.length < 1) {
+ alert("Your mailbox is empty!")
+ } else {
+ const tabList = document.getElementById("list-tab");
+ const tabContent = document.getElementById("nav-tabContent");
+
+ data.value.map((d, i) => {
+ // Keeping it simple
+ if (i < 10) {
+ const listItem = document.createElement("a");
+ listItem.setAttribute("class", "list-group-item list-group-item-action")
+ listItem.setAttribute("id", "list" + i + "list")
+ listItem.setAttribute("data-toggle", "list")
+ listItem.setAttribute("href", "#list" + i)
+ listItem.setAttribute("role", "tab")
+ listItem.setAttribute("aria-controls", i)
+ listItem.innerHTML = d.subject;
+ tabList.appendChild(listItem)
+
+ const contentItem = document.createElement("div");
+ contentItem.setAttribute("class", "tab-pane fade")
+ contentItem.setAttribute("id", "list" + i)
+ contentItem.setAttribute("role", "tabpanel")
+ contentItem.setAttribute("aria-labelledby", "list" + i + "list")
+ contentItem.innerHTML = " from: " + d.from.emailAddress.address + "
" + d.bodyPreview + "...";
+ tabContent.appendChild(contentItem);
+ }
+ });
+ }
+ }
+}
diff --git a/samples/msal-browser-samples/popup-coop/jest.config.js b/samples/msal-browser-samples/popup-coop/jest.config.js
new file mode 100644
index 0000000000..ddf4593104
--- /dev/null
+++ b/samples/msal-browser-samples/popup-coop/jest.config.js
@@ -0,0 +1,16 @@
+const fs = require("fs");
+const path = require("path");
+
+const APP_DIR = path.join(__dirname, 'app');
+
+// if a sample is specified, only run tests for that sample
+const sampleFolders = process.argv.find(arg => arg.startsWith('--sample='))
+ ? [path.join(APP_DIR, process.argv.find(arg => arg.startsWith('--sample='))?.split('=')[1])]
+ : fs.readdirSync(APP_DIR, { withFileTypes: true })
+ .filter(file => file.isDirectory() && file.name !== "node_modules" && fs.existsSync(path.resolve(APP_DIR, file.name, "jest.config.js")))
+ .map(file => path.join(APP_DIR, file.name));
+
+module.exports = {
+ projects: sampleFolders,
+ testTimeout: 30000
+};
diff --git a/samples/msal-browser-samples/popup-coop/package.json b/samples/msal-browser-samples/popup-coop/package.json
new file mode 100644
index 0000000000..446af6f7de
--- /dev/null
+++ b/samples/msal-browser-samples/popup-coop/package.json
@@ -0,0 +1,19 @@
+{
+ "name": "msal-browser-popup-coop",
+ "version": "1.0.0",
+ "license": "MIT",
+ "private": true,
+ "main": "server.js",
+ "scripts": {
+ "start": "node server.js",
+ "test": "npx playwright test"
+ },
+ "dependencies": {
+ "@azure/msal-node": "^2.1.0",
+ "express": "^4.19.2",
+ "path": "^0.11.14"
+ },
+ "devDependencies": {
+ "@playwright/test": "^1.30.0"
+ }
+}
diff --git a/samples/msal-browser-samples/popup-coop/server.js b/samples/msal-browser-samples/popup-coop/server.js
new file mode 100644
index 0000000000..3f481516f5
--- /dev/null
+++ b/samples/msal-browser-samples/popup-coop/server.js
@@ -0,0 +1,90 @@
+/*
+* Copyright (c) Microsoft. All rights reserved. Licensed under the MIT license.
+* See LICENSE in the source repository root for complete license information.
+*/
+const express = require('express');
+const morgan = require('morgan');
+const fs = require('fs');
+const path = require('path');
+const argv = require("yargs")
+ .usage("Usage: $0 -p [PORT] -https")
+ .alias("p", "port")
+ .alias("h", "https")
+ .describe("port", "(Optional) Port Number - default is 30662")
+ .describe("https", "(Optional) Serve over https")
+ .strict()
+ .argv;
+
+
+const DEFAULT_PORT = 30662;
+const APP_DIR = __dirname + `/app`;
+
+//initialize express.
+const app = express();
+
+// Initialize variables.
+let port = DEFAULT_PORT; // -p {PORT} || 30662;
+if (argv.p) {
+ port = argv.p;
+}
+
+let logHttpRequests = true;
+
+// Set the front-end folder to serve public assets.
+app.use("/lib", express.static(path.join(__dirname, "../../../lib/msal-browser/lib")));
+
+// app.use(express.static('app/'));
+
+// app.use(express.static('app/', {
+// setHeaders: (res) => {
+// res.set('Cross-Origin-Opener-Policy', 'same-origin');
+// }
+// }));
+
+app.use(express.static('app/', {
+ setHeaders: (res) => {
+ res.set('Cross-Origin-Opener-Policy', 'same-origin-allow-popups');
+ }
+}));
+
+if (logHttpRequests) {
+ // Configure morgan module to log all requests.
+ app.use(morgan('dev'));
+}
+
+// set up a route for redirect.html. When using popup and silent APIs,
+// we recommend setting the redirectUri to a blank page or a page that does not implement MSAL.
+app.get("/redirect", function (req, res) {
+ res.sendFile(path.join(__dirname + "/redirect.html"));
+});
+
+// Set up a route for index.html.
+app.get('*', function (req, res) {
+ res.sendFile(path.join(__dirname + '/index.html'));
+});
+
+// Start the server.
+if (argv.https) {
+ const https = require('https');
+
+ /**
+ * Secrets should never be hardcoded. The dotenv npm package can be used to store secrets or certificates
+ * in a .env file (located in project's root directory) that should be included in .gitignore to prevent
+ * accidental uploads of the secrets.
+ *
+ * Certificates can also be read-in from files via NodeJS's fs module. However, they should never be
+ * stored in the project's directory. Production apps should fetch certificates from
+ * Azure KeyVault (https://azure.microsoft.com/products/key-vault), or other secure key vaults.
+ *
+ * Please see "Certificates and Secrets" (https://learn.microsoft.com/azure/active-directory/develop/security-best-practices-for-app-registration#certificates-and-secrets)
+ * for more information.
+ */
+ const privateKey = fs.readFileSync('/certs/key.pem', 'utf8');
+ const certificate = fs.readFileSync('/certs/cert.pem', 'utf8');
+ const credentials = { key: privateKey, cert: certificate };
+ const httpsServer = https.createServer(credentials, app);
+ httpsServer.listen(port);
+} else {
+ app.listen(port);
+}
+console.log(`Listening on port ${port}...`);
diff --git a/samples/msal-browser-samples/popup-coop/tsconfig.base.json b/samples/msal-browser-samples/popup-coop/tsconfig.base.json
new file mode 100644
index 0000000000..d519609f49
--- /dev/null
+++ b/samples/msal-browser-samples/popup-coop/tsconfig.base.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "suppressImplicitAnyIndexErrors": true,
+ "sourceMap": true,
+ "declaration": true,
+ "experimentalDecorators": true,
+ "importHelpers": true,
+ "noImplicitAny": true
+ },
+ "compileOnSave": false,
+ "buildOnSave": false
+}
diff --git a/samples/msal-browser-samples/popup-coop/tsconfig.json b/samples/msal-browser-samples/popup-coop/tsconfig.json
new file mode 100644
index 0000000000..5e6695a0fa
--- /dev/null
+++ b/samples/msal-browser-samples/popup-coop/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "extends": "./tsconfig.base.json",
+ "compilerOptions": {
+ "moduleResolution": "node",
+ "module": "commonjs",
+ "target": "es5",
+ "lib": [
+ "es2015",
+ "dom",
+ "es2015.promise"
+ ],
+ "allowUnusedLabels": false,
+ "noImplicitReturns": true,
+ "esModuleInterop": true,
+ "resolveJsonModule": true,
+ "types": [
+ "node",
+ "jest"
+ ]
+ },
+ "include": [
+ "./app/**/test/*.spec.ts",
+ "./app/testUtils.ts"
+ ],
+ "exclude": [
+ "node_modules"
+ ]
+}
\ No newline at end of file