diff --git a/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts b/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts index 31ee9fc9e5..4f250a27b9 100644 --- a/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts +++ b/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts @@ -14,6 +14,8 @@ export const hashDoesNotContainKnownProperties = "hash_does_not_contain_known_properties"; export const unableToParseState = "unable_to_parse_state"; export const stateInteractionTypeMismatch = "state_interaction_type_mismatch"; +export const noBroadcastChannelNameInState = + "no_broadcast_channel_name_in_state"; export const interactionInProgress = "interaction_in_progress"; export const popupWindowError = "popup_window_error"; export const emptyWindowError = "empty_window_error"; diff --git a/lib/msal-browser/src/interaction_client/PopupClient.ts b/lib/msal-browser/src/interaction_client/PopupClient.ts index 4939499a68..ff0f96ab7d 100644 --- a/lib/msal-browser/src/interaction_client/PopupClient.ts +++ b/lib/msal-browser/src/interaction_client/PopupClient.ts @@ -50,6 +50,7 @@ import { isPlatformAuthAllowed } from "../broker/nativeBroker/PlatformAuthProvid import { generateEarKey } from "../crypto/BrowserCrypto.js"; import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js"; import { validateRequestMethod } from "../request/RequestHelpers.js"; +import { extractBrowserRequestState } from "../utils/BrowserProtocolUtils.js"; export type PopupParams = { popup?: Window | null; @@ -335,7 +336,8 @@ export class PopupClient extends StandardInteractionClient { // 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 responseString = await this.monitorPopupForHash( popupWindow, - popupParams.popupWindowParent + popupParams.popupWindowParent, + request.state ); const serverParams = invoke( @@ -437,7 +439,7 @@ export class PopupClient extends StandardInteractionClient { this.logger, this.performanceClient, correlationId - )(popupWindow, popupParams.popupWindowParent); + )(popupWindow, popupParams.popupWindowParent, request.state); const serverParams = invoke( ResponseHandler.deserializeResponse, @@ -514,7 +516,7 @@ export class PopupClient extends StandardInteractionClient { this.logger, this.performanceClient, correlationId - )(popupWindow, popupParams.popupWindowParent); + )(popupWindow, popupParams.popupWindowParent, request.state); const serverParams = invoke( ResponseHandler.deserializeResponse, @@ -653,7 +655,8 @@ export class PopupClient extends StandardInteractionClient { await this.monitorPopupForHash( popupWindow, - popupParams.popupWindowParent + popupParams.popupWindowParent, + validRequest.state || "" ).catch(() => { // Swallow any errors related to monitoring the window. Server logout is best effort }); @@ -735,8 +738,29 @@ export class PopupClient extends StandardInteractionClient { */ monitorPopupForHash( popupWindow: Window, - popupWindowParent: Window + popupWindowParent: Window, + state: string ): Promise { + const parsedState = extractBrowserRequestState( + this.browserCrypto, + state + ); + if (!parsedState) { + throw createBrowserAuthError( + BrowserAuthErrorCodes.unableToParseState + ); + } + if (!parsedState.broadcastChannelName) { + throw createBrowserAuthError( + BrowserAuthErrorCodes.noBroadcastChannelNameInState + ); + } + const authBroadcastChannel = new BroadcastChannel( + parsedState.broadcastChannelName + ); + + authBroadcastChannel.addEventListener("message"); + return new Promise((resolve, reject) => { this.logger.verbose( "PopupHandler.monitorPopupForHash - polling started" diff --git a/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts b/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts index a36be14462..fd7d8ab9e9 100644 --- a/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts @@ -302,6 +302,10 @@ export abstract class StandardInteractionClient extends BaseInteractionClient { const redirectUri = this.getRedirectUri(request.redirectUri); const browserState: BrowserStateObject = { interactionType: interactionType, + broadcastChannelName: + interactionType === InteractionType.Popup + ? "msal.broadcast." + this.browserCrypto.createNewGuid() + : "", }; const state = ProtocolUtils.setRequestState( this.browserCrypto, diff --git a/lib/msal-browser/src/utils/BrowserProtocolUtils.ts b/lib/msal-browser/src/utils/BrowserProtocolUtils.ts index 4a6271c131..2615f7cde2 100644 --- a/lib/msal-browser/src/utils/BrowserProtocolUtils.ts +++ b/lib/msal-browser/src/utils/BrowserProtocolUtils.ts @@ -14,6 +14,7 @@ import { export type BrowserStateObject = { interactionType: InteractionType; + broadcastChannelName: string; }; /** diff --git a/package-lock.json b/package-lock.json index 768a5ed4db..4bb6671419 100644 --- a/package-lock.json +++ b/package-lock.json @@ -31363,6 +31363,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 @@ -61261,6 +61265,48 @@ "webpack": "^5.0.0" } }, + "samples/msal-browser-samples/popup-coop": { + "name": "msal-browser-popup-coop", + "version": "1.0.0", + "license": "MIT", + "dependencies": { + "@azure/msal-node": "^2.1.0", + "express": "^4.19.2", + "path": "^0.11.14" + }, + "devDependencies": { + "@playwright/test": "^1.30.0" + } + }, + "samples/msal-browser-samples/popup-coop/node_modules/@azure/msal-common": { + "version": "14.16.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.16.0.tgz", + "integrity": "sha512-1KOZj9IpcDSwpNiQNjt0jDYZpQvNZay7QAEi/5DLubay40iGYtLzya/jbjRPLyOTZhEKyL1MzPuw2HqBCjceYA==", + "license": "MIT", + "engines": { + "node": ">=0.8.0" + } + }, + "samples/msal-browser-samples/popup-coop/node_modules/@azure/msal-node": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-2.16.2.tgz", + "integrity": "sha512-An7l1hEr0w1HMMh1LU+rtDtqL7/jw74ORlc9Wnh06v7TU/xpG39/Zdr1ZJu3QpjUfKJ+E0/OXMW8DRSWTlh7qQ==", + "license": "MIT", + "dependencies": { + "@azure/msal-common": "14.16.0", + "jsonwebtoken": "^9.0.0", + "uuid": "^8.3.0" + }, + "engines": { + "node": ">=16" + } + }, + "samples/msal-browser-samples/popup-coop/node_modules/path": { + "version": "0.11.14", + "resolved": "https://registry.npmjs.org/path/-/path-0.11.14.tgz", + "integrity": "sha512-CzEXTDgcEfa0yqMe+DJCSbEB5YCv4JZoic5xulBNFF2ifIMjNrTWbNSPNhgKfSo0MjneGIx9RLy4pCFuZPaMSQ==", + "license": "MIT" + }, "samples/msal-browser-samples/TestingSample": { "name": "msal-browser-testing-sample", "version": "1.0.0", diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/default/authConfig.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/default/authConfig.js index afb8e0933f..2424f6ae13 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/default/authConfig.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/default/authConfig.js @@ -1,7 +1,7 @@ // Config object to be passed to Msal on creation const msalConfig = { auth: { - clientId: "b5c2e510-4a17-4feb-b219-e55aa5b74144", + clientId: "8015f5e0-0370-427c-9b0d-d834189ffdd0", authority: "https://login.microsoftonline.com/72f988bf-86f1-41af-91ab-2d7cd011db47", }, diff --git a/samples/msal-browser-samples/popup-coop/README.md b/samples/msal-browser-samples/popup-coop/README.md new file mode 100644 index 0000000000..982b97d6d1 --- /dev/null +++ b/samples/msal-browser-samples/popup-coop/README.md @@ -0,0 +1,60 @@ +# MSAL Browser - Popup with Cross-Origin-Opener-Policy Sample + +This sample demonstrates how to use MSAL Browser with popup authentication while implementing the `Cross-Origin-Opener-Policy` (COOP) header to enhance security. The COOP header helps protect against cross-origin attacks by isolating the browsing context. + +## Setup + +### Step 1: Clone or download this repository + +From your shell or command line: + +```bash +git clone https://github.com/AzureAD/microsoft-authentication-library-for-js.git +cd microsoft-authentication-library-for-js/samples/msal-browser-samples/popup-coop +``` + +### Step 2: Install dependencies + +```bash +npm install +``` + +### Step 3: Register the sample application in Azure portal + +1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure AD** service. +2. Select the **App Registrations** blade on the left, then select **New registration**. +3. In the **Register an application page** that appears, enter your application's registration information: + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-browser-popup-coop`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Redirect URI (optional)** section, select **Single-page application** in the combo-box and enter the following redirect URI: `http://localhost:30662/redirect`. +4. Select **Register** to create the application. +5. In the app's registration screen, find and note the **Application (client) ID**. You use this value in your app's configuration file(s) later in your code. + +### Step 4: Configure the sample + +1. Open the `app/authConfig.js` file. +2. Find the key `clientId` and replace the existing value with the application ID (clientId) of the application copied from the Azure portal. +3. Find the key `authority` and replace the existing value with your tenant ID if you want to sign in users from your specific tenant only. + +## Running the sample + +1. Start the web server: + + ```bash + npm start + ``` + +2. Open your browser and navigate to `http://localhost:30662`. + +3. **Testing different COOP headers**: You can experiment with different Cross-Origin-Opener-Policy values by modifying the header in `server.js`: + + ```javascript + res.set("Cross-Origin-Opener-Policy", "same-origin-allow-popups"); + ``` + + **To verify the COOP header is set correctly:** + + - Open Developer Tools (F12) + - Go to the **Application** tab + - Scroll down to the **top** section + - Look for `Cross-Origin-Opener-Policy` to confirm the header value 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..9220215e4b --- /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..0605e1049c --- /dev/null +++ b/samples/msal-browser-samples/popup-coop/app/authConfig.js @@ -0,0 +1,70 @@ +// 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: { + asyncPopups: false, // Set to true to enable the use of async popups for authentication + 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 + + + + + + + + + + +
+
Vanilla JavaScript SPA calling MS Graph API with MSAL.JS
+
+
+ +
+
+
+
+
+
+
+ +
+
+
+
+ + + + + + + + + + + + + + \ 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..b958a2b115 --- /dev/null +++ b/samples/msal-browser-samples/popup-coop/package.json @@ -0,0 +1,20 @@ +{ + "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", + "generate:certs": "openssl.exe req -x509 -newkey rsa:4096 -keyout key.pem -out cert.pem -days 7 -nodes -subj /CN=localhost" + }, + "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..524054ee65 --- /dev/null +++ b/samples/msal-browser-samples/popup-coop/server.js @@ -0,0 +1,83 @@ +/* +* 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/', { + setHeaders: (res) => { + res.set('Cross-Origin-Opener-Policy', 'unsafe-none'); + } +})); + +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(APP_DIR + "/redirect.html")); +}); + +// Set up a route for index.html. +app.get('*', function (req, res) { + res.sendFile(path.join(APP_DIR + '/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('./key.pem', 'utf8'); + const certificate = fs.readFileSync('./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