From 4a8d7495b8c6a3ff0d74a3ca9994e0e1627c90c6 Mon Sep 17 00:00:00 2001 From: Sameera Gajjarapu Date: Sat, 21 Sep 2024 22:29:55 -0700 Subject: [PATCH 1/7] Update COOP test sample --- package-lock.json | 23 ++++ .../popup-coop/app/auth.js | 124 ++++++++++++++++++ .../popup-coop/app/authConfig.js | 69 ++++++++++ .../popup-coop/app/graph.js | 64 +++++++++ .../popup-coop/app/index.html | 82 ++++++++++++ .../popup-coop/app/redirect.html | 6 + .../msal-browser-samples/popup-coop/app/ui.js | 70 ++++++++++ .../popup-coop/jest.config.js | 16 +++ .../popup-coop/package.json | 19 +++ .../msal-browser-samples/popup-coop/server.js | 90 +++++++++++++ .../popup-coop/tsconfig.base.json | 12 ++ .../popup-coop/tsconfig.json | 28 ++++ 12 files changed, 603 insertions(+) create mode 100644 samples/msal-browser-samples/popup-coop/app/auth.js create mode 100644 samples/msal-browser-samples/popup-coop/app/authConfig.js create mode 100644 samples/msal-browser-samples/popup-coop/app/graph.js create mode 100644 samples/msal-browser-samples/popup-coop/app/index.html create mode 100644 samples/msal-browser-samples/popup-coop/app/redirect.html create mode 100644 samples/msal-browser-samples/popup-coop/app/ui.js create mode 100644 samples/msal-browser-samples/popup-coop/jest.config.js create mode 100644 samples/msal-browser-samples/popup-coop/package.json create mode 100644 samples/msal-browser-samples/popup-coop/server.js create mode 100644 samples/msal-browser-samples/popup-coop/tsconfig.base.json create mode 100644 samples/msal-browser-samples/popup-coop/tsconfig.json diff --git a/package-lock.json b/package-lock.json index d6f998989f..8f1f2875e0 100644 --- a/package-lock.json +++ b/package-lock.json @@ -25102,6 +25102,10 @@ "version": "2.1.2", "license": "MIT" }, + "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 @@ -59450,6 +59454,25 @@ } } }, + "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/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/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 + + + + + + + + + + +
+
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..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 From 83ba2663b9672021fa802cbcbee640fb013517aa Mon Sep 17 00:00:00 2001 From: Lalima Sharda Date: Wed, 16 Jul 2025 11:37:28 -0700 Subject: [PATCH 2/7] resolving merge conflicts --- .../issue_template_bot/package-lock.json | 6 +- .github/copilot-instructions.md | 32 + .github/workflows/beachball-check.yml | 3 +- .pipelines/1p-build.yml | 10 + .pipelines/3p-e2e.yml | 2 +- CODEOWNERS | 13 +- README.md | 7 + ...-5ca5dd1f-922c-43cf-b6b5-cd03fb787ae1.json | 7 + ...-fac3c638-31fb-4b30-bca8-dcba43a3e70a.json | 7 - ...-8ac1af0a-9ef9-4ee0-be7f-6b1d9de8d169.json | 7 - ...-8304ae25-1718-4f95-8511-303fb19fa240.json | 7 - .../msal-node-extensions/CHANGELOG.json | 344 + extensions/msal-node-extensions/CHANGELOG.md | 113 +- extensions/msal-node-extensions/package.json | 7 +- .../src/packageMetadata.ts | 2 +- .../samples/electron-webpack/CHANGELOG.json | 26 + .../samples/electron-webpack/CHANGELOG.md | 14 + .../samples/electron-webpack/package.json | 118 +- .../samples/msal-node-extensions/package.json | 6 +- lib/msal-angular/CHANGELOG.json | 219 + lib/msal-angular/CHANGELOG.md | 88 +- lib/msal-angular/FAQ.md | 2 +- lib/msal-angular/README.md | 2 +- lib/msal-angular/docs/configuration.md | 4 +- lib/msal-angular/docs/initialization.md | 4 +- lib/msal-angular/karma.conf.js | 3 +- lib/msal-angular/package.json | 9 +- lib/msal-angular/src/IMsalService.ts | 7 +- .../src/msal.broadcast.service.spec.ts | 26 + .../src/msal.broadcast.service.ts | 18 +- lib/msal-angular/src/msal.guard.spec.ts | 1 + lib/msal-angular/src/msal.interceptor.spec.ts | 1 + .../src/msal.navigation.client.spec.ts | 3 +- .../src/msal.redirect.component.spec.ts | 3 +- lib/msal-angular/src/msal.service.spec.ts | 28 +- lib/msal-angular/src/msal.service.ts | 11 +- lib/msal-angular/src/packageMetadata.ts | 2 +- lib/msal-browser/CHANGELOG.json | 592 ++ lib/msal-browser/CHANGELOG.md | 180 +- .../apiReview/msal-browser.api.md | 89 +- lib/msal-browser/docs/caching.md | 3 + lib/msal-browser/docs/configuration.md | 5 + lib/msal-browser/docs/logout.md | 4 +- lib/msal-browser/package.json | 15 +- lib/msal-browser/rollup.config.js | 74 +- .../src/app/PublicClientApplication.ts | 3 +- .../nativeBroker/IPlatformAuthHandler.ts | 17 + .../broker/nativeBroker/NativeStatusCodes.ts | 1 + .../nativeBroker/PlatformAuthDOMHandler.ts | 248 + ...ler.ts => PlatformAuthExtensionHandler.ts} | 169 +- .../nativeBroker/PlatformAuthProvider.ts | 164 + ...ativeRequest.ts => PlatformAuthRequest.ts} | 43 +- ...iveResponse.ts => PlatformAuthResponse.ts} | 26 +- lib/msal-browser/src/cache/AccountManager.ts | 60 +- .../src/cache/BrowserCacheManager.ts | 911 +- lib/msal-browser/src/cache/ITokenCache.ts | 8 +- lib/msal-browser/src/cache/IWindowStorage.ts | 7 +- lib/msal-browser/src/cache/LocalStorage.ts | 9 +- lib/msal-browser/src/cache/TokenCache.ts | 7 +- lib/msal-browser/src/config/Configuration.ts | 16 +- .../src/controllers/IController.ts | 5 +- .../controllers/NestedAppAuthController.ts | 74 +- .../src/controllers/StandardController.ts | 326 +- .../UnknownOperatingContextController.ts | 2 + lib/msal-browser/src/crypto/BrowserCrypto.ts | 93 +- lib/msal-browser/src/crypto/CryptoOps.ts | 10 +- .../src/crypto/SignedHttpRequest.ts | 20 +- .../src/custom_auth/CustomAuthActionInputs.ts | 37 + .../src/custom_auth/CustomAuthConstants.ts | 50 + .../CustomAuthPublicClientApplication.ts | 158 + .../ICustomAuthPublicClientApplication.ts | 51 + .../src/custom_auth/UserAccountAttributes.ts | 16 + .../configuration/CustomAuthConfiguration.ts | 22 + .../CustomAuthStandardController.ts | 509 ++ .../ICustomAuthStandardController.ts | 53 + .../custom_auth/core/CustomAuthAuthority.ts | 112 + .../core/auth_flow/AuthFlowErrorBase.ts | 140 + .../core/auth_flow/AuthFlowResultBase.ts | 55 + .../core/auth_flow/AuthFlowState.ts | 73 + .../core/error/CustomAuthApiError.ts | 41 + .../custom_auth/core/error/CustomAuthError.ts | 20 + .../src/custom_auth/core/error/HttpError.ts | 13 + .../custom_auth/core/error/HttpErrorCodes.ts | 7 + .../core/error/InvalidArgumentError.ts | 15 + .../core/error/InvalidConfigurationError.ts | 13 + .../error/InvalidConfigurationErrorCodes.ts | 8 + .../core/error/MethodNotImplementedError.ts | 15 + .../core/error/MsalCustomAuthError.ts | 22 + .../core/error/NoCachedAccountFoundError.ts | 17 + .../custom_auth/core/error/ParsedUrlError.ts | 13 + .../core/error/ParsedUrlErrorCodes.ts | 6 + .../custom_auth/core/error/UnexpectedError.ts | 25 + .../core/error/UnsupportedEnvironmentError.ts | 17 + .../core/error/UserAccountAttributeError.ts | 15 + .../error/UserAccountAttributeErrorCodes.ts | 6 + .../core/error/UserAlreadySignedInError.ts | 17 + .../CustomAuthInteractionClientBase.ts | 92 + .../CustomAuthInterationClientFactory.ts | 57 + .../custom_auth_api/BaseApiClient.ts | 168 + .../custom_auth_api/CustomAuthApiClient.ts | 38 + .../custom_auth_api/CustomAuthApiEndpoint.ts | 18 + .../custom_auth_api/ICustomAuthApiClient.ts | 13 + .../custom_auth_api/ResetPasswordApiClient.ts | 172 + .../custom_auth_api/SignInApiClient.ts | 186 + .../custom_auth_api/SignupApiClient.ts | 141 + .../custom_auth_api/types/ApiErrorCodes.ts | 26 + .../types/ApiErrorResponseTypes.ts | 36 + .../custom_auth_api/types/ApiRequestTypes.ts | 91 + .../custom_auth_api/types/ApiResponseTypes.ts | 66 + .../custom_auth_api/types/ApiSuberrors.ts | 14 + .../custom_auth_api/types/ApiTypesBase.ts | 15 + .../http_client/FetchHttpClient.ts | 86 + .../network_client/http_client/IHttpClient.ts | 54 + .../custom_auth/core/telemetry/PublicApiId.ts | 37 + .../core/utils/ArgumentValidator.ts | 26 + .../src/custom_auth/core/utils/UrlUtils.ts | 25 + .../auth_flow/CustomAuthAccountData.ts | 185 + .../auth_flow/error_type/GetAccountError.ts | 45 + .../auth_flow/result/GetAccessTokenResult.ts | 72 + .../auth_flow/result/GetAccountResult.ts | 69 + .../auth_flow/result/SignOutResult.ts | 62 + .../auth_flow/state/GetAccessTokenState.ts | 16 + .../auth_flow/state/GetAccountState.ts | 16 + .../auth_flow/state/SignOutState.ts | 16 + .../CustomAuthSilentCacheClient.ts | 215 + lib/msal-browser/src/custom_auth/index.ts | 185 + .../CustomAuthOperatingContext.ts | 43 + .../error_type/ResetPasswordError.ts | 96 + .../result/ResetPasswordResendCodeResult.ts | 76 + .../result/ResetPasswordStartResult.ts | 70 + .../result/ResetPasswordSubmitCodeResult.ts | 70 + .../ResetPasswordSubmitPasswordResult.ts | 65 + .../state/ResetPasswordCodeRequiredState.ts | 130 + .../state/ResetPasswordCompletedState.ts | 11 + .../state/ResetPasswordFailedState.ts | 11 + .../ResetPasswordPasswordRequiredState.ts | 73 + .../auth_flow/state/ResetPasswordState.ts | 29 + .../state/ResetPasswordStateParameters.ts | 25 + .../interaction_client/ResetPasswordClient.ts | 311 + .../parameter/ResetPasswordParams.ts | 28 + .../result/ResetPasswordActionResult.ts | 21 + .../sign_in/auth_flow/SignInScenario.ts | 12 + .../auth_flow/error_type/SignInError.ts | 79 + .../result/SignInResendCodeResult.ts | 67 + .../sign_in/auth_flow/result/SignInResult.ts | 87 + .../result/SignInSubmitCodeResult.ts | 44 + .../result/SignInSubmitCredentialResult.ts | 43 + .../result/SignInSubmitPasswordResult.ts | 41 + .../state/SignInCodeRequiredState.ts | 141 + .../auth_flow/state/SignInCompletedState.ts | 12 + .../state/SignInContinuationState.ts | 71 + .../auth_flow/state/SignInFailedState.ts | 11 + .../state/SignInPasswordRequiredState.ts | 83 + .../sign_in/auth_flow/state/SignInState.ts | 34 + .../auth_flow/state/SignInStateParameters.ts | 32 + .../interaction_client/SignInClient.ts | 396 + .../parameter/SignInParams.ts | 39 + .../result/SignInActionResult.ts | 65 + .../auth_flow/error_type/SignUpError.ts | 138 + .../result/SignUpResendCodeResult.ts | 70 + .../sign_up/auth_flow/result/SignUpResult.ts | 88 + .../result/SignUpSubmitAttributesResult.ts | 70 + .../result/SignUpSubmitCodeResult.ts | 90 + .../result/SignUpSubmitPasswordResult.ts | 80 + .../state/SignUpAttributesRequiredState.ts | 115 + .../state/SignUpCodeRequiredState.ts | 196 + .../auth_flow/state/SignUpCompletedState.ts | 11 + .../auth_flow/state/SignUpFailedState.ts | 11 + .../state/SignUpPasswordRequiredState.ts | 112 + .../sign_up/auth_flow/state/SignUpState.ts | 34 + .../auth_flow/state/SignUpStateParameters.ts | 31 + .../interaction_client/SignUpClient.ts | 496 ++ .../parameter/SignUpParams.ts | 36 + .../result/SignUpActionResult.ts | 77 + .../src/error/BrowserAuthError.ts | 15 +- .../src/error/BrowserAuthErrorCodes.ts | 5 +- lib/msal-browser/src/error/NativeAuthError.ts | 4 + lib/msal-browser/src/event/EventMessage.ts | 11 + lib/msal-browser/src/event/EventType.ts | 1 + lib/msal-browser/src/index.ts | 3 + .../BaseInteractionClient.ts | 20 +- ...nt.ts => PlatformAuthInteractionClient.ts} | 350 +- .../src/interaction_client/PopupClient.ts | 302 +- .../src/interaction_client/RedirectClient.ts | 436 +- .../SilentAuthCodeClient.ts | 10 +- .../interaction_client/SilentCacheClient.ts | 5 +- .../interaction_client/SilentIframeClient.ts | 274 +- .../StandardInteractionClient.ts | 49 +- .../interaction_handler/InteractionHandler.ts | 15 +- .../interaction_handler/RedirectHandler.ts | 251 - .../src/interaction_handler/SilentHandler.ts | 33 +- .../src/navigation/NavigationClient.ts | 17 +- lib/msal-browser/src/network/FetchClient.ts | 39 +- lib/msal-browser/src/packageMetadata.ts | 2 +- lib/msal-browser/src/protocol/Authorize.ts | 502 ++ .../src/request/AuthorizationUrlRequest.ts | 8 +- lib/msal-browser/src/request/PopupRequest.ts | 1 + .../src/request/RedirectRequest.ts | 1 + .../src/request/SsoSilentRequest.ts | 1 + .../src/response/ResponseHandler.ts | 6 +- .../src/utils/BrowserConstants.ts | 28 +- lib/msal-browser/src/utils/BrowserUtils.ts | 2 +- .../src/utils/MsalFrameStatsUtils.ts | 32 + .../test/app/PublicClientApplication.spec.ts | 951 +- .../broker/PlatformAuthDOMHandler.spec.ts | 553 ++ ...s => PlatformAuthExtensionHandler.spec.ts} | 150 +- .../test/broker/PlatformAuthProvider.spec.ts | 277 + .../test/cache/BrowserCacheManager.spec.ts | 2166 +++-- .../test/cache/LocalStorage.spec.ts | 21 +- .../test/cache/TestStorageManager.ts | 6 +- .../test/cache/TokenCache.spec.ts | 197 +- .../test/crypto/BrowserCrypto.spec.ts | 155 + .../test/crypto/CryptoOps.spec.ts | 3 +- .../CustomAuthPublicClientApplication.spec.ts | 184 + .../CustomAuthStandardController.spec.ts | 411 + .../core/CustomAuthAuthority.spec.ts | 178 + .../CustomAuthApiClient.spec.ts | 28 + .../http_client/FetchClient.spec.ts | 59 + .../core/utils/ArgumentValidator.spec.ts | 85 + .../custom_auth/core/utils/UrlUtils.spec.ts | 73 + .../auth_flow/CustomAuthAccountData.spec.ts | 267 + .../error_type/GetAccountError.spec.ts | 34 + .../CustomAuthSilentCacheClient.spec.ts | 448 + .../integration_tests/GetAccount.spec.ts | 181 + .../integration_tests/ResetPassword.spec.ts | 247 + .../integration_tests/SignIn.spec.ts | 395 + .../integration_tests/SignUp.spec.ts | 648 ++ .../error_type/ResetPasswordError.spec.ts | 133 + .../ResetPasswordCodeRequiredState.spec.ts | 126 + ...ResetPasswordPasswordRequiredState.spec.ts | 121 + .../ResetPasswordClient.spec.ts | 357 + .../auth_flow/error_type/SignInError.spec.ts | 128 + .../state/SignInCodeRequiredState.spec.ts | 196 + .../state/SignInContinuationState.spec.ts | 107 + .../state/SignInPasswordRequiredState.spec.ts | 113 + .../interation_client/SignInClient.spec.ts | 358 + .../auth_flow/error_type/SignUpError.spec.ts | 189 + .../SignUpAttributesRequiredState.spec.ts | 97 + .../state/SignUpCodeRequiredState.spec.ts | 168 + .../state/SignUpPasswordRequiredState.spec.ts | 118 + .../interaction_client/SignUpClient.spec.ts | 655 ++ .../test_resources/CustomAuthConfig.ts | 49 + .../test_resources/TestConstants.ts | 57 + .../custom_auth/test_resources/TestModules.ts | 76 + .../test/error/NativeAuthError.spec.ts | 18 + .../BaseInteractionClient.spec.ts | 3 +- ... => PlatformAuthInteractionClient.spec.ts} | 656 +- .../interaction_client/PopupClient.spec.ts | 109 +- .../interaction_client/RedirectClient.spec.ts | 1200 +-- .../SilentAuthCodeClient.spec.ts | 5 +- .../SilentIframeClient.spec.ts | 119 +- .../StandardInteractionClient.spec.ts | 82 +- .../InteractionHandler.spec.ts | 78 +- .../RedirectHandler.spec.ts | 576 -- .../interaction_handler/SilentHandler.spec.ts | 12 +- lib/msal-browser/test/naa/JSRuntime.spec.ts | 17 + lib/msal-browser/test/naa/MockBridge.ts | 6 + .../test/navigation/NavigationClient.spec.ts | 32 +- .../test/network/FetchClient.spec.ts | 20 +- .../test/protocol/Authorize.spec.ts | 376 + .../test/utils/StringConstants.ts | 212 +- .../tsconfig.custom-auth.build.json | 8 + lib/msal-common/CHANGELOG.json | 441 + lib/msal-common/CHANGELOG.md | 132 +- lib/msal-common/apiReview/msal-common.api.md | 564 +- lib/msal-common/package.json | 3 +- lib/msal-common/src/account/AccountInfo.ts | 1 + lib/msal-common/src/authority/Authority.ts | 6 +- lib/msal-common/src/authority/ProtocolMode.ts | 11 + .../src/authority/RegionDiscovery.ts | 15 +- lib/msal-common/src/cache/CacheManager.ts | 282 +- .../src/cache/entities/AccountEntity.ts | 9 +- .../src/cache/entities/CredentialEntity.ts | 2 + .../src/cache/interface/ICacheManager.ts | 50 +- .../src/client/AuthorizationCodeClient.ts | 409 +- lib/msal-common/src/client/BaseClient.ts | 13 +- .../src/client/RefreshTokenClient.ts | 9 +- .../src/client/SilentFlowClient.ts | 13 +- .../src/config/ClientConfiguration.ts | 15 +- .../src/constants/AADServerParamKeys.ts | 2 + lib/msal-common/src/crypto/ICrypto.ts | 8 +- lib/msal-common/src/error/CacheError.ts | 31 +- lib/msal-common/src/error/CacheErrorCodes.ts | 4 +- .../src/error/ClientConfigurationError.ts | 8 - .../error/ClientConfigurationErrorCodes.ts | 1 - .../src/error/InteractionRequiredAuthError.ts | 3 + .../InteractionRequiredAuthErrorCodes.ts | 1 + lib/msal-common/src/error/NetworkError.ts | 4 +- lib/msal-common/src/exports-common.ts | 12 +- lib/msal-common/src/logger/Logger.ts | 2 +- .../src/network/ThrottlingUtils.ts | 13 +- lib/msal-common/src/packageMetadata.ts | 2 +- lib/msal-common/src/protocol/Authorize.ts | 415 + .../src/request/BaseAuthRequest.ts | 2 +- .../request/CommonAuthorizationUrlRequest.ts | 5 +- .../src/request/RequestParameterBuilder.ts | 186 +- .../src/request/RequestValidator.ts | 90 - .../src/response/AuthorizeResponse.ts | 80 + .../src/response/ResponseHandler.ts | 107 +- .../ServerAuthorizationCodeResponse.ts | 34 - .../telemetry/performance/PerformanceEvent.ts | 35 +- .../server/ServerTelemetryManager.ts | 20 +- lib/msal-common/src/utils/Constants.ts | 37 +- lib/msal-common/src/utils/UrlUtils.ts | 27 +- .../test/account/AuthToken.spec.ts | 73 +- .../test/account/ClientInfo.spec.ts | 67 +- .../test/authority/Authority.spec.ts | 4 +- .../test/cache/CacheManager.spec.ts | 509 +- lib/msal-common/test/cache/MockCache.ts | 2 + .../test/cache/entities/AccountEntity.spec.ts | 96 +- .../client/AuthorizationCodeClient.spec.ts | 1570 +--- .../test/client/BaseClient.spec.ts | 3 +- .../test/client/ClientTestUtils.ts | 123 +- .../test/client/RefreshTokenClient.spec.ts | 40 +- .../test/client/SilentFlowClient.spec.ts | 3 +- .../test/config/ClientConfiguration.spec.ts | 112 +- .../test/crypto/PopTokenGenerator.spec.ts | 70 +- .../test/network/ThrottlingUtils.spec.ts | 48 +- .../test/protocol/Authorize.spec.ts | 1743 ++++ .../request/RequestParameterBuilder.spec.ts | 242 +- .../test/request/RequestValidator.spec.ts | 94 - .../test/response/ResponseHandler.spec.ts | 475 +- .../test/telemetry/PerformanceClient.spec.ts | 6 +- .../telemetry/ServerTelemetryManager.spec.ts | 4 +- .../test/utils/ProtocolUtils.spec.ts | 66 +- lib/msal-node/CHANGELOG.json | 390 + lib/msal-node/CHANGELOG.md | 123 +- lib/msal-node/README.md | 1 + lib/msal-node/apiReview/msal-node.api.md | 22 +- lib/msal-node/docs/certificate-credentials.md | 12 +- lib/msal-node/docs/faq.md | 2 +- ...tialize-confidential-client-application.md | 4 +- lib/msal-node/docs/request.md | 68 +- lib/msal-node/docs/sni.md | 4 +- lib/msal-node/jest.config.cjs | 1 + lib/msal-node/package.json | 5 +- lib/msal-node/src/cache/NodeStorage.ts | 9 +- lib/msal-node/src/cache/TokenCache.ts | 18 +- lib/msal-node/src/client/ClientApplication.ts | 88 +- lib/msal-node/src/client/ClientAssertion.ts | 3 +- .../src/client/ClientCredentialClient.ts | 12 +- .../client/ConfidentialClientApplication.ts | 22 +- .../src/client/ManagedIdentityApplication.ts | 81 +- .../ManagedIdentitySources/AppService.ts | 12 +- .../client/ManagedIdentitySources/AzureArc.ts | 19 +- .../BaseManagedIdentitySource.ts | 49 +- .../ManagedIdentitySources/CloudShell.ts | 8 +- .../src/client/ManagedIdentitySources/Imds.ts | 53 +- .../ManagedIdentitySources/MachineLearning.ts | 42 +- .../ManagedIdentitySources/ServiceFabric.ts | 47 +- lib/msal-node/src/client/OnBehalfOfClient.ts | 19 +- .../src/client/PublicClientApplication.ts | 19 +- .../src/client/UsernamePasswordClient.ts | 6 +- lib/msal-node/src/config/Configuration.ts | 12 +- lib/msal-node/src/config/ManagedIdentityId.ts | 2 +- .../ManagedIdentityRequestParameters.ts | 18 +- lib/msal-node/src/crypto/CryptoProvider.ts | 8 +- lib/msal-node/src/crypto/PkceGenerator.ts | 6 +- lib/msal-node/src/index.ts | 7 +- .../src/network/HttpClientWithRetries.ts | 11 +- lib/msal-node/src/network/ILoopbackClient.ts | 4 +- lib/msal-node/src/network/LoopbackClient.ts | 89 +- lib/msal-node/src/packageMetadata.ts | 2 +- lib/msal-node/src/protocol/Authorize.ts | 74 + .../src/request/ManagedIdentityRequest.ts | 9 +- .../DefaultManagedIdentityRetryPolicy.ts | 70 + .../src/retry/ExponentialRetryStrategy.ts | 53 + lib/msal-node/src/retry/IHttpRetryPolicy.ts | 19 +- lib/msal-node/src/retry/ImdsRetryPolicy.ts | 121 + lib/msal-node/src/retry/LinearRetryPolicy.ts | 71 - .../src/retry/LinearRetryStrategy.ts | 40 + lib/msal-node/src/utils/Constants.ts | 52 +- lib/msal-node/src/utils/EncodingUtils.ts | 6 +- lib/msal-node/test/cache/TokenCache.spec.ts | 13 +- .../test/client/ClientAssertion.spec.ts | 10 +- .../client/ClientCredentialClient.spec.ts | 6 +- lib/msal-node/test/client/ClientTestUtils.ts | 21 +- .../ConfidentialClientApplication.spec.ts | 25 +- .../ManagedIdentitySources/AppService.spec.ts | 20 +- .../ManagedIdentitySources/AzureArc.spec.ts | 4 +- .../DefaultManagedIdentityRetryPolicy.spec.ts | 394 + .../ManagedIdentitySources/Imds.spec.ts | 427 +- .../MachineLearning.spec.ts | 100 +- .../ServiceFabric.spec.ts | 104 +- .../client/PublicClientApplication.spec.ts | 69 +- .../test/test_kit/ManagedIdentityTestUtils.ts | 9 + .../test/test_kit/StringConstants.ts | 11 +- lib/msal-node/test/utils/CryptoKeys.ts | 3 +- lib/msal-node/test/utils/TestConstants.ts | 2 +- lib/msal-react/CHANGELOG.json | 263 + lib/msal-react/CHANGELOG.md | 94 +- lib/msal-react/README.md | 46 +- lib/msal-react/apiReview/msal-react.api.md | 2 +- lib/msal-react/package.json | 19 +- lib/msal-react/src/MsalProvider.tsx | 7 +- lib/msal-react/src/hooks/useAccount.ts | 41 +- .../src/hooks/useMsalAuthentication.ts | 3 +- lib/msal-react/src/packageMetadata.ts | 2 +- package-lock.json | 7658 ++++++++--------- samples/e2eTestUtils/package.json | 2 +- samples/e2eTestUtils/src/Constants.ts | 36 +- .../src/ElectronPlaywrightTestUtils.ts | 109 +- samples/e2eTestUtils/src/TestUtils.ts | 273 +- .../angular-b2c-sample/package.json | 2 +- .../angular-b2c-sample/tsconfig.json | 2 +- .../angular-modules-sample/package.json | 2 +- .../angular-standalone-sample/angular.json | 34 +- .../angular-standalone-sample/package.json | 28 +- .../angular-standalone-sample/tsconfig.json | 2 +- .../HybridSample/package.json | 3 +- .../HybridSample/routes/auth.js | 9 +- .../HybridSample/views/client-redirect.hbs | 4 +- .../HybridSample/views/implicit-redirect.hbs | 2 +- .../OfficeAddin/package.json | 2 +- .../test/browserAAD.spec.ts | 86 +- .../test/browserAADMultiTenant.spec.ts | 1 - .../test/browserAADTenanted.spec.ts | 1 - .../test/localStorage.spec.ts | 10 +- .../app/wamBroker/authConfig.js | 12 +- .../VanillaJSTestApp2.0/tsconfig.json | 4 +- .../vue3-sample-app/package.json | 2 +- .../ElectronSystemBrowserTestApp/package.json | 10 +- .../src/app/components/PageLayout.tsx | 2 +- .../ElectronTestApp/package.json | 2 +- .../Managed-Identity/FIC/.npmrc | 1 + .../Managed-Identity/FIC/package.json | 2 +- .../Managed-Identity/Imds/.npmrc | 1 + .../Managed-Identity/Imds/package.json | 2 +- .../auth-code-cli-app/package.json | 2 +- .../auth-code-distributed-cache/package.json | 4 +- .../auth-code-key-vault/package.json | 3 +- .../auth-code-pkce/package.json | 2 +- .../auth-code-with-certs/package.json | 3 +- .../msal-node-samples/auth-code/package.json | 2 +- .../b2c-user-flows/package.json | 4 +- .../package.json | 6 +- .../README.md | 26 +- .../app.ts | 29 +- .../cliArgs.ts | 28 +- .../package.json | 6 +- ...tials-with-cert-from-key-vault-aad.spec.ts | 8 +- .../tsconfig.json | 17 +- .../client-credentials/README.md | 8 +- .../client-credentials/index.js | 8 +- .../client-credentials/package.json | 3 +- .../package.json | 6 +- .../on-behalf-of/package.json | 2 +- .../refresh-token/package.json | 4 +- .../silent-flow/package.json | 4 +- samples/msal-node-samples/tsconfig.json | 2 +- .../username-password-cca/package.json | 4 +- .../b2c-sample/package.json | 4 +- .../src/ui-components/WelcomeName.jsx | 5 +- .../b2c-sample/test/msa-account.spec.ts | 13 +- .../nextjs-sample/next-env.d.ts | 2 +- .../nextjs-sample/package.json | 6 +- .../src/ui-components/WelcomeName.jsx | 5 +- .../nextjs-sample/test/profile.spec.ts | 2 +- .../react-router-sample/package.json | 4 +- .../src/ui-components/WelcomeName.jsx | 5 +- .../react-router-sample/test/profile.spec.ts | 2 +- .../test/profileRawContext.spec.ts | 2 +- .../test/profileWithMsal.spec.ts | 2 +- samples/msal-react-samples/tsconfig.json | 4 +- .../typescript-sample/README.md | 4 +- .../typescript-sample/package.json | 10 +- .../typescript-sample/test/profile.spec.ts | 2 +- shared-configs/eslint-config-msal/index.js | 1 + shared-configs/jest-config/jest.config.cjs | 1 + shared-configs/jest-config/setupGlobals.cjs | 5 +- 470 files changed, 35630 insertions(+), 13715 deletions(-) create mode 100644 .github/copilot-instructions.md create mode 100644 change/@azure-msal-browser-5ca5dd1f-922c-43cf-b6b5-cd03fb787ae1.json delete mode 100644 change/@azure-msal-browser-fac3c638-31fb-4b30-bca8-dcba43a3e70a.json delete mode 100644 change/@azure-msal-common-8ac1af0a-9ef9-4ee0-be7f-6b1d9de8d169.json delete mode 100644 change/@azure-msal-node-8304ae25-1718-4f95-8511-303fb19fa240.json create mode 100644 extensions/samples/electron-webpack/CHANGELOG.json create mode 100644 extensions/samples/electron-webpack/CHANGELOG.md create mode 100644 lib/msal-browser/src/broker/nativeBroker/IPlatformAuthHandler.ts create mode 100644 lib/msal-browser/src/broker/nativeBroker/PlatformAuthDOMHandler.ts rename lib/msal-browser/src/broker/nativeBroker/{NativeMessageHandler.ts => PlatformAuthExtensionHandler.ts} (75%) create mode 100644 lib/msal-browser/src/broker/nativeBroker/PlatformAuthProvider.ts rename lib/msal-browser/src/broker/nativeBroker/{NativeRequest.ts => PlatformAuthRequest.ts} (53%) rename lib/msal-browser/src/broker/nativeBroker/{NativeResponse.ts => PlatformAuthResponse.ts} (70%) create mode 100644 lib/msal-browser/src/custom_auth/CustomAuthActionInputs.ts create mode 100644 lib/msal-browser/src/custom_auth/CustomAuthConstants.ts create mode 100644 lib/msal-browser/src/custom_auth/CustomAuthPublicClientApplication.ts create mode 100644 lib/msal-browser/src/custom_auth/ICustomAuthPublicClientApplication.ts create mode 100644 lib/msal-browser/src/custom_auth/UserAccountAttributes.ts create mode 100644 lib/msal-browser/src/custom_auth/configuration/CustomAuthConfiguration.ts create mode 100644 lib/msal-browser/src/custom_auth/controller/CustomAuthStandardController.ts create mode 100644 lib/msal-browser/src/custom_auth/controller/ICustomAuthStandardController.ts create mode 100644 lib/msal-browser/src/custom_auth/core/CustomAuthAuthority.ts create mode 100644 lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowErrorBase.ts create mode 100644 lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowResultBase.ts create mode 100644 lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowState.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/CustomAuthApiError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/CustomAuthError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/HttpError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/HttpErrorCodes.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/InvalidArgumentError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/InvalidConfigurationError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/InvalidConfigurationErrorCodes.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/MethodNotImplementedError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/MsalCustomAuthError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/NoCachedAccountFoundError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/ParsedUrlError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/ParsedUrlErrorCodes.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/UnexpectedError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/UnsupportedEnvironmentError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/UserAccountAttributeError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/UserAccountAttributeErrorCodes.ts create mode 100644 lib/msal-browser/src/custom_auth/core/error/UserAlreadySignedInError.ts create mode 100644 lib/msal-browser/src/custom_auth/core/interaction_client/CustomAuthInteractionClientBase.ts create mode 100644 lib/msal-browser/src/custom_auth/core/interaction_client/CustomAuthInterationClientFactory.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/BaseApiClient.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiEndpoint.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/ICustomAuthApiClient.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/ResetPasswordApiClient.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/SignInApiClient.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/SignupApiClient.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorCodes.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorResponseTypes.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiRequestTypes.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiResponseTypes.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiSuberrors.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiTypesBase.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/http_client/FetchHttpClient.ts create mode 100644 lib/msal-browser/src/custom_auth/core/network_client/http_client/IHttpClient.ts create mode 100644 lib/msal-browser/src/custom_auth/core/telemetry/PublicApiId.ts create mode 100644 lib/msal-browser/src/custom_auth/core/utils/ArgumentValidator.ts create mode 100644 lib/msal-browser/src/custom_auth/core/utils/UrlUtils.ts create mode 100644 lib/msal-browser/src/custom_auth/get_account/auth_flow/CustomAuthAccountData.ts create mode 100644 lib/msal-browser/src/custom_auth/get_account/auth_flow/error_type/GetAccountError.ts create mode 100644 lib/msal-browser/src/custom_auth/get_account/auth_flow/result/GetAccessTokenResult.ts create mode 100644 lib/msal-browser/src/custom_auth/get_account/auth_flow/result/GetAccountResult.ts create mode 100644 lib/msal-browser/src/custom_auth/get_account/auth_flow/result/SignOutResult.ts create mode 100644 lib/msal-browser/src/custom_auth/get_account/auth_flow/state/GetAccessTokenState.ts create mode 100644 lib/msal-browser/src/custom_auth/get_account/auth_flow/state/GetAccountState.ts create mode 100644 lib/msal-browser/src/custom_auth/get_account/auth_flow/state/SignOutState.ts create mode 100644 lib/msal-browser/src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.ts create mode 100644 lib/msal-browser/src/custom_auth/index.ts create mode 100644 lib/msal-browser/src/custom_auth/operating_context/CustomAuthOperatingContext.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordResendCodeResult.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordStartResult.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitCodeResult.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitPasswordResult.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordCompletedState.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordFailedState.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordState.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordStateParameters.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/interaction_client/ResetPasswordClient.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/interaction_client/parameter/ResetPasswordParams.ts create mode 100644 lib/msal-browser/src/custom_auth/reset_password/interaction_client/result/ResetPasswordActionResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/SignInScenario.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/error_type/SignInError.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInResendCodeResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitCodeResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitCredentialResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitPasswordResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInCodeRequiredState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInCompletedState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInContinuationState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInFailedState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInPasswordRequiredState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInStateParameters.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/interaction_client/SignInClient.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/interaction_client/parameter/SignInParams.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_in/interaction_client/result/SignInActionResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/error_type/SignUpError.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpResendCodeResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitAttributesResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitCodeResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitPasswordResult.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpAttributesRequiredState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpCodeRequiredState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpCompletedState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpFailedState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpPasswordRequiredState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpState.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpStateParameters.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/interaction_client/SignUpClient.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/interaction_client/parameter/SignUpParams.ts create mode 100644 lib/msal-browser/src/custom_auth/sign_up/interaction_client/result/SignUpActionResult.ts rename lib/msal-browser/src/interaction_client/{NativeInteractionClient.ts => PlatformAuthInteractionClient.ts} (82%) delete mode 100644 lib/msal-browser/src/interaction_handler/RedirectHandler.ts create mode 100644 lib/msal-browser/src/protocol/Authorize.ts create mode 100644 lib/msal-browser/src/utils/MsalFrameStatsUtils.ts create mode 100644 lib/msal-browser/test/broker/PlatformAuthDOMHandler.spec.ts rename lib/msal-browser/test/broker/{NativeMessageHandler.spec.ts => PlatformAuthExtensionHandler.spec.ts} (80%) create mode 100644 lib/msal-browser/test/broker/PlatformAuthProvider.spec.ts create mode 100644 lib/msal-browser/test/crypto/BrowserCrypto.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/CustomAuthPublicClientApplication.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/controller/CustomAuthStandardController.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/core/CustomAuthAuthority.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/core/network_client/http_client/FetchClient.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/core/utils/ArgumentValidator.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/core/utils/UrlUtils.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/get_account/auth_flow/CustomAuthAccountData.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/get_account/auth_flow/error_type/GetAccountError.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/integration_tests/GetAccount.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/integration_tests/ResetPassword.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/integration_tests/SignIn.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/integration_tests/SignUp.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/reset_password/interaction_client/ResetPasswordClient.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/sign_in/auth_flow/error_type/SignInError.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInCodeRequiredState.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInContinuationState.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInPasswordRequiredState.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/sign_in/interation_client/SignInClient.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/sign_up/auth_flow/error_type/SignUpError.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpAttributesRequiredState.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpCodeRequiredState.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpPasswordRequiredState.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/sign_up/interaction_client/SignUpClient.spec.ts create mode 100644 lib/msal-browser/test/custom_auth/test_resources/CustomAuthConfig.ts create mode 100644 lib/msal-browser/test/custom_auth/test_resources/TestConstants.ts create mode 100644 lib/msal-browser/test/custom_auth/test_resources/TestModules.ts rename lib/msal-browser/test/interaction_client/{NativeInteractionClient.spec.ts => PlatformAuthInteractionClient.spec.ts} (66%) delete mode 100644 lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts create mode 100644 lib/msal-browser/test/protocol/Authorize.spec.ts create mode 100644 lib/msal-browser/tsconfig.custom-auth.build.json create mode 100644 lib/msal-common/src/protocol/Authorize.ts delete mode 100644 lib/msal-common/src/request/RequestValidator.ts create mode 100644 lib/msal-common/src/response/AuthorizeResponse.ts delete mode 100644 lib/msal-common/src/response/ServerAuthorizationCodeResponse.ts create mode 100644 lib/msal-common/test/protocol/Authorize.spec.ts delete mode 100644 lib/msal-common/test/request/RequestValidator.spec.ts create mode 100644 lib/msal-node/src/protocol/Authorize.ts create mode 100644 lib/msal-node/src/retry/DefaultManagedIdentityRetryPolicy.ts create mode 100644 lib/msal-node/src/retry/ExponentialRetryStrategy.ts create mode 100644 lib/msal-node/src/retry/ImdsRetryPolicy.ts delete mode 100644 lib/msal-node/src/retry/LinearRetryPolicy.ts create mode 100644 lib/msal-node/src/retry/LinearRetryStrategy.ts create mode 100644 lib/msal-node/test/client/ManagedIdentitySources/DefaultManagedIdentityRetryPolicy.spec.ts create mode 100644 samples/msal-node-samples/Managed-Identity/FIC/.npmrc create mode 100644 samples/msal-node-samples/Managed-Identity/Imds/.npmrc rename samples/msal-node-samples/{ => client-credentials-with-cert-from-key-vault}/cliArgs.ts (58%) diff --git a/.github/actions/issue_template_bot/package-lock.json b/.github/actions/issue_template_bot/package-lock.json index 484387e2f9..b0c98aef76 100644 --- a/.github/actions/issue_template_bot/package-lock.json +++ b/.github/actions/issue_template_bot/package-lock.json @@ -291,9 +291,9 @@ } }, "node_modules/undici": { - "version": "5.28.5", - "resolved": "https://registry.npmjs.org/undici/-/undici-5.28.5.tgz", - "integrity": "sha512-zICwjrDrcrUE0pyyJc1I2QzBkLM8FINsgOrt6WjA+BgajVq9Nxu2PbFFXUrAggLfDXlZGZBVZYw7WNV5KiBiBA==", + "version": "5.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.29.0.tgz", + "integrity": "sha512-raqeBD6NQK4SkWhQzeYKd1KmIG6dllBOTt55Rmkt4HtI9mwdWtJljnrXjAFUBLTSN67HWrOIZ3EPF4kjUw80Bg==", "license": "MIT", "dependencies": { "@fastify/busboy": "^2.0.0" diff --git a/.github/copilot-instructions.md b/.github/copilot-instructions.md new file mode 100644 index 0000000000..0999d2dccd --- /dev/null +++ b/.github/copilot-instructions.md @@ -0,0 +1,32 @@ +This repository contains JavaScript/TypeScript SDKs for integrating with Microsoft Entra for authentication and authorization. Please follow these guidelines when contributing: + +## Code Standards + +### Required before each commit: +- Pull the latest changes from the base branch +- Install dependencies for the relevant packages, for example if making changes to `msal-browser` you would run `npm install --workspace=@azure/msal-browser --workspace=@azure/msal-common --include-workspace-root` +- Ensure all code builds successfully by running `npm run build:all` from the directory of each changed package. +- Ensure all code is formatted by running `npm run format:fix` from the directory of each changed package. +- Ensure changes pass linting by running `npm run lint` from the directory of each changed package. +- Test all changes by running `npm run test` from the directory of each changed package. +- Update apiExtractor reports by running `npm run apiExtractor -- --local` from the directory of each changed package. +- Create changefiles by running `npm run beachball:change` from the root of the repo. Include PR number in changelog message. + +## Repository Structure + +- `lib/`: Contains the source code for the MSAL SDKs +- `extensions/`: Contains source code for an additional extension SDK for use with MSAL-Node +- `docs/`: Contains documentation +- `lib/*/test/`: Contains unit tests for the SDKs +- `samples/`: Contains sample applications demonstrating how to use the SDKs + +## Key guidelines +- This repo utilizes npm workspaces. Dependencies can be installed and scripts can be run for specific packages using the workspace flag, for example `npm install --workspace=@azure/msal-common --include-workspace-root` +- Follow JavaScript and TypeScript best practices. +- Follow Angular and React best practices in their respective SDKs (msal-angular and msal-react). +- Never make breaking, non-backwards compatible, changes. Follow semantic versioning principles. +- Maintain existing code structure, organization and naming conventions. +- Use descriptive commit messages that explain the changes made. +- Write unit tests for new functionality and bug fixes. +- Any new public facing functionality should update samples and documentation to demonstrate usage. +- Update documentation as needed to reflect changes in functionality or usage. diff --git a/.github/workflows/beachball-check.yml b/.github/workflows/beachball-check.yml index 907bae5bf7..2738a314a9 100644 --- a/.github/workflows/beachball-check.yml +++ b/.github/workflows/beachball-check.yml @@ -6,6 +6,7 @@ on: pull_request: branches: - dev + - update_beachball_filter concurrency: group: beachball-${{github.ref}} @@ -19,7 +20,7 @@ jobs: permissions: contents: read # for actions/checkout to fetch code pull-requests: read # for dorny/paths-filter to read pull requests - if: github.actor != 'dependabot[bot]' && github.head_ref != 'release-staging' && github.head_ref != '3p-release-staging' && !contains(github.head_ref, 'post-release') + if: github.actor != 'dependabot[bot]' && !startsWith(github.head_ref, 'release-staging-') && github.head_ref != '3p-release-staging' && !contains(github.head_ref, 'post-release') runs-on: ubuntu-latest env: BEACHBALL: 1 diff --git a/.pipelines/1p-build.yml b/.pipelines/1p-build.yml index 324ea883f2..b4eb0b819d 100644 --- a/.pipelines/1p-build.yml +++ b/.pipelines/1p-build.yml @@ -3,6 +3,10 @@ parameters: displayName: "NPM Install Timeout (Tests)" type: number default: 15 + - name: debug + displayName: "Debug" + type: boolean + default: false variables: CDP_DEFINITION_BUILD_COUNT: $[counter('', 0)] # needed for onebranch.pipeline.version task https://aka.ms/obpipelines/versioning LinuxContainerImage: "mcr.microsoft.com/onebranch/cbl-mariner/build:2.0" # Docker image which is used to build the project https://aka.ms/obpipelines/containers @@ -41,11 +45,13 @@ extends: libName: msal-common path: "lib/" npmInstallTimeout: ${{ parameters.npmInstallTimeout }} + debug: ${{ parameters.debug }} - template: .pipelines/templates/ci-template.yml@1P parameters: libName: msal-browser path: "lib/" npmInstallTimeout: ${{ parameters.npmInstallTimeout }} + debug: ${{ parameters.debug }} - template: .pipelines/templates/ci-template.yml@1P parameters: libName: msal-browser-1p @@ -55,18 +61,22 @@ extends: libName: msal-node path: "lib/" npmInstallTimeout: ${{ parameters.npmInstallTimeout }} + debug: ${{ parameters.debug }} - template: .pipelines/templates/ci-template.yml@1P parameters: libName: msal-react path: "lib/" npmInstallTimeout: ${{ parameters.npmInstallTimeout }} + debug: ${{ parameters.debug }} - template: .pipelines/templates/ci-template.yml@1P parameters: libName: msal-angular path: "lib/" npmInstallTimeout: ${{ parameters.npmInstallTimeout }} + debug: ${{ parameters.debug }} - template: .pipelines/templates/ci-template.yml@1P parameters: libName: msal-node-extensions path: "extensions/" os: ["linux", "windows", "macOs"] + debug: ${{ parameters.debug }} diff --git a/.pipelines/3p-e2e.yml b/.pipelines/3p-e2e.yml index bd47fcb9ed..9e9a76c9d1 100644 --- a/.pipelines/3p-e2e.yml +++ b/.pipelines/3p-e2e.yml @@ -89,7 +89,7 @@ extends: sourceRepo: ${{ variables.sourceRepo }} sourceBranch: ${{ variables.sourceBranch }} workspace: "samples/msal-node-samples" - nodeVersions: [16, 18, 20, 22] + nodeVersions: [16, 18, 20, 22, 24] samples: - "auth-code" - "auth-code-cli-app" diff --git a/CODEOWNERS b/CODEOWNERS index fb25fccaca..1b7124f553 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1,11 +1,12 @@ * @tnorling @sameerag @jo-arroyo @hectormmg @lalimasharda @peterzenz @bgavrilMS @robbie-microsoft @konstantin-msft @shylasummers # MSAL Angular -/lib/msal-angular/ @jo-arroyo @tnorling @peterzenz -/samples/msal-angular-samples/ @jo-arroyo @tnorling @peterzenz +/lib/msal-angular/ @jo-arroyo @tnorling @peterzenz @shylasummers @sameerag @konstantin-msft +/samples/msal-angular-samples/ @jo-arroyo @tnorling @peterzenz @shylasummers @sameerag @konstantin-msft # MSAL Browser /lib/msal-browser/ @sameerag @tnorling @hectormmg @jo-arroyo @peterzenz @konstantin-msft @lalimasharda @shylasummers +/lib/msal-browser/custom-auth @shenj @yongdiw /samples/msal-browser-samples/ @sameerag @tnorling @hectormmg @jo-arroyo @peterzenz @konstantin-msft @lalimasharda @shylasummers # MSAL Common @@ -17,14 +18,14 @@ /extensions/msal-node-extensions/ @sameerag @tnorling @hectormmg @peterzenz # MSAL React -/lib/msal-react/ @tnorling @jo-arroyo @peterzenz -/samples/msal-react-samples/ @tnorling @jo-arroyo @peterzenz +/lib/msal-react/ @tnorling @jo-arroyo @peterzenz @sameerag @konstantin-msft +/samples/msal-react-samples/ @tnorling @jo-arroyo @peterzenz @sameerag @konstantin-msft # Build /build/ @sameerag @tnorling @hectormmg @peterzenz -/release-scripts/ @sameerag @tnorling @hectormmg @peterzenz +/release-scripts/ @sameerag @tnorling @hectormmg @peterzenz @shylasummers /.github/ @sameerag @tnorling @hectormmg @peterzenz -/.pipelines/ @hectormmg @tnorling @peterzenz @sameerag @konstantin-msft +/.pipelines/ @hectormmg @tnorling @peterzenz @sameerag @konstantin-msft @shylasummers # MSAL Node Confidential Client Regression Tests .github/workflows/client-credential-benchmark.yml @AzureAD/id4s-msal-team diff --git a/README.md b/README.md index a5c3808a34..f0d93a1098 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,13 @@ The [`lib`](https://github.com/AzureAD/microsoft-authentication-library-for-js/t - [On-behalf-of Flow](https://learn.microsoft.com/en-us/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) - [Microsoft Authentication Library for JavaScript](lib/msal-browser/): A browser-based, framework-agnostic browser library that enables authentication and token acquisition with the Microsoft Identity platform in JavaScript applications. Implements the OAuth 2.0 [Authorization Code Flow with PKCE](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow), and is [OpenID-compliant](https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc). + +- [Native Authentication Support for JavaScript](lib/msal-browser/src/custom_auth/): MSAL also provides native authentication APIs that allow applications to implement a native experience with end-to-end customizable flows in their applications. With native authentication, users are guided through a rich, native, sign-up and sign-in journey without leaving the app. The native authentication feature is available for SPAs on [External ID for customers](https://learn.microsoft.com/en-us/entra/identity-platform/concept-native-authentication). It is recommended to always use the most up-to-date version of the SDK. + + > **Note:** The native authentication feature is currently in preview and is not considered production-stable. Features and APIs may change before general availability. + > + > **Terminology:** In the codebase, the term "Custom Auth" is used instead of "Native Auth". You will find classes, interfaces, and configuration options prefixed with `CustomAuth` (e.g., `CustomAuthPublicClientApplication`, `CustomAuthConfiguration`). Please refer to these when implementing or exploring the native authentication feature in the code. + - [Microsoft Authentication Library for React](lib/msal-react/): A wrapper of the msal-browser library for apps using React. - [Microsoft Authentication Library for Angular](lib/msal-angular/): A wrapper of the msal-browser library for apps using Angular framework. - [Microsoft Authentication Extensions for Node](extensions/msal-node-extensions/): The Microsoft Authentication Extensions for Node offers secure mechanisms for client applications to perform cross-platform token cache serialization and persistence. It gives additional support to the Microsoft Authentication Library for Node (MSAL). diff --git a/change/@azure-msal-browser-5ca5dd1f-922c-43cf-b6b5-cd03fb787ae1.json b/change/@azure-msal-browser-5ca5dd1f-922c-43cf-b6b5-cd03fb787ae1.json new file mode 100644 index 0000000000..01d430f27c --- /dev/null +++ b/change/@azure-msal-browser-5ca5dd1f-922c-43cf-b6b5-cd03fb787ae1.json @@ -0,0 +1,7 @@ +{ + "type": "patch", + "comment": "Fix exception when using claims with Nested App Auth in JS Runtime environment (#7926)", + "packageName": "@azure/msal-browser", + "email": "dasau@microsoft.com", + "dependentChangeType": "patch" +} diff --git a/change/@azure-msal-browser-fac3c638-31fb-4b30-bca8-dcba43a3e70a.json b/change/@azure-msal-browser-fac3c638-31fb-4b30-bca8-dcba43a3e70a.json deleted file mode 100644 index c25b03cd86..0000000000 --- a/change/@azure-msal-browser-fac3c638-31fb-4b30-bca8-dcba43a3e70a.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "patch", - "comment": "refactor RequestParameterBuilder", - "packageName": "@azure/msal-browser", - "email": "thomas.norling@microsoft.com", - "dependentChangeType": "patch" -} diff --git a/change/@azure-msal-common-8ac1af0a-9ef9-4ee0-be7f-6b1d9de8d169.json b/change/@azure-msal-common-8ac1af0a-9ef9-4ee0-be7f-6b1d9de8d169.json deleted file mode 100644 index 04693c34df..0000000000 --- a/change/@azure-msal-common-8ac1af0a-9ef9-4ee0-be7f-6b1d9de8d169.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "minor", - "comment": "refactor RequestParameterBuilder", - "packageName": "@azure/msal-common", - "email": "thomas.norling@microsoft.com", - "dependentChangeType": "patch" -} diff --git a/change/@azure-msal-node-8304ae25-1718-4f95-8511-303fb19fa240.json b/change/@azure-msal-node-8304ae25-1718-4f95-8511-303fb19fa240.json deleted file mode 100644 index c459c4d662..0000000000 --- a/change/@azure-msal-node-8304ae25-1718-4f95-8511-303fb19fa240.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "type": "patch", - "comment": "refactor RequestParameterBuilder", - "packageName": "@azure/msal-node", - "email": "thomas.norling@microsoft.com", - "dependentChangeType": "patch" -} diff --git a/extensions/msal-node-extensions/CHANGELOG.json b/extensions/msal-node-extensions/CHANGELOG.json index e925d6a60a..d5977a8661 100644 --- a/extensions/msal-node-extensions/CHANGELOG.json +++ b/extensions/msal-node-extensions/CHANGELOG.json @@ -1,6 +1,350 @@ { "name": "@azure/msal-node-extensions", "entries": [ + { + "date": "Tue, 08 Jul 2025 14:21:12 GMT", + "version": "1.5.17", + "tag": "@azure/msal-node-extensions_v1.5.17", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump @azure/msal-common to v15.8.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 01 Jul 2025 14:23:25 GMT", + "version": "1.5.16", + "tag": "@azure/msal-node-extensions_v1.5.16", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump @azure/msal-common to v15.8.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Wed, 18 Jun 2025 15:21:55 GMT", + "version": "1.5.15", + "tag": "@azure/msal-node-extensions_v1.5.15", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump @azure/msal-common to v15.7.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Fri, 30 May 2025 22:36:45 GMT", + "version": "1.5.14", + "tag": "@azure/msal-node-extensions_v1.5.14", + "comments": { + "patch": [ + { + "author": "shylasummers@microsoft.com", + "package": "@azure/msal-node-extensions", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "update common version" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 06 May 2025 22:47:43 GMT", + "version": "1.5.13", + "tag": "@azure/msal-node-extensions_v1.5.13", + "comments": { + "none": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-node-extensions", + "commit": "b6098464c80f3db124ca3db145446ddb71be936d", + "comment": "New dev dependency" + } + ], + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump @azure/msal-common to v15.6.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 29 Apr 2025 20:25:48 GMT", + "version": "1.5.12", + "tag": "@azure/msal-node-extensions_v1.5.12", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump @azure/msal-common to v15.5.2", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 15 Apr 2025 23:34:14 GMT", + "version": "1.5.11", + "tag": "@azure/msal-node-extensions_v1.5.11", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump @azure/msal-common to v15.5.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 08 Apr 2025 16:56:06 GMT", + "version": "1.5.10", + "tag": "@azure/msal-node-extensions_v1.5.10", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump @azure/msal-common to v15.5.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 25 Mar 2025 22:29:34 GMT", + "version": "1.5.9", + "tag": "@azure/msal-node-extensions_v1.5.9", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-node-extensions", + "commit": "8bbed7012616d9106c923d08b5c4e408a741577f", + "comment": "Update @azure/msal-node-runtime dependency to ^0.18.1" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump @azure/msal-common to v15.4.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Thu, 20 Mar 2025 22:09:04 GMT", + "version": "1.5.8", + "tag": "@azure/msal-node-extensions_v1.5.8", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump @azure/msal-common to v15.3.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node-extensions", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, { "date": "Tue, 11 Mar 2025 18:51:25 GMT", "version": "1.5.7", diff --git a/extensions/msal-node-extensions/CHANGELOG.md b/extensions/msal-node-extensions/CHANGELOG.md index 20c525f4d6..8d8d136c30 100644 --- a/extensions/msal-node-extensions/CHANGELOG.md +++ b/extensions/msal-node-extensions/CHANGELOG.md @@ -1,9 +1,120 @@ # Change Log - @azure/msal-node-extensions - + +## 1.5.17 + +Tue, 08 Jul 2025 14:21:12 GMT + +### Patches + +- Bump @azure/msal-common to v15.8.1 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 1.5.16 + +Tue, 01 Jul 2025 14:23:25 GMT + +### Patches + +- Bump @azure/msal-common to v15.8.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 1.5.15 + +Wed, 18 Jun 2025 15:21:55 GMT + +### Patches + +- Bump @azure/msal-common to v15.7.1 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 1.5.14 + +Fri, 30 May 2025 22:36:45 GMT + +### Patches + +- update common version (shylasummers@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 1.5.13 + +Tue, 06 May 2025 22:47:43 GMT + +### Patches + +- Bump @azure/msal-common to v15.6.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 1.5.12 + +Tue, 29 Apr 2025 20:25:48 GMT + +### Patches + +- Bump @azure/msal-common to v15.5.2 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 1.5.11 + +Tue, 15 Apr 2025 23:34:14 GMT + +### Patches + +- Bump @azure/msal-common to v15.5.1 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 1.5.10 + +Tue, 08 Apr 2025 16:56:06 GMT + +### Patches + +- Bump @azure/msal-common to v15.5.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 1.5.9 + +Tue, 25 Mar 2025 22:29:34 GMT + +### Patches + +- Update @azure/msal-node-runtime dependency to ^0.18.1 (thomas.norling@microsoft.com) +- Bump @azure/msal-common to v15.4.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 1.5.8 + +Thu, 20 Mar 2025 22:09:04 GMT + +### Patches + +- Bump @azure/msal-common to v15.3.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + ## 1.5.7 Tue, 11 Mar 2025 18:51:25 GMT diff --git a/extensions/msal-node-extensions/package.json b/extensions/msal-node-extensions/package.json index 920e5075d8..53c6d4dfac 100644 --- a/extensions/msal-node-extensions/package.json +++ b/extensions/msal-node-extensions/package.json @@ -1,6 +1,6 @@ { "name": "@azure/msal-node-extensions", - "version": "1.5.7", + "version": "1.5.17", "repository": { "type": "git", "url": "https://github.com/AzureAD/microsoft-authentication-library-for-js.git" @@ -64,8 +64,8 @@ ] }, "dependencies": { - "@azure/msal-common": "15.2.1", - "@azure/msal-node-runtime": "^0.17.1", + "@azure/msal-common": "15.8.1", + "@azure/msal-node-runtime": "^0.18.1", "keytar": "^7.8.0" }, "devDependencies": { @@ -75,6 +75,7 @@ "@types/node": "^20.3.1", "eslint-config-msal": "file:../../shared-configs/eslint-config-msal", "jest": "^29.5.0", + "jest-junit": "^16.0.0", "msal-test-utils": "file:../../shared-test-utils", "node-addon-api": "^6.1.0", "rollup": "^4.22.4", diff --git a/extensions/msal-node-extensions/src/packageMetadata.ts b/extensions/msal-node-extensions/src/packageMetadata.ts index 2bdd71b34e..c40e53b467 100644 --- a/extensions/msal-node-extensions/src/packageMetadata.ts +++ b/extensions/msal-node-extensions/src/packageMetadata.ts @@ -1,3 +1,3 @@ /* eslint-disable header/header */ export const name = "@azure/msal-node-extensions"; -export const version = "1.5.7"; +export const version = "1.5.17"; diff --git a/extensions/samples/electron-webpack/CHANGELOG.json b/extensions/samples/electron-webpack/CHANGELOG.json new file mode 100644 index 0000000000..1aadac7aef --- /dev/null +++ b/extensions/samples/electron-webpack/CHANGELOG.json @@ -0,0 +1,26 @@ +{ + "name": "electron-webpack", + "entries": [ + { + "date": "Fri, 30 May 2025 22:36:45 GMT", + "version": "1.0.1", + "tag": "electron-webpack_v1.0.1", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "electron-webpack", + "comment": "Bump @azure/msal-node to v3.6.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "electron-webpack", + "comment": "Bump @azure/msal-node-extensions to v1.5.14", + "commit": "not available" + } + ] + } + } + ] +} diff --git a/extensions/samples/electron-webpack/CHANGELOG.md b/extensions/samples/electron-webpack/CHANGELOG.md new file mode 100644 index 0000000000..77ca232e8b --- /dev/null +++ b/extensions/samples/electron-webpack/CHANGELOG.md @@ -0,0 +1,14 @@ +# Change Log - electron-webpack + + + + + +## 1.0.1 + +Fri, 30 May 2025 22:36:45 GMT + +### Patches + +- Bump @azure/msal-node to v3.6.0 +- Bump @azure/msal-node-extensions to v1.5.14 diff --git a/extensions/samples/electron-webpack/package.json b/extensions/samples/electron-webpack/package.json index 3178e0bf78..c3b450d955 100644 --- a/extensions/samples/electron-webpack/package.json +++ b/extensions/samples/electron-webpack/package.json @@ -1,61 +1,61 @@ { - "name": "electron-webpack", - "productName": "electron-webpack", - "version": "1.0.0", - "description": "An electron application implementing msal-node-extensions bundler with webpack", - "main": ".webpack/main", - "scripts": { - "start": "electron-forge start", - "package": "electron-forge package", - "make": "electron-forge make", - "publish": "electron-forge publish", - "lint": "eslint --ext .ts,.tsx .", - "build:package": "npm run build --workspace=@azure/msal-common --workspace=@azure/msal-node --workspace=@azure/msal-node-extensions", - "start:build": "npm run build:package && npm start" - }, - "keywords": [ - "Electron", - "quick", - "start", - "tutorial", - "demo", - "typescript", - "webpack", - "msal-node-extensions", - "msal-node" - ], - "license": "MIT", - "devDependencies": { - "@electron-forge/cli": "^6.0.0", - "@electron-forge/maker-deb": "^6.0.0", - "@electron-forge/maker-rpm": "^6.0.0", - "@electron-forge/maker-squirrel": "^6.0.0", - "@electron-forge/maker-zip": "^6.0.0", - "@electron-forge/plugin-webpack": "^6.0.0", - "@popperjs/core": "^2.11.6", - "@typescript-eslint/eslint-plugin": "^5.42.1", - "@typescript-eslint/parser": "^5.42.1", - "@vercel/webpack-asset-relocator-loader": "^1.7.3", - "autoprefixer": "^10.4.13", - "css-loader": "^6.7.1", - "electron": "22.3.25", - "eslint": "^8.27.0", - "eslint-plugin-import": "^2.26.0", - "fork-ts-checker-webpack-plugin": "^7.2.13", - "node-loader": "^2.0.0", - "postcss": "^8.4.18", - "postcss-loader": "^4.2.0", - "sass": "^1.3.0", - "sass-loader": "^10.1.1", - "style-loader": "^3.3.1", - "ts-loader": "^9.4.1", - "ts-node": "^10.9.1", - "typescript": "~4.5.4" - }, - "dependencies": { - "@azure/msal-node": "^3.0.0", - "@azure/msal-node-extensions": "^1.0.10", - "bootstrap": "^5.0.0", - "electron-squirrel-startup": "^1.0.0" - } + "name": "electron-webpack", + "productName": "electron-webpack", + "version": "1.0.1", + "description": "An electron application implementing msal-node-extensions bundler with webpack", + "main": ".webpack/main", + "scripts": { + "start": "electron-forge start", + "package": "electron-forge package", + "make": "electron-forge make", + "publish": "electron-forge publish", + "lint": "eslint --ext .ts,.tsx .", + "build:package": "npm run build --workspace=@azure/msal-common --workspace=@azure/msal-node --workspace=@azure/msal-node-extensions", + "start:build": "npm run build:package && npm start" + }, + "keywords": [ + "Electron", + "quick", + "start", + "tutorial", + "demo", + "typescript", + "webpack", + "msal-node-extensions", + "msal-node" + ], + "license": "MIT", + "devDependencies": { + "@electron-forge/cli": "^6.0.0", + "@electron-forge/maker-deb": "^6.0.0", + "@electron-forge/maker-rpm": "^6.0.0", + "@electron-forge/maker-squirrel": "^6.0.0", + "@electron-forge/maker-zip": "^6.0.0", + "@electron-forge/plugin-webpack": "^6.0.0", + "@popperjs/core": "^2.11.6", + "@typescript-eslint/eslint-plugin": "^5.42.1", + "@typescript-eslint/parser": "^5.42.1", + "@vercel/webpack-asset-relocator-loader": "^1.7.3", + "autoprefixer": "^10.4.13", + "css-loader": "^6.7.1", + "electron": "22.3.25", + "eslint": "^8.27.0", + "eslint-plugin-import": "^2.26.0", + "fork-ts-checker-webpack-plugin": "^7.2.13", + "node-loader": "^2.0.0", + "postcss": "^8.4.18", + "postcss-loader": "^4.2.0", + "sass": "^1.3.0", + "sass-loader": "^10.1.1", + "style-loader": "^3.3.1", + "ts-loader": "^9.4.1", + "ts-node": "^10.9.1", + "typescript": "~4.5.4" + }, + "dependencies": { + "@azure/msal-node": "^3.6.0", + "@azure/msal-node-extensions": "^1.5.14", + "bootstrap": "^5.0.0", + "electron-squirrel-startup": "^1.0.0" + } } diff --git a/extensions/samples/msal-node-extensions/package.json b/extensions/samples/msal-node-extensions/package.json index 2384bc9989..0d567e78f9 100644 --- a/extensions/samples/msal-node-extensions/package.json +++ b/extensions/samples/msal-node-extensions/package.json @@ -12,8 +12,8 @@ "author": "Microsoft", "license": "MIT", "dependencies": { - "@azure/msal-node": "^3.0.0", - "@azure/msal-node-extensions": "^1.0.0", + "@azure/msal-node": "^3.6.0", + "@azure/msal-node-extensions": "^1.5.14", "express": "^4.20.0" } -} \ No newline at end of file +} diff --git a/lib/msal-angular/CHANGELOG.json b/lib/msal-angular/CHANGELOG.json index 2ce6bb5004..0c04199ef8 100644 --- a/lib/msal-angular/CHANGELOG.json +++ b/lib/msal-angular/CHANGELOG.json @@ -1,6 +1,225 @@ { "name": "@azure/msal-angular", "entries": [ + { + "date": "Tue, 08 Jul 2025 14:21:12 GMT", + "version": "4.0.15", + "tag": "@azure/msal-angular_v4.0.15", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump @azure/msal-browser to v4.15.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 01 Jul 2025 14:23:25 GMT", + "version": "4.0.14", + "tag": "@azure/msal-angular_v4.0.14", + "comments": { + "patch": [ + { + "author": "shylasummers@microsoft.com", + "package": "@azure/msal-angular", + "commit": "b2942e458324c8bf767a068cae3eccbd4863baf3", + "comment": "Fix build output during publishing" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump @azure/msal-browser to v4.14.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Wed, 18 Jun 2025 15:21:55 GMT", + "version": "4.0.13", + "tag": "@azure/msal-angular_v4.0.13", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump @azure/msal-browser to v4.13.2", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 06 May 2025 22:47:43 GMT", + "version": "4.0.12", + "tag": "@azure/msal-angular_v4.0.12", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump @azure/msal-browser to v4.12.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 29 Apr 2025 20:25:48 GMT", + "version": "4.0.11", + "tag": "@azure/msal-angular_v4.0.11", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-angular", + "commit": "2b6112d62495ac4e3245d8a0d9be6b541ffb206c", + "comment": "Fix circular dependency" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump @azure/msal-browser to v4.11.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 15 Apr 2025 23:34:14 GMT", + "version": "4.0.10", + "tag": "@azure/msal-angular_v4.0.10", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump @azure/msal-browser to v4.11.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 08 Apr 2025 16:56:05 GMT", + "version": "4.0.9", + "tag": "@azure/msal-angular_v4.0.9", + "comments": { + "patch": [ + { + "author": "lalimasharda@microsoft.com", + "package": "@azure/msal-angular", + "commit": "fabf962dcb69611b4516342107bc7bcc364c4e68", + "comment": "added resetInProgressEvent function to reset event after handleRedirectPromise is called #7682" + }, + { + "author": "shylasummers@microsoft.com", + "package": "@azure/msal-angular", + "commit": "fabf962dcb69611b4516342107bc7bcc364c4e68", + "comment": "Deprecate logout() in Angular" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump @azure/msal-browser to v4.10.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 25 Mar 2025 22:29:34 GMT", + "version": "4.0.8", + "tag": "@azure/msal-angular_v4.0.8", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-angular", + "commit": "8bbed7012616d9106c923d08b5c4e408a741577f", + "comment": "Fix request type for ssoSilent" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump @azure/msal-browser to v4.9.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Thu, 20 Mar 2025 22:09:04 GMT", + "version": "4.0.7", + "tag": "@azure/msal-angular_v4.0.7", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump @azure/msal-browser to v4.8.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-angular", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, { "date": "Tue, 11 Mar 2025 18:51:25 GMT", "version": "4.0.6", diff --git a/lib/msal-angular/CHANGELOG.md b/lib/msal-angular/CHANGELOG.md index 9dccdaa87a..290a84f089 100644 --- a/lib/msal-angular/CHANGELOG.md +++ b/lib/msal-angular/CHANGELOG.md @@ -1,9 +1,95 @@ # Change Log - @azure/msal-angular - + +## 4.0.15 + +Tue, 08 Jul 2025 14:21:12 GMT + +### Patches + +- Bump @azure/msal-browser to v4.15.0 +- Bump eslint-config-msal to v0.0.0 + +## 4.0.14 + +Tue, 01 Jul 2025 14:23:25 GMT + +### Patches + +- Fix build output during publishing (shylasummers@microsoft.com) +- Bump @azure/msal-browser to v4.14.0 +- Bump eslint-config-msal to v0.0.0 + +## 4.0.13 + +Wed, 18 Jun 2025 15:21:55 GMT + +### Patches + +- Bump @azure/msal-browser to v4.13.2 +- Bump eslint-config-msal to v0.0.0 + +## 4.0.12 + +Tue, 06 May 2025 22:47:43 GMT + +### Patches + +- Bump @azure/msal-browser to v4.12.0 +- Bump eslint-config-msal to v0.0.0 + +## 4.0.11 + +Tue, 29 Apr 2025 20:25:48 GMT + +### Patches + +- Fix circular dependency (thomas.norling@microsoft.com) +- Bump @azure/msal-browser to v4.11.1 +- Bump eslint-config-msal to v0.0.0 + +## 4.0.10 + +Tue, 15 Apr 2025 23:34:14 GMT + +### Patches + +- Bump @azure/msal-browser to v4.11.0 +- Bump eslint-config-msal to v0.0.0 + +## 4.0.9 + +Tue, 08 Apr 2025 16:56:05 GMT + +### Patches + +- added resetInProgressEvent function to reset event after handleRedirectPromise is called #7682 (lalimasharda@microsoft.com) +- Deprecate logout() in Angular (shylasummers@microsoft.com) +- Bump @azure/msal-browser to v4.10.0 +- Bump eslint-config-msal to v0.0.0 + +## 4.0.8 + +Tue, 25 Mar 2025 22:29:34 GMT + +### Patches + +- Fix request type for ssoSilent (thomas.norling@microsoft.com) +- Bump @azure/msal-browser to v4.9.0 +- Bump eslint-config-msal to v0.0.0 + +## 4.0.7 + +Thu, 20 Mar 2025 22:09:04 GMT + +### Patches + +- Bump @azure/msal-browser to v4.8.0 +- Bump eslint-config-msal to v0.0.0 + ## 4.0.6 Tue, 11 Mar 2025 18:51:25 GMT diff --git a/lib/msal-angular/FAQ.md b/lib/msal-angular/FAQ.md index 297360c0ae..9890d19d03 100644 --- a/lib/msal-angular/FAQ.md +++ b/lib/msal-angular/FAQ.md @@ -44,7 +44,7 @@ Please see [here](https://github.com/AzureAD/microsoft-authentication-library-fo ### What versions of Angular are supported? -MSAL Angular v4 currently supports Angular 15, 16, 17, 18, and 19. +MSAL Angular v4 currently supports Angular 15, 16, 17, 18, 19 and 20. MSAL Angular v3 supports Angular 15, 16, 17 and 18. diff --git a/lib/msal-angular/README.md b/lib/msal-angular/README.md index ab0211e0d6..26d6d54c3b 100644 --- a/lib/msal-angular/README.md +++ b/lib/msal-angular/README.md @@ -46,7 +46,7 @@ At a minimum, `@azure/msal-angular` will follow the [support schedule of the mai | MSAL Angular version | MSAL support status | Supported Angular versions | | -------------------- | ------------------- | -------------------------- | -| MSAL Angular v4 | Active development | 15, 16, 17, 18, 19 | +| MSAL Angular v4 | Active development | 15, 16, 17, 18, 19, 20 | | MSAL Angular v3 | In maintenance | 15, 16, 17, 18 | | MSAL Angular v2 | In maintenance | 9, 10, 11, 12, 13, 14 | | MSAL Angular v1 | In maintenance | 6, 7, 8, 9 | diff --git a/lib/msal-angular/docs/configuration.md b/lib/msal-angular/docs/configuration.md index 0468194f93..ac12cecc6b 100644 --- a/lib/msal-angular/docs/configuration.md +++ b/lib/msal-angular/docs/configuration.md @@ -58,7 +58,7 @@ import { PublicClientApplication, InteractionType, BrowserCacheLocation } from " }, cache: { cacheLocation: BrowserCacheLocation.LocalStorage, - storeAuthStateInCookie: true, // set to true for IE 11 + storeAuthStateInCookie: true, // Deprecated, will be removed in the next major version }, system: { loggerOptions: { @@ -165,7 +165,7 @@ export class AppModule {} ## platformBrowserDynamic -If you need to dynamically configure MSAL Angular (e.g. based on values returned from an API), you can use `platformBrowserDynamic`. `platformBrowserDyamic` is a platform factory, used to bootstrap the application, and is able to take in configuration options. `platformBrowserDynamic` should already be present when the Angular application is set up. +If you need to dynamically configure MSAL Angular (e.g. based on values returned from an API), you can use `platformBrowserDynamic`. `platformBrowserDynamic` is a platform factory, used to bootstrap the application, and is able to take in configuration options. `platformBrowserDynamic` should already be present when the Angular application is set up. The following is an example of how to dynamically configure `@azure/msal-angular` with `platformBrowserDynamic` and a json file: diff --git a/lib/msal-angular/docs/initialization.md b/lib/msal-angular/docs/initialization.md index a8b2f7e1e7..933056e374 100644 --- a/lib/msal-angular/docs/initialization.md +++ b/lib/msal-angular/docs/initialization.md @@ -31,7 +31,7 @@ import { PublicClientApplication, InteractionType, BrowserCacheLocation } from " }, cache: { cacheLocation : BrowserCacheLocation.LocalStorage, - storeAuthStateInCookie: true, // set to true for IE 11 + storeAuthStateInCookie: true, // Deprecated, will be removed in future version }, system: { loggerOptions: { @@ -115,7 +115,7 @@ import { PublicClientApplication, InteractionType, BrowserCacheLocation } from " }, cache: { cacheLocation : BrowserCacheLocation.LocalStorage, - storeAuthStateInCookie: true, // set to true for IE 11 + storeAuthStateInCookie: true, // Deprecated, will be removed in future version }, system: { loggerOptions: { diff --git a/lib/msal-angular/karma.conf.js b/lib/msal-angular/karma.conf.js index 52cf3da0d2..5c0544b82e 100644 --- a/lib/msal-angular/karma.conf.js +++ b/lib/msal-angular/karma.conf.js @@ -18,7 +18,8 @@ module.exports = function (config) { jasmine: { failSpecWithNoExpectations: true }, - clearContext: false // leave Jasmine Spec Runner output visible in browser + clearContext: false, // leave Jasmine Spec Runner output visible in browser + captureConsole: true // turns on console logging for test debugging }, coverageIstanbulReporter: { dir: require("path").join(__dirname, "./coverage"), diff --git a/lib/msal-angular/package.json b/lib/msal-angular/package.json index ab13a32bb0..57aa53ba76 100644 --- a/lib/msal-angular/package.json +++ b/lib/msal-angular/package.json @@ -1,6 +1,6 @@ { "name": "@azure/msal-angular", - "version": "4.0.6", + "version": "4.0.15", "author": { "name": "Microsoft", "email": "nugetaad@microsoft.com", @@ -17,8 +17,7 @@ "start": "ng serve", "build": "ng build", "build:prod": "ng build --configuration production", - "build:all": "npm run build:browser && npm run build", - "build:all:prod": "npm run build:browser && npm run build:prod", + "build:all": "npm run build --workspace=@azure/msal-common --workspace=@azure/msal-browser && ng build --configuration production", "build:browser": "cd ../.. && npm run build --workspace=@azure/msal-common --workspace=@azure/msal-browser", "build:pack": "npm run build && cd dist && npm pack", "deploy": "npm run build:all:prod && npm publish ./dist --workspaces=false", @@ -50,7 +49,7 @@ "@angular/platform-browser": "^15.1.4", "@angular/platform-browser-dynamic": "^15.1.4", "@angular/router": "^15.1.4", - "@azure/msal-browser": "^4.7.0", + "@azure/msal-browser": "^4.15.0", "@types/jasmine": "~3.6.0", "@types/jasminewd2": "~2.0.3", "@types/node": "^12.11.1", @@ -71,7 +70,7 @@ "zone.js": "~0.11.8" }, "peerDependencies": { - "@azure/msal-browser": "^4.7.0", + "@azure/msal-browser": "^4.15.0", "rxjs": "^7.0.0" } } diff --git a/lib/msal-angular/src/IMsalService.ts b/lib/msal-angular/src/IMsalService.ts index 7a798d43a8..2b86eac1b8 100644 --- a/lib/msal-angular/src/IMsalService.ts +++ b/lib/msal-angular/src/IMsalService.ts @@ -5,12 +5,12 @@ import { EndSessionRequest, - AuthorizationUrlRequest, AuthenticationResult, PopupRequest, RedirectRequest, SilentRequest, Logger, + SsoSilentRequest, } from "@azure/msal-browser"; import { Observable } from "rxjs"; @@ -24,8 +24,11 @@ export interface IMsalService { handleRedirectObservable(): Observable; loginPopup(request?: PopupRequest): Observable; loginRedirect(request?: RedirectRequest): Observable; + // @deprecated: Use logoutRedirect or logoutPopup logout(logoutRequest?: EndSessionRequest): Observable; - ssoSilent(request: AuthorizationUrlRequest): Observable; + logoutRedirect(logoutRequest?: EndSessionRequest): Observable; + logoutPopup(logoutRequest?: EndSessionRequest): Observable; + ssoSilent(request: SsoSilentRequest): Observable; getLogger(): Logger; setLogger(logger: Logger): void; } diff --git a/lib/msal-angular/src/msal.broadcast.service.spec.ts b/lib/msal-angular/src/msal.broadcast.service.spec.ts index 89ec85abea..589791d477 100644 --- a/lib/msal-angular/src/msal.broadcast.service.spec.ts +++ b/lib/msal-angular/src/msal.broadcast.service.spec.ts @@ -40,6 +40,7 @@ function initializeMsal(providers: any[] = []) { }), ], providers: [MsalBroadcastService, ...providers], + teardown: { destroyAfterEach: false }, }); broadcastService = TestBed.inject(MsalBroadcastService); } @@ -442,4 +443,29 @@ describe("MsalBroadcastService", () => { InteractionType.Redirect ); }); + + it("automatically sets inProgress to None when handleRedirectPromise returns without emitting an event", (done) => { + const expectedInProgress = [ + InteractionStatus.Startup, + InteractionStatus.None, + ]; + let index = 0; + + subscription = broadcastService.inProgress$.subscribe((result) => { + expect(result).toEqual(expectedInProgress[index]); + if (index === expectedInProgress.length - 1) { + done(); + } else if (expectedInProgress[index] === InteractionStatus.Startup) { + index++; + broadcastService.resetInProgressEvent(); + } else { + index++; + } + }); + + eventHandler.emitEvent( + EventType.INITIALIZE_START, + InteractionType.Redirect + ); + }); }); diff --git a/lib/msal-angular/src/msal.broadcast.service.ts b/lib/msal-angular/src/msal.broadcast.service.ts index fbe2f6a710..30116b13f0 100644 --- a/lib/msal-angular/src/msal.broadcast.service.ts +++ b/lib/msal-angular/src/msal.broadcast.service.ts @@ -11,9 +11,9 @@ import { InteractionStatus, } from "@azure/msal-browser"; import { BehaviorSubject, Observable, ReplaySubject, Subject } from "rxjs"; -import { MsalService } from "./msal.service"; import { MsalBroadcastConfiguration } from "./msal.broadcast.config"; import { MSAL_BROADCAST_CONFIG, MSAL_INSTANCE } from "./constants"; +import { name, version } from "./packageMetadata"; @Injectable() export class MsalBroadcastService { @@ -24,7 +24,6 @@ export class MsalBroadcastService { constructor( @Inject(MSAL_INSTANCE) private msalInstance: IPublicClientApplication, - private authService: MsalService, @Optional() @Inject(MSAL_BROADCAST_CONFIG) private msalBroadcastConfig?: MsalBroadcastConfiguration @@ -34,8 +33,9 @@ export class MsalBroadcastService { this.msalBroadcastConfig && this.msalBroadcastConfig.eventsToReplay > 0 ) { - this.authService + this.msalInstance .getLogger() + .clone(name, version) .verbose( `BroadcastService - eventsToReplay set on BroadcastConfig, replaying the last ${this.msalBroadcastConfig.eventsToReplay} events` ); @@ -62,8 +62,9 @@ export class MsalBroadcastService { this._inProgress.value ); if (status !== null) { - this.authService + this.msalInstance .getLogger() + .clone(name, version) .verbose( `BroadcastService - ${message.eventType} results in setting inProgress from ${this._inProgress.value} to ${status}` ); @@ -71,4 +72,13 @@ export class MsalBroadcastService { } }); } + + /** + * Resets inProgress state to None + */ + resetInProgressEvent(): void { + if (this._inProgress.value === InteractionStatus.Startup) { + this._inProgress.next(InteractionStatus.None); + } + } } diff --git a/lib/msal-angular/src/msal.guard.spec.ts b/lib/msal-angular/src/msal.guard.spec.ts index 4f4087862f..a201d2c7af 100644 --- a/lib/msal-angular/src/msal.guard.spec.ts +++ b/lib/msal-angular/src/msal.guard.spec.ts @@ -68,6 +68,7 @@ function initializeMsal(providers: any[] = []) { RouterTestingModule.withRoutes([]), ], providers: [MsalGuard, MsalService, MsalBroadcastService, ...providers], + teardown: { destroyAfterEach: false }, }); authService = TestBed.inject(MsalService); diff --git a/lib/msal-angular/src/msal.interceptor.spec.ts b/lib/msal-angular/src/msal.interceptor.spec.ts index 4d61d74aff..7d5ae07281 100644 --- a/lib/msal-angular/src/msal.interceptor.spec.ts +++ b/lib/msal-angular/src/msal.interceptor.spec.ts @@ -124,6 +124,7 @@ function initializeMsal() { }, Location, ], + teardown: { destroyAfterEach: false }, }); interceptor = TestBed.inject(MsalInterceptor); diff --git a/lib/msal-angular/src/msal.navigation.client.spec.ts b/lib/msal-angular/src/msal.navigation.client.spec.ts index af121ab1b9..df1c2c7b2c 100644 --- a/lib/msal-angular/src/msal.navigation.client.spec.ts +++ b/lib/msal-angular/src/msal.navigation.client.spec.ts @@ -26,7 +26,7 @@ const msalInstance = new PublicClientApplication({ }); describe("MsalCustomNaviationClient", () => { - beforeAll(() => { + beforeEach(() => { TestBed.resetTestingModule(); TestBed.configureTestingModule({ @@ -44,6 +44,7 @@ describe("MsalCustomNaviationClient", () => { MsalService, MsalGuard, ], + teardown: { destroyAfterEach: false }, }); authService = TestBed.inject(MsalService); navigationClient = TestBed.inject(MsalCustomNavigationClient); diff --git a/lib/msal-angular/src/msal.redirect.component.spec.ts b/lib/msal-angular/src/msal.redirect.component.spec.ts index c62dfe129c..532520d91a 100644 --- a/lib/msal-angular/src/msal.redirect.component.spec.ts +++ b/lib/msal-angular/src/msal.redirect.component.spec.ts @@ -27,6 +27,7 @@ function initializeMsal() { declarations: [MsalRedirectComponent], imports: [MsalModule.forRoot(MSALInstanceFactory(), null, null)], providers: [], + teardown: { destroyAfterEach: false }, }); authService = TestBed.inject(MsalService); @@ -34,7 +35,7 @@ function initializeMsal() { } describe("MsalRedirectComponent", () => { - beforeAll(initializeMsal); + beforeEach(initializeMsal); it("calls handleRedirectObservable on ngInit", (done) => { const sampleAccessToken = { diff --git a/lib/msal-angular/src/msal.service.spec.ts b/lib/msal-angular/src/msal.service.spec.ts index ebe680c4fb..087637c1c7 100644 --- a/lib/msal-angular/src/msal.service.spec.ts +++ b/lib/msal-angular/src/msal.service.spec.ts @@ -2,12 +2,14 @@ import { TestBed } from "@angular/core/testing"; import { AuthenticationResult, AuthError, + InteractionStatus, InteractionType, Logger, PublicClientApplication, SilentRequest, } from "@azure/msal-browser"; import { MsalModule, MsalBroadcastService, MsalService } from "./public-api"; +import { takeLast } from "rxjs/operators"; let authService: MsalService; let broadcastService: MsalBroadcastService; @@ -30,6 +32,7 @@ function initializeMsal() { }), ], providers: [MsalService, MsalBroadcastService], + teardown: { destroyAfterEach: false }, }); authService = TestBed.inject(MsalService); @@ -37,7 +40,7 @@ function initializeMsal() { } describe("MsalService", () => { - beforeAll(initializeMsal); + beforeEach(initializeMsal); describe("loginPopup", () => { it("success", (done) => { @@ -198,7 +201,7 @@ describe("MsalService", () => { authService.ssoSilent(request).subscribe({ error: (error: AuthError) => { - expect(error.message).toBe(sampleError.message); + expect(error.errorMessage).toBe(sampleError.errorMessage); expect( PublicClientApplication.prototype.ssoSilent ).toHaveBeenCalledWith(request); @@ -259,7 +262,7 @@ describe("MsalService", () => { authService.acquireTokenSilent(request).subscribe({ error: (error: AuthError) => { - expect(error.message).toBe(sampleError.message); + expect(error.errorMessage).toBe(sampleError.errorMessage); expect( PublicClientApplication.prototype.acquireTokenSilent ).toHaveBeenCalledWith(request); @@ -339,7 +342,7 @@ describe("MsalService", () => { authService.acquireTokenPopup(request).subscribe({ error: (error: AuthError) => { - expect(error.message).toBe(sampleError.message); + expect(error.errorMessage).toBe(sampleError.errorMessage); expect( PublicClientApplication.prototype.acquireTokenPopup ).toHaveBeenCalledWith(request); @@ -350,11 +353,14 @@ describe("MsalService", () => { }); describe("handleRedirectObservable", () => { - it("success", (done) => { + it("success and resets inProgress event to none", (done) => { const sampleAccessToken = { accessToken: "123abc", }; + //@ts-ignore + broadcastService._inProgress.next(InteractionStatus.Startup); + spyOn(PublicClientApplication.prototype, "initialize").and.returnValue( Promise.resolve() ); @@ -379,13 +385,19 @@ describe("MsalService", () => { expect( PublicClientApplication.prototype.handleRedirectPromise ).toHaveBeenCalled(); + broadcastService.inProgress$.subscribe((result) => { + expect(result).toBe(InteractionStatus.None); + }); done(); }); }); - it("failure", (done) => { + it("failure and also resets inProgress event to none", (done) => { const sampleError = new AuthError("123", "message"); + //@ts-ignore + broadcastService._inProgress.next(InteractionStatus.Startup); + spyOn(PublicClientApplication.prototype, "initialize").and.returnValue( Promise.resolve() ); @@ -408,6 +420,10 @@ describe("MsalService", () => { expect( PublicClientApplication.prototype.handleRedirectPromise ).toHaveBeenCalled(); + broadcastService.inProgress$.pipe(takeLast(1)).subscribe((result) => { + console.log("failure result", result); + expect(result).toBe(InteractionStatus.None); + }); done(); }, }); diff --git a/lib/msal-angular/src/msal.service.ts b/lib/msal-angular/src/msal.service.ts index a190da55a8..6a440d14dd 100644 --- a/lib/msal-angular/src/msal.service.ts +++ b/lib/msal-angular/src/msal.service.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { Inject, Injectable } from "@angular/core"; +import { Inject, Injectable, Injector } from "@angular/core"; import { Location } from "@angular/common"; import { IPublicClientApplication, @@ -21,6 +21,7 @@ import { Observable, from } from "rxjs"; import { IMsalService } from "./IMsalService"; import { name, version } from "./packageMetadata"; import { MSAL_INSTANCE } from "./constants"; +import { MsalBroadcastService } from "./msal.broadcast.service"; @Injectable() export class MsalService implements IMsalService { @@ -29,7 +30,8 @@ export class MsalService implements IMsalService { constructor( @Inject(MSAL_INSTANCE) public instance: IPublicClientApplication, - private location: Location + private location: Location, + private injector: Injector ) { const hash = this.location.path(true).split("#").pop(); if (hash) { @@ -59,6 +61,10 @@ export class MsalService implements IMsalService { .then(() => this.instance.handleRedirectPromise(hash || this.redirectHash) ) + .finally(() => { + // update inProgress state to none + this.injector.get(MsalBroadcastService).resetInProgressEvent(); + }) ); } loginPopup(request?: PopupRequest): Observable { @@ -67,6 +73,7 @@ export class MsalService implements IMsalService { loginRedirect(request?: RedirectRequest): Observable { return from(this.instance.loginRedirect(request)); } + // @deprecated: Use logoutRedirect or logoutPopup logout(logoutRequest?: EndSessionRequest): Observable { return from(this.instance.logout(logoutRequest)); } diff --git a/lib/msal-angular/src/packageMetadata.ts b/lib/msal-angular/src/packageMetadata.ts index 4a3eb057da..7f68186cc0 100644 --- a/lib/msal-angular/src/packageMetadata.ts +++ b/lib/msal-angular/src/packageMetadata.ts @@ -1,3 +1,3 @@ /* eslint-disable header/header */ export const name = "@azure/msal-angular"; -export const version = "4.0.6"; +export const version = "4.0.15"; diff --git a/lib/msal-browser/CHANGELOG.json b/lib/msal-browser/CHANGELOG.json index ac299a61d9..126ae22cc0 100644 --- a/lib/msal-browser/CHANGELOG.json +++ b/lib/msal-browser/CHANGELOG.json @@ -1,6 +1,598 @@ { "name": "@azure/msal-browser", "entries": [ + { + "date": "Tue, 08 Jul 2025 14:21:12 GMT", + "version": "4.15.0", + "tag": "@azure/msal-browser_v4.15.0", + "comments": { + "minor": [ + { + "author": "msaljsbuilds@microsoft.com", + "package": "@azure/msal-browser", + "commit": "2e52c24168403e6ca4aab48db4b45013a51c6d94", + "comment": "Bump @azure/msal-browser to match @azure/msal-browser-1p" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump @azure/msal-common to v15.8.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ], + "none": [ + { + "author": "shen.jian@live.com", + "package": "@azure/msal-browser", + "commit": "aab16ab730aa302042f8d68422391497816ec4b7", + "comment": "Improve the custom auth unit tests" + } + ] + } + }, + { + "date": "Tue, 01 Jul 2025 14:23:25 GMT", + "version": "4.14.0", + "tag": "@azure/msal-browser_v4.14.0", + "comments": { + "minor": [ + { + "author": "shen.jian@live.com", + "package": "@azure/msal-browser", + "commit": "b2942e458324c8bf767a068cae3eccbd4863baf3", + "comment": "Add native authentication feaetures for the external ID" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "b2942e458324c8bf767a068cae3eccbd4863baf3", + "comment": "Remove access tokens when cache quota is exceeded #7819" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump @azure/msal-common to v15.8.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ], + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "b2942e458324c8bf767a068cae3eccbd4863baf3", + "comment": "Track cache lastUpdatedAt timestamp and stop removing cache entries in get calls" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "b2942e458324c8bf767a068cae3eccbd4863baf3", + "comment": "Throw timeout error in acquireTokenRedirect if the redirect doesn't happen" + } + ] + } + }, + { + "date": "Wed, 18 Jun 2025 15:21:55 GMT", + "version": "4.13.2", + "tag": "@azure/msal-browser_v4.13.2", + "comments": { + "patch": [ + { + "author": "lalimasharda@microsoft.com", + "package": "@azure/msal-browser", + "commit": "32c6a1bd1f2a8d2da6a1b0ad69a5fd9c7e4107c4", + "comment": "added UX_NOT_ALLOWED suberror to InteractionRequired error type #7834" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "32c6a1bd1f2a8d2da6a1b0ad69a5fd9c7e4107c4", + "comment": "remove access tokens synchronously" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump @azure/msal-common to v15.7.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 10 Jun 2025 20:38:38 GMT", + "version": "4.13.1", + "tag": "@azure/msal-browser_v4.13.1", + "comments": { + "patch": [ + { + "author": "kshabelko@microsoft.com", + "package": "@azure/msal-browser", + "commit": "75f62379475a5084ddc9dfe1efa436ad3a78024b", + "comment": "\u0016Suppress false-positive CodeQL finding in NavigationClient #7814" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Fri, 30 May 2025 22:36:44 GMT", + "version": "4.13.0", + "tag": "@azure/msal-browser_v4.13.0", + "comments": { + "patch": [ + { + "author": "shylasummers@microsoft.com", + "package": "@azure/msal-browser", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "update common version" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "Add BrokerConnectionEvent to exports" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "Upgrade/rollback telemetry #7738" + }, + { + "author": "lalimasharda@microsoft.com", + "package": "@azure/msal-browser", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "updated naming for PlatformAuth request and response objects and added new type for DOM extraParameters #7759" + } + ], + "minor": [ + { + "author": "lalimasharda@microsoft.com", + "package": "@azure/msal-browser", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "Add support for new platform broker flow via DOM API #7632" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 06 May 2025 22:47:42 GMT", + "version": "4.12.0", + "tag": "@azure/msal-browser_v4.12.0", + "comments": { + "none": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "b6098464c80f3db124ca3db145446ddb71be936d", + "comment": "New dev dependency" + } + ], + "patch": [ + { + "author": "kshabelko@microsoft.com", + "package": "@azure/msal-browser", + "commit": "b6098464c80f3db124ca3db145446ddb71be936d", + "comment": "Gracefully handle old interaction format #7731" + } + ], + "minor": [ + { + "author": "shylasummers@microsoft.com", + "package": "@azure/msal-browser", + "commit": "b6098464c80f3db124ca3db145446ddb71be936d", + "comment": "Multi-instance detection" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump @azure/msal-common to v15.6.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 29 Apr 2025 20:25:47 GMT", + "version": "4.11.1", + "tag": "@azure/msal-browser_v4.11.1", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "2b6112d62495ac4e3245d8a0d9be6b541ffb206c", + "comment": "Fix throw when attempting to getAccount before initialization #7720" + }, + { + "author": "sameera.gajjarapu@microsoft.com", + "package": "@azure/msal-browser", + "commit": "2b6112d62495ac4e3245d8a0d9be6b541ffb206c", + "comment": "Add error messaging and change type for get/post failures #7721" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "2b6112d62495ac4e3245d8a0d9be6b541ffb206c", + "comment": "Fix circular type imports" + }, + { + "author": "joarroyo@microsoft.com", + "package": "@azure/msal-browser", + "commit": "2b6112d62495ac4e3245d8a0d9be6b541ffb206c", + "comment": "Deprecate cache options #7707" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump @azure/msal-common to v15.5.2", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 15 Apr 2025 23:34:14 GMT", + "version": "4.11.0", + "tag": "@azure/msal-browser_v4.11.0", + "comments": { + "minor": [ + { + "author": "msaljsbuilds@microsoft.com", + "package": "@azure/msal-browser", + "commit": "e6fd8893c06cdf9f4e30fbb858c31a0338a17de4", + "comment": "Bump @azure/msal-browser to match @azure/msal-browser-1p" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump @azure/msal-common to v15.5.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ], + "patch": [ + { + "author": "shylasummers@microsoft.com", + "package": "@azure/msal-browser", + "commit": "66a6372ce1e4f80992e49d2c153628ad0415b71f", + "comment": "Add config option to not encode extra params" + } + ] + } + }, + { + "date": "Tue, 08 Apr 2025 16:56:05 GMT", + "version": "4.10.0", + "tag": "@azure/msal-browser_v4.10.0", + "comments": { + "minor": [ + { + "author": "kshabelko@microsoft.com", + "package": "@azure/msal-browser", + "commit": "fabf962dcb69611b4516342107bc7bcc364c4e68", + "comment": "Fix a bug in handleRedirectPromise when invoked after logoutRedirect #7680" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump @azure/msal-common to v15.5.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ], + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "fabf962dcb69611b4516342107bc7bcc364c4e68", + "comment": "Export additional function internally for use in tests" + }, + { + "author": "sameera.gajjarapu@microsoft.com", + "package": "@azure/msal-browser", + "commit": "fabf962dcb69611b4516342107bc7bcc364c4e68", + "comment": "update cache miss to warning from error" + } + ] + } + }, + { + "date": "Wed, 26 Mar 2025 17:56:52 GMT", + "version": "4.9.1", + "tag": "@azure/msal-browser_v4.9.1", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "90b28e7d5df871fee6ccd85feebfd8c1cc7ebae7", + "comment": "remove EAR override" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 25 Mar 2025 22:29:34 GMT", + "version": "4.9.0", + "tag": "@azure/msal-browser_v4.9.0", + "comments": { + "minor": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "8bbed7012616d9106c923d08b5c4e408a741577f", + "comment": "EAR Protocol response handling" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "8bbed7012616d9106c923d08b5c4e408a741577f", + "comment": "EAR protocol request support" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "8bbed7012616d9106c923d08b5c4e408a741577f", + "comment": "refactor redirect request caching" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump @azure/msal-common to v15.4.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Thu, 20 Mar 2025 22:09:04 GMT", + "version": "4.8.0", + "tag": "@azure/msal-browser_v4.8.0", + "comments": { + "patch": [ + { + "author": "sameera.gajjarapu@microsoft.com", + "package": "@azure/msal-browser", + "commit": "0de0f206568d7f075d12177172d108efc01c9fab", + "comment": "Fix expires_in format & double brokering errors #7646" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "0de0f206568d7f075d12177172d108efc01c9fab", + "comment": "refactor RequestParameterBuilder" + } + ], + "minor": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-browser", + "commit": "0de0f206568d7f075d12177172d108efc01c9fab", + "comment": "Refactor /authorize request generation" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump @azure/msal-common to v15.3.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-browser", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, { "date": "Tue, 11 Mar 2025 18:51:24 GMT", "version": "4.7.0", diff --git a/lib/msal-browser/CHANGELOG.md b/lib/msal-browser/CHANGELOG.md index eec1a39c64..a527c87d9a 100644 --- a/lib/msal-browser/CHANGELOG.md +++ b/lib/msal-browser/CHANGELOG.md @@ -1,9 +1,187 @@ # Change Log - @azure/msal-browser - + +## 4.15.0 + +Tue, 08 Jul 2025 14:21:12 GMT + +### Minor changes + +- Bump @azure/msal-browser to match @azure/msal-browser-1p (msaljsbuilds@microsoft.com) +- Bump @azure/msal-common to v15.8.1 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 4.14.0 + +Tue, 01 Jul 2025 14:23:25 GMT + +### Minor changes + +- Add native authentication feaetures for the external ID (shen.jian@live.com) +- Remove access tokens when cache quota is exceeded #7819 (thomas.norling@microsoft.com) +- Bump @azure/msal-common to v15.8.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +### Patches + +- Track cache lastUpdatedAt timestamp and stop removing cache entries in get calls (thomas.norling@microsoft.com) +- Throw timeout error in acquireTokenRedirect if the redirect doesn't happen (thomas.norling@microsoft.com) + +## 4.13.2 + +Wed, 18 Jun 2025 15:21:55 GMT + +### Patches + +- added UX_NOT_ALLOWED suberror to InteractionRequired error type #7834 (lalimasharda@microsoft.com) +- remove access tokens synchronously (thomas.norling@microsoft.com) +- Bump @azure/msal-common to v15.7.1 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 4.13.1 + +Tue, 10 Jun 2025 20:38:38 GMT + +### Patches + +- Suppress false-positive CodeQL finding in NavigationClient #7814 (kshabelko@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 4.13.0 + +Fri, 30 May 2025 22:36:44 GMT + +### Minor changes + +- Add support for new platform broker flow via DOM API #7632 (lalimasharda@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +### Patches + +- update common version (shylasummers@microsoft.com) +- Add BrokerConnectionEvent to exports (thomas.norling@microsoft.com) +- Upgrade/rollback telemetry #7738 (thomas.norling@microsoft.com) +- updated naming for PlatformAuth request and response objects and added new type for DOM extraParameters #7759 (lalimasharda@microsoft.com) + +## 4.12.0 + +Tue, 06 May 2025 22:47:42 GMT + +### Minor changes + +- Multi-instance detection (shylasummers@microsoft.com) +- Bump @azure/msal-common to v15.6.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +### Patches + +- Gracefully handle old interaction format #7731 (kshabelko@microsoft.com) + +## 4.11.1 + +Tue, 29 Apr 2025 20:25:47 GMT + +### Patches + +- Fix throw when attempting to getAccount before initialization #7720 (thomas.norling@microsoft.com) +- Add error messaging and change type for get/post failures #7721 (sameera.gajjarapu@microsoft.com) +- Fix circular type imports (thomas.norling@microsoft.com) +- Deprecate cache options #7707 (joarroyo@microsoft.com) +- Bump @azure/msal-common to v15.5.2 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 4.11.0 + +Tue, 15 Apr 2025 23:34:14 GMT + +### Minor changes + +- Bump @azure/msal-browser to match @azure/msal-browser-1p (msaljsbuilds@microsoft.com) +- Bump @azure/msal-common to v15.5.1 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +### Patches + +- Add config option to not encode extra params (shylasummers@microsoft.com) + +## 4.10.0 + +Tue, 08 Apr 2025 16:56:05 GMT + +### Minor changes + +- Fix a bug in handleRedirectPromise when invoked after logoutRedirect #7680 (kshabelko@microsoft.com) +- Bump @azure/msal-common to v15.5.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +### Patches + +- Export additional function internally for use in tests (thomas.norling@microsoft.com) +- update cache miss to warning from error (sameera.gajjarapu@microsoft.com) + +## 4.9.1 + +Wed, 26 Mar 2025 17:56:52 GMT + +### Patches + +- remove EAR override (thomas.norling@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 4.9.0 + +Tue, 25 Mar 2025 22:29:34 GMT + +### Minor changes + +- EAR Protocol response handling (thomas.norling@microsoft.com) +- EAR protocol request support (thomas.norling@microsoft.com) +- refactor redirect request caching (thomas.norling@microsoft.com) +- Bump @azure/msal-common to v15.4.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 4.8.0 + +Thu, 20 Mar 2025 22:09:04 GMT + +### Minor changes + +- Refactor /authorize request generation (thomas.norling@microsoft.com) +- Bump @azure/msal-common to v15.3.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +### Patches + +- Fix expires_in format & double brokering errors #7646 (sameera.gajjarapu@microsoft.com) +- refactor RequestParameterBuilder (thomas.norling@microsoft.com) + ## 4.7.0 Tue, 11 Mar 2025 18:51:24 GMT diff --git a/lib/msal-browser/apiReview/msal-browser.api.md b/lib/msal-browser/apiReview/msal-browser.api.md index 7cf7a4522d..5e1585f7da 100644 --- a/lib/msal-browser/apiReview/msal-browser.api.md +++ b/lib/msal-browser/apiReview/msal-browser.api.md @@ -126,11 +126,8 @@ export type AuthorizationCodeRequest = Partial & { - state: string; - nonce: string; -}; +// @public @deprecated +export type AuthorizationUrlRequest = Omit; // Warning: (ae-missing-release-tag) "authRequestNotSetError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -180,6 +177,13 @@ function blockRedirectInIframe(allowRedirectInIframe: boolean): void; // @public function blockReloadInHiddenIframes(): void; +// Warning: (ae-missing-release-tag) "BrokerConnectionEvent" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type BrokerConnectionEvent = { + pairwiseBrokerOrigin: string; +}; + // Warning: (ae-missing-release-tag) "BrowserAuthError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -190,6 +194,8 @@ export class BrowserAuthError extends AuthError { declare namespace BrowserAuthErrorCodes { export { pkceNotCreated, + earJwkEmpty, + earJweEmpty, cryptoNonExistent, emptyNavigateUri, hashEmptyError, @@ -212,7 +218,6 @@ declare namespace BrowserAuthErrorCodes { silentPromptValueError, noTokenRequestCacheError, unableToParseTokenRequestCacheError, - noCachedAuthorityError, authRequestNotSetError, invalidCacheType, nonBrowserEnvironment, @@ -236,7 +241,9 @@ declare namespace BrowserAuthErrorCodes { invalidBase64String, invalidPopTokenRequest, failedToBuildHeaders, - failedToParseHeaders + failedToParseHeaders, + failedToDecryptEarResponse, + timedOut } } export { BrowserAuthErrorCodes } @@ -337,10 +344,6 @@ export const BrowserAuthErrorMessage: { code: string; desc: string; }; - noCachedAuthorityError: { - code: string; - desc: string; - }; authRequestNotSet: { code: string; desc: string; @@ -451,6 +454,7 @@ export type BrowserAuthOptions = { supportsNestedAppAuth?: boolean; onRedirectNavigate?: (url: string) => boolean | void; instanceAware?: boolean; + encodeExtraQueryParams?: boolean; }; // Warning: (ae-missing-release-tag) "BrowserCacheLocation" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -718,6 +722,16 @@ const databaseUnavailable = "database_unavailable"; // @public (undocumented) export const DEFAULT_IFRAME_TIMEOUT_MS = 10000; +// Warning: (ae-missing-release-tag) "earJweEmpty" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const earJweEmpty = "ear_jwe_empty"; + +// Warning: (ae-missing-release-tag) "earJwkEmpty" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const earJwkEmpty = "ear_jwk_empty"; + // Warning: (ae-missing-release-tag) "emptyNavigateUri" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -796,7 +810,7 @@ export class EventMessageUtils { // Warning: (ae-missing-release-tag) "EventPayload" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export type EventPayload = AccountInfo | PopupRequest | RedirectRequest | SilentRequest | SsoSilentRequest | EndSessionRequest | AuthenticationResult | PopupEvent | null; +export type EventPayload = AccountInfo | PopupRequest | RedirectRequest | SilentRequest | SsoSilentRequest | EndSessionRequest | AuthenticationResult | PopupEvent | BrokerConnectionEvent | null; // Warning: (ae-missing-release-tag) "EventType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // Warning: (ae-missing-release-tag) "EventType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -829,6 +843,7 @@ export const EventType: { readonly LOGOUT_FAILURE: "msal:logoutFailure"; readonly LOGOUT_END: "msal:logoutEnd"; readonly RESTORE_FROM_BFCACHE: "msal:restoreFromBFCache"; + readonly BROKER_CONNECTION_ESTABLISHED: "msal:brokerConnectionEstablished"; }; // @public (undocumented) @@ -841,6 +856,11 @@ export { ExternalTokenResponse } // @public (undocumented) const failedToBuildHeaders = "failed_to_build_headers"; +// Warning: (ae-missing-release-tag) "failedToDecryptEarResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const failedToDecryptEarResponse = "failed_to_decrypt_ear_response"; + // Warning: (ae-missing-release-tag) "failedToParseHeaders" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -925,7 +945,7 @@ export interface IController { // (undocumented) hydrateCache(result: AuthenticationResult, request: SilentRequest | SsoSilentRequest | RedirectRequest | PopupRequest): Promise; // (undocumented) - initialize(request?: InitializeApplicationRequest): Promise; + initialize(request?: InitializeApplicationRequest, isBroker?: boolean): Promise; // (undocumented) initializeWrapperLibrary(sku: WrapperSKU, version: string): void; // @internal (undocumented) @@ -1132,6 +1152,13 @@ function isInIframe(): boolean; // @public function isInPopup(): boolean; +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-missing-release-tag) "isPlatformBrokerAvailable" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function isPlatformBrokerAvailable(loggerOptions?: LoggerOptions, perfClient?: IPerformanceClient, correlationId?: string): Promise; + // Warning: (ae-missing-release-tag) "ITokenCache" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1155,7 +1182,7 @@ export interface IWindowStorage { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen setItem(key: string, value: T): void; - setUserData(key: string, value: T, correlationId: string): Promise; + setUserData(key: string, value: T, correlationId: string, timestamp: string): Promise; } export { JsonWebTokenTypes } @@ -1190,7 +1217,7 @@ export class LocalStorage implements IWindowStorage { // (undocumented) setItem(key: string, value: string): void; // (undocumented) - setUserData(key: string, value: string, correlationId: string): Promise; + setUserData(key: string, value: string, correlationId: string, timestamp: string): Promise; } export { Logger } @@ -1282,11 +1309,6 @@ export { NetworkResponse } // @public (undocumented) const noAccountError = "no_account_error"; -// Warning: (ae-missing-release-tag) "noCachedAuthorityError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -const noCachedAuthorityError = "no_cached_authority_error"; - // Warning: (ae-missing-release-tag) "nonBrowserEnvironment" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1338,7 +1360,7 @@ export type PopupPosition = { // Warning: (ae-missing-release-tag) "PopupRequest" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type PopupRequest = Partial> & { +export type PopupRequest = Partial> & { scopes: Array; popupWindowAttributes?: PopupWindowAttributes; popupWindowParent?: Window; @@ -1459,6 +1481,8 @@ export class PublicClientApplication implements IPublicClientApplication { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen initializeWrapperLibrary(sku: WrapperSKU, version: string): void; + // (undocumented) + protected isBroker: boolean; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen loginPopup(request?: PopupRequest | undefined): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -1595,7 +1619,7 @@ function redirectPreflightCheck(initialized: boolean, config: BrowserConfigurati // Warning: (ae-missing-release-tag) "RedirectRequest" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type RedirectRequest = Partial> & { +export type RedirectRequest = Partial> & { scopes: Array; redirectStartPage?: string; onRedirectNavigate?: (url: string) => boolean | void; @@ -1687,7 +1711,7 @@ const spaCodeAndNativeAccountIdPresent = "spa_code_and_nativeAccountId_present"; // Warning: (ae-missing-release-tag) "SsoSilentRequest" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export type SsoSilentRequest = Partial>; +export type SsoSilentRequest = Partial>; // Warning: (ae-missing-release-tag) "stateInteractionTypeMismatch" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -1715,6 +1739,11 @@ export { StubPerformanceClient } export { TenantProfile } +// Warning: (ae-missing-release-tag) "timedOut" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const timedOut = "timed_out"; + // Warning: (ae-missing-release-tag) "unableToAcquireTokenFromNativePlatform" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1750,7 +1779,7 @@ const userCancelled = "user_cancelled"; // Warning: (ae-missing-release-tag) "version" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const version = "4.7.0"; +export const version = "4.15.0"; // Warning: (ae-missing-release-tag) "WrapperSKU" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // Warning: (ae-missing-release-tag) "WrapperSKU" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -1777,15 +1806,15 @@ export type WrapperSKU = (typeof WrapperSKU)[keyof typeof WrapperSKU]; // src/app/PublicClientNext.ts:85:79 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" // src/app/PublicClientNext.ts:88:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/app/PublicClientNext.ts:89:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/LocalStorage.ts:296:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/LocalStorage.ts:354:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/LocalStorage.ts:385:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/config/Configuration.ts:247:5 - (ae-forgotten-export) The symbol "InternalAuthOptions" needs to be exported by the entry point index.d.ts +// src/cache/LocalStorage.ts:299:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/LocalStorage.ts:357:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/LocalStorage.ts:388:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/config/Configuration.ts:256:5 - (ae-forgotten-export) The symbol "InternalAuthOptions" needs to be exported by the entry point index.d.ts // src/event/EventHandler.ts:113:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/event/EventHandler.ts:139:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/index.ts:8:12 - (tsdoc-characters-after-block-tag) The token "@azure" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@" // src/index.ts:8:4 - (tsdoc-undefined-tag) The TSDoc tag "@module" is not defined in this configuration -// src/navigation/NavigationClient.ts:36:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/navigation/NavigationClient.ts:37:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/navigation/NavigationClient.ts:40:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/navigation/NavigationClient.ts:41:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen ``` diff --git a/lib/msal-browser/docs/caching.md b/lib/msal-browser/docs/caching.md index 0661f06c5b..34ae27b430 100644 --- a/lib/msal-browser/docs/caching.md +++ b/lib/msal-browser/docs/caching.md @@ -71,6 +71,9 @@ To faciliate efficient token acquisition while maintaining a good UX, MSAL cache > :bulb: The authorization code is only stored in memory and will be discarded after redeeming it for tokens. ## Warning :warning: + +**NOTE: `temporaryCacheLocation` is deprecated will be removed in the next major version.** + Overriding `temporaryCacheLocation` should be done with caution. Specifically when choosing `localStorage`. Interaction in more than one tab/window will not be supported and you may receive `interaction_in_progress` errors unexpectedly. This is an escape hatch, not a fully supported feature. When using MSAL.js with the default configuration in a scenario where the user is redirected after successful authentication in a new window or tab, the OAuth 2.0 Authorization Code with PKCE flow will be interrupted. In this case, the original window or tab where the authentication state (code verifier and challenge) are stored, will be lost, and the authentication flow will fail. diff --git a/lib/msal-browser/docs/configuration.md b/lib/msal-browser/docs/configuration.md index 62dda1c2fc..2969d33ce2 100644 --- a/lib/msal-browser/docs/configuration.md +++ b/lib/msal-browser/docs/configuration.md @@ -97,6 +97,11 @@ const msalInstance = new PublicClientApplication(msalConfig); | Option | Description | Format | Default Value | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | `cacheLocation` | Location of token cache in browser. | String value that must be one of the following: `"sessionStorage"`, `"localStorage"`, `"memoryStorage"` | `sessionStorage` | + +**Note: The following cache config options are deprecated and will be removed in the next major version** + +| Option | Description | Format | Default Value | +| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------- | --------------------------------------------------- | | `temporaryCacheLocation` | Location of temporary cache in browser. This option should only be changed for specific edge cases. Please refer to [caching](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/caching.md#cached-artifacts) for more. | String value that must be one of the following: `"sessionStorage"`, `"localStorage"`, `"memoryStorage"` | `sessionStorage` | | `storeAuthStateInCookie` | If true, stores cache items in cookies as well as browser cache. Should be set to true for use cases using IE. | boolean | `false` | | `secureCookies` | If true and `storeAuthStateInCookies` is also enabled, MSAL adds the `Secure` flag to the browser cookie so it can only be sent over HTTPS. | boolean | `false` | diff --git a/lib/msal-browser/docs/logout.md b/lib/msal-browser/docs/logout.md index 4fcfb506b5..4583a88741 100644 --- a/lib/msal-browser/docs/logout.md +++ b/lib/msal-browser/docs/logout.md @@ -119,7 +119,7 @@ await msalInstance.logoutRedirect({ account: currentAccount}); ### Option 2: Manually set the logoutHint option in the logout request -Alternatively, if you prefer to manually set the `logoutHint`, you can extract the `login_hint` claim in your app and set it as the `logoutHint` in the logout request: +Alternatively, if you prefer to manually set the `logoutHint`, you can extract the `login_hint` claim in your app and set it as the `logoutHint` in the logout request: ```javascript const currentAccount = msalInstance.getAccountByHomeId(homeAccountId); @@ -185,7 +185,7 @@ Events will be emitted when logout succeeds or fails and when the popup is opene ## Important Notes -- If no account is passed to the logout API, or no EndSessionRequest object, it will log out of all accounts. +- If no account is passed to the logout API, or no EndSessionRequest object, all accounts will be removed from the cache and the server will prompt the user to select the account they wish to sign out of. - If an account is passed to the logout API, MSAL will only clear tokens related to that account. - Server signout is a convenience feature and, as such, is done with best effort. Logout APIs will resolve successfully as long as the local application cache has been successfully cleared, regardless of whether or not server signout is successful. diff --git a/lib/msal-browser/package.json b/lib/msal-browser/package.json index 96d2ed3bbf..f836a52e00 100644 --- a/lib/msal-browser/package.json +++ b/lib/msal-browser/package.json @@ -10,7 +10,7 @@ "type": "git", "url": "https://github.com/AzureAD/microsoft-authentication-library-for-js.git" }, - "version": "4.7.0", + "version": "4.15.0", "description": "Microsoft Authentication Library for js", "keywords": [ "implicit", @@ -27,6 +27,16 @@ "module": "./dist/index.mjs", "types": "./dist/index.d.ts", "exports": { + "./custom-auth": { + "import": { + "types": "./dist/custom-auth-path/custom_auth/index.d.ts", + "default": "./dist/custom-auth-path/custom_auth/index.mjs" + }, + "require": { + "types": "./lib/custom-auth-path/types/custom_auth/index.d.ts", + "default": "./lib/custom-auth-path/msal-custom-auth.cjs" + } + }, ".": { "import": { "types": "./dist/index.d.ts", @@ -90,6 +100,7 @@ "fake-indexeddb": "^3.1.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "jest-junit": "^16.0.0", "msal-test-utils": "file:../../shared-test-utils", "prettier": "^2.8.7", "rimraf": "^3.0.0", @@ -103,6 +114,6 @@ "typescript": "^4.9.5" }, "dependencies": { - "@azure/msal-common": "15.2.1" + "@azure/msal-common": "15.8.1" } } diff --git a/lib/msal-browser/rollup.config.js b/lib/msal-browser/rollup.config.js index 0febd37288..72a2c9001e 100644 --- a/lib/msal-browser/rollup.config.js +++ b/lib/msal-browser/rollup.config.js @@ -17,7 +17,7 @@ const fileHeader = `${libraryHeader}\n${useStrictHeader}`; export default [ { - // for es build + // Main SDK - ES build input: "src/index.ts", output: { dir: "dist", @@ -32,17 +32,16 @@ export default [ moduleSideEffects: false, propertyReadSideEffects: false, }, - external: [ - "@azure/msal-common/browser" - ], + external: ["@azure/msal-common/browser"], plugins: [ typescript({ typescript: require("typescript"), tsconfig: "tsconfig.build.json", - }) + }), ], }, { + // Main SDK - CommonJS build input: "src/index.ts", output: [ { @@ -65,10 +64,11 @@ export default [ sourceMap: true, compilerOptions: { outDir: "lib/types" }, }), - createPackageJson({libPath: __dirname}) + createPackageJson({ libPath: __dirname }), ], }, { + // Main SDK - UMD build input: "src/index.ts", output: [ { @@ -90,12 +90,16 @@ export default [ typescript: require("typescript"), tsconfig: "tsconfig.build.json", sourceMap: true, - compilerOptions: { outDir: "lib/types", declaration: false, declarationMap: false }, + compilerOptions: { + outDir: "lib/types", + declaration: false, + declarationMap: false, + }, }), ], }, { - // Minified version of msal + // Main SDK - UMD minified build input: "src/index.ts", output: [ { @@ -117,7 +121,11 @@ export default [ typescript: require("typescript"), tsconfig: "tsconfig.build.json", sourceMap: false, - compilerOptions: { outDir: "lib/types", declaration: false, declarationMap: false }, + compilerOptions: { + outDir: "lib/types", + declaration: false, + declarationMap: false, + }, }), terser({ output: { @@ -126,4 +134,52 @@ export default [ }), ], }, + { + // Custom Auth - ES module build + input: "src/custom_auth/index.ts", + output: { + dir: "dist/custom-auth-path", + preserveModules: true, + preserveModulesRoot: "src", + format: "es", + entryFileNames: "[name].mjs", + banner: fileHeader, + sourcemap: true, + }, + treeshake: { + moduleSideEffects: false, + propertyReadSideEffects: false, + }, + external: ["@azure/msal-common/browser"], + plugins: [ + typescript({ + typescript: require("typescript"), + tsconfig: "tsconfig.custom-auth.build.json", + }), + ], + }, + { + // Custom Auth - CommonJS build + input: "src/custom_auth/index.ts", + output: { + dir: "lib/custom-auth-path", + format: "cjs", + banner: fileHeader, + sourcemap: true, + entryFileNames: "msal-custom-auth.cjs", + inlineDynamicImports: true, + }, + plugins: [ + nodeResolve({ + browser: true, + resolveOnly: ["@azure/msal-common", "tslib"], + }), + typescript({ + typescript: require("typescript"), + tsconfig: "tsconfig.custom-auth.build.json", + sourceMap: true, + compilerOptions: { outDir: "lib/custom-auth-path/types" }, + }), + ], + }, ]; diff --git a/lib/msal-browser/src/app/PublicClientApplication.ts b/lib/msal-browser/src/app/PublicClientApplication.ts index e9b9e8e36f..e479003029 100644 --- a/lib/msal-browser/src/app/PublicClientApplication.ts +++ b/lib/msal-browser/src/app/PublicClientApplication.ts @@ -42,6 +42,7 @@ import { EventType } from "../event/EventType.js"; */ export class PublicClientApplication implements IPublicClientApplication { protected controller: IController; + protected isBroker: boolean = false; /** * Creates StandardController and passes it to the PublicClientApplication @@ -92,7 +93,7 @@ export class PublicClientApplication implements IPublicClientApplication { * @param request {?InitializeApplicationRequest} */ async initialize(request?: InitializeApplicationRequest): Promise { - return this.controller.initialize(request); + return this.controller.initialize(request, this.isBroker); } /** diff --git a/lib/msal-browser/src/broker/nativeBroker/IPlatformAuthHandler.ts b/lib/msal-browser/src/broker/nativeBroker/IPlatformAuthHandler.ts new file mode 100644 index 0000000000..edc29b6284 --- /dev/null +++ b/lib/msal-browser/src/broker/nativeBroker/IPlatformAuthHandler.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { PlatformAuthRequest } from "./PlatformAuthRequest.js"; +import { PlatformAuthResponse } from "./PlatformAuthResponse.js"; + +/** + * Interface for the Platform Broker Handlers + */ +export interface IPlatformAuthHandler { + getExtensionId(): string | undefined; + getExtensionVersion(): string | undefined; + getExtensionName(): string | undefined; + sendMessage(request: PlatformAuthRequest): Promise; +} diff --git a/lib/msal-browser/src/broker/nativeBroker/NativeStatusCodes.ts b/lib/msal-browser/src/broker/nativeBroker/NativeStatusCodes.ts index a7d42e8ac1..7c9601117d 100644 --- a/lib/msal-browser/src/broker/nativeBroker/NativeStatusCodes.ts +++ b/lib/msal-browser/src/broker/nativeBroker/NativeStatusCodes.ts @@ -11,3 +11,4 @@ export const TRANSIENT_ERROR = "TRANSIENT_ERROR"; export const PERSISTENT_ERROR = "PERSISTENT_ERROR"; export const DISABLED = "DISABLED"; export const ACCOUNT_UNAVAILABLE = "ACCOUNT_UNAVAILABLE"; +export const UX_NOT_ALLOWED = "UX_NOT_ALLOWED"; diff --git a/lib/msal-browser/src/broker/nativeBroker/PlatformAuthDOMHandler.ts b/lib/msal-browser/src/broker/nativeBroker/PlatformAuthDOMHandler.ts new file mode 100644 index 0000000000..1f6bc116db --- /dev/null +++ b/lib/msal-browser/src/broker/nativeBroker/PlatformAuthDOMHandler.ts @@ -0,0 +1,248 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Logger, + createAuthError, + AuthErrorCodes, + IPerformanceClient, + StringDict, +} from "@azure/msal-common/browser"; +import { + DOMExtraParameters, + PlatformAuthRequest, + PlatformDOMTokenRequest, +} from "./PlatformAuthRequest.js"; +import { PlatformAuthConstants } from "../../utils/BrowserConstants.js"; +import { + PlatformAuthResponse, + PlatformDOMTokenResponse, +} from "./PlatformAuthResponse.js"; +import { createNativeAuthError } from "../../error/NativeAuthError.js"; +import { IPlatformAuthHandler } from "./IPlatformAuthHandler.js"; + +export class PlatformAuthDOMHandler implements IPlatformAuthHandler { + protected logger: Logger; + protected performanceClient: IPerformanceClient; + protected correlationId: string; + platformAuthType: string; + + constructor( + logger: Logger, + performanceClient: IPerformanceClient, + correlationId: string + ) { + this.logger = logger; + this.performanceClient = performanceClient; + this.correlationId = correlationId; + this.platformAuthType = PlatformAuthConstants.PLATFORM_DOM_PROVIDER; + } + + static async createProvider( + logger: Logger, + performanceClient: IPerformanceClient, + correlationId: string + ): Promise { + logger.trace("PlatformAuthDOMHandler: createProvider called"); + + // @ts-ignore + if (window.navigator?.platformAuthentication) { + const supportedContracts = + // @ts-ignore + await window.navigator.platformAuthentication.getSupportedContracts( + PlatformAuthConstants.MICROSOFT_ENTRA_BROKERID + ); + if ( + supportedContracts?.includes( + PlatformAuthConstants.PLATFORM_DOM_APIS + ) + ) { + logger.trace("Platform auth api available in DOM"); + return new PlatformAuthDOMHandler( + logger, + performanceClient, + correlationId + ); + } + } + return undefined; + } + + /** + * Returns the Id for the broker extension this handler is communicating with + * @returns + */ + getExtensionId(): string { + return PlatformAuthConstants.MICROSOFT_ENTRA_BROKERID; + } + + getExtensionVersion(): string | undefined { + return ""; + } + + getExtensionName(): string | undefined { + return PlatformAuthConstants.DOM_API_NAME; + } + + /** + * Send token request to platform broker via browser DOM API + * @param request + * @returns + */ + async sendMessage( + request: PlatformAuthRequest + ): Promise { + this.logger.trace( + this.platformAuthType + " - Sending request to browser DOM API" + ); + + try { + const platformDOMRequest: PlatformDOMTokenRequest = + this.initializePlatformDOMRequest(request); + const response: object = + // @ts-ignore + await window.navigator.platformAuthentication.executeGetToken( + platformDOMRequest + ); + return this.validatePlatformBrokerResponse(response); + } catch (e) { + this.logger.error( + this.platformAuthType + " - executeGetToken DOM API error" + ); + throw e; + } + } + + private initializePlatformDOMRequest( + request: PlatformAuthRequest + ): PlatformDOMTokenRequest { + this.logger.trace( + this.platformAuthType + " - initializeNativeDOMRequest called" + ); + + const { + accountId, + clientId, + authority, + scope, + redirectUri, + correlationId, + state, + storeInCache, + embeddedClientId, + extraParameters, + ...remainingProperties + } = request; + + const validExtraParameters: DOMExtraParameters = + this.getDOMExtraParams(remainingProperties); + + const platformDOMRequest: PlatformDOMTokenRequest = { + accountId: accountId, + brokerId: this.getExtensionId(), + authority: authority, + clientId: clientId, + correlationId: correlationId || this.correlationId, + extraParameters: { ...extraParameters, ...validExtraParameters }, + isSecurityTokenService: false, + redirectUri: redirectUri, + scope: scope, + state: state, + storeInCache: storeInCache, + embeddedClientId: embeddedClientId, + }; + + return platformDOMRequest; + } + + private validatePlatformBrokerResponse( + response: object + ): PlatformAuthResponse { + if (response.hasOwnProperty("isSuccess")) { + if ( + response.hasOwnProperty("accessToken") && + response.hasOwnProperty("idToken") && + response.hasOwnProperty("clientInfo") && + response.hasOwnProperty("account") && + response.hasOwnProperty("scopes") && + response.hasOwnProperty("expiresIn") + ) { + this.logger.trace( + this.platformAuthType + + " - platform broker returned successful and valid response" + ); + return this.convertToPlatformBrokerResponse( + response as PlatformDOMTokenResponse + ); + } else if (response.hasOwnProperty("error")) { + const errorResponse = response as PlatformDOMTokenResponse; + if ( + errorResponse.isSuccess === false && + errorResponse.error && + errorResponse.error.code + ) { + this.logger.trace( + this.platformAuthType + + " - platform broker returned error response" + ); + throw createNativeAuthError( + errorResponse.error.code, + errorResponse.error.description, + { + error: parseInt(errorResponse.error.errorCode), + protocol_error: errorResponse.error.protocolError, + status: errorResponse.error.status, + properties: errorResponse.error.properties, + } + ); + } + } + } + throw createAuthError( + AuthErrorCodes.unexpectedError, + "Response missing expected properties." + ); + } + + private convertToPlatformBrokerResponse( + response: PlatformDOMTokenResponse + ): PlatformAuthResponse { + this.logger.trace( + this.platformAuthType + " - convertToNativeResponse called" + ); + const nativeResponse: PlatformAuthResponse = { + access_token: response.accessToken, + id_token: response.idToken, + client_info: response.clientInfo, + account: response.account, + expires_in: response.expiresIn, + scope: response.scopes, + state: response.state || "", + properties: response.properties || {}, + extendedLifetimeToken: response.extendedLifetimeToken ?? false, + shr: response.proofOfPossessionPayload, + }; + + return nativeResponse; + } + + private getDOMExtraParams( + extraParameters: Record + ): DOMExtraParameters { + const stringifiedParams = Object.entries(extraParameters).reduce( + (record, [key, value]) => { + record[key] = String(value); + return record; + }, + {} as StringDict + ); + + const validExtraParams: DOMExtraParameters = { + ...stringifiedParams, + }; + + return validExtraParams; + } +} diff --git a/lib/msal-browser/src/broker/nativeBroker/NativeMessageHandler.ts b/lib/msal-browser/src/broker/nativeBroker/PlatformAuthExtensionHandler.ts similarity index 75% rename from lib/msal-browser/src/broker/nativeBroker/NativeMessageHandler.ts rename to lib/msal-browser/src/broker/nativeBroker/PlatformAuthExtensionHandler.ts index 3314601734..011c76af89 100644 --- a/lib/msal-browser/src/broker/nativeBroker/NativeMessageHandler.ts +++ b/lib/msal-browser/src/broker/nativeBroker/PlatformAuthExtensionHandler.ts @@ -4,7 +4,7 @@ */ import { - NativeConstants, + PlatformAuthConstants, NativeExtensionMethod, } from "../../utils/BrowserConstants.js"; import { @@ -12,7 +12,6 @@ import { AuthError, createAuthError, AuthErrorCodes, - AuthenticationScheme, InProgressPerformanceEvent, PerformanceEvents, IPerformanceClient, @@ -20,14 +19,16 @@ import { import { NativeExtensionRequest, NativeExtensionRequestBody, -} from "./NativeRequest.js"; + PlatformAuthRequest, +} from "./PlatformAuthRequest.js"; import { createNativeAuthError } from "../../error/NativeAuthError.js"; import { createBrowserAuthError, BrowserAuthErrorCodes, } from "../../error/BrowserAuthError.js"; -import { BrowserConfiguration } from "../../config/Configuration.js"; import { createNewGuid } from "../../crypto/BrowserCrypto.js"; +import { PlatformAuthResponse } from "./PlatformAuthResponse.js"; +import { IPlatformAuthHandler } from "./IPlatformAuthHandler.js"; type ResponseResolvers = { resolve: (value: T | PromiseLike) => void; @@ -36,7 +37,7 @@ type ResponseResolvers = { ) => void; }; -export class NativeMessageHandler { +export class PlatformAuthExtensionHandler implements IPlatformAuthHandler { private extensionId: string | undefined; private extensionVersion: string | undefined; private logger: Logger; @@ -48,6 +49,7 @@ export class NativeMessageHandler { private readonly windowListener: (event: MessageEvent) => void; private readonly performanceClient: IPerformanceClient; private readonly handshakeEvent: InProgressPerformanceEvent; + platformAuthType: string; constructor( logger: Logger, @@ -66,34 +68,51 @@ export class NativeMessageHandler { this.handshakeEvent = performanceClient.startMeasurement( PerformanceEvents.NativeMessageHandlerHandshake ); + this.platformAuthType = + PlatformAuthConstants.PLATFORM_EXTENSION_PROVIDER; } /** * Sends a given message to the extension and resolves with the extension response - * @param body + * @param request */ - async sendMessage(body: NativeExtensionRequestBody): Promise { - this.logger.trace("NativeMessageHandler - sendMessage called."); + async sendMessage( + request: PlatformAuthRequest + ): Promise { + this.logger.trace(this.platformAuthType + " - sendMessage called."); + + // fall back to native calls + const messageBody: NativeExtensionRequestBody = { + method: NativeExtensionMethod.GetToken, + request: request, + }; + const req: NativeExtensionRequest = { - channel: NativeConstants.CHANNEL_ID, + channel: PlatformAuthConstants.CHANNEL_ID, extensionId: this.extensionId, responseId: createNewGuid(), - body: body, + body: messageBody, }; this.logger.trace( - "NativeMessageHandler - Sending request to browser extension" + this.platformAuthType + " - Sending request to browser extension" ); this.logger.tracePii( - `NativeMessageHandler - Sending request to browser extension: ${JSON.stringify( - req - )}` + this.platformAuthType + + ` - Sending request to browser extension: ${JSON.stringify( + req + )}` ); this.messageChannel.port1.postMessage(req); - return new Promise((resolve, reject) => { + const response: object = await new Promise((resolve, reject) => { this.resolvers.set(req.responseId, { resolve, reject }); }); + + const validatedResponse: PlatformAuthResponse = + this.validatePlatformBrokerResponse(response); + + return validatedResponse; } /** @@ -107,20 +126,21 @@ export class NativeMessageHandler { logger: Logger, handshakeTimeoutMs: number, performanceClient: IPerformanceClient - ): Promise { - logger.trace("NativeMessageHandler - createProvider called."); + ): Promise { + logger.trace("PlatformAuthExtensionHandler - createProvider called."); + try { - const preferredProvider = new NativeMessageHandler( + const preferredProvider = new PlatformAuthExtensionHandler( logger, handshakeTimeoutMs, performanceClient, - NativeConstants.PREFERRED_EXTENSION_ID + PlatformAuthConstants.PREFERRED_EXTENSION_ID ); await preferredProvider.sendHandshakeRequest(); return preferredProvider; } catch (e) { // If preferred extension fails for whatever reason, fallback to using any installed extension - const backupProvider = new NativeMessageHandler( + const backupProvider = new PlatformAuthExtensionHandler( logger, handshakeTimeoutMs, performanceClient @@ -135,13 +155,13 @@ export class NativeMessageHandler { */ private async sendHandshakeRequest(): Promise { this.logger.trace( - "NativeMessageHandler - sendHandshakeRequest called." + this.platformAuthType + " - sendHandshakeRequest called." ); // Register this event listener before sending handshake window.addEventListener("message", this.windowListener, false); // false is important, because content script message processing should work first const req: NativeExtensionRequest = { - channel: NativeConstants.CHANNEL_ID, + channel: PlatformAuthConstants.CHANNEL_ID, extensionId: this.extensionId, responseId: createNewGuid(), body: { @@ -192,7 +212,7 @@ export class NativeMessageHandler { * @param event */ private onWindowMessage(event: MessageEvent): void { - this.logger.trace("NativeMessageHandler - onWindowMessage called"); + this.logger.trace(this.platformAuthType + " - onWindowMessage called"); // We only accept messages from ourselves if (event.source !== window) { return; @@ -202,7 +222,7 @@ export class NativeMessageHandler { if ( !request.channel || - request.channel !== NativeConstants.CHANNEL_ID + request.channel !== PlatformAuthConstants.CHANNEL_ID ) { return; } @@ -221,7 +241,8 @@ export class NativeMessageHandler { */ if (!handshakeResolver) { this.logger.trace( - `NativeMessageHandler.onWindowMessage - resolver can't be found for request ${request.responseId}` + this.platformAuthType + + `.onWindowMessage - resolver can't be found for request ${request.responseId}` ); return; } @@ -253,7 +274,9 @@ export class NativeMessageHandler { * @param event */ private onChannelMessage(event: MessageEvent): void { - this.logger.trace("NativeMessageHandler - onChannelMessage called."); + this.logger.trace( + this.platformAuthType + " - onChannelMessage called." + ); const request = event.data; const resolver = this.resolvers.get(request.responseId); @@ -270,12 +293,14 @@ export class NativeMessageHandler { } const response = request.body.response; this.logger.trace( - "NativeMessageHandler - Received response from browser extension" + this.platformAuthType + + " - Received response from browser extension" ); this.logger.tracePii( - `NativeMessageHandler - Received response from browser extension: ${JSON.stringify( - response - )}` + this.platformAuthType + + ` - Received response from browser extension: ${JSON.stringify( + response + )}` ); if (response.status !== "Success") { resolver.reject( @@ -310,7 +335,8 @@ export class NativeMessageHandler { } else if (method === NativeExtensionMethod.HandshakeResponse) { if (!handshakeResolver) { this.logger.trace( - `NativeMessageHandler.onChannelMessage - resolver can't be found for request ${request.responseId}` + this.platformAuthType + + `.onChannelMessage - resolver can't be found for request ${request.responseId}` ); return; } @@ -323,7 +349,8 @@ export class NativeMessageHandler { this.extensionId = request.extensionId; this.extensionVersion = request.body.version; this.logger.verbose( - `NativeMessageHandler - Received HandshakeResponse from extension: ${this.extensionId}` + this.platformAuthType + + ` - Received HandshakeResponse from extension: ${this.extensionId}` ); this.handshakeEvent.end({ extensionInstalled: true, @@ -349,6 +376,30 @@ export class NativeMessageHandler { } } + /** + * Validates native platform response before processing + * @param response + */ + private validatePlatformBrokerResponse( + response: object + ): PlatformAuthResponse { + if ( + response.hasOwnProperty("access_token") && + response.hasOwnProperty("id_token") && + response.hasOwnProperty("client_info") && + response.hasOwnProperty("account") && + response.hasOwnProperty("scope") && + response.hasOwnProperty("expires_in") + ) { + return response as PlatformAuthResponse; + } else { + throw createAuthError( + AuthErrorCodes.unexpectedError, + "Response missing expected properties." + ); + } + } + /** * Returns the Id for the browser extension this handler is communicating with * @returns @@ -365,52 +416,12 @@ export class NativeMessageHandler { return this.extensionVersion; } - /** - * Returns boolean indicating whether or not the request should attempt to use native broker - * @param logger - * @param config - * @param nativeExtensionProvider - * @param authenticationScheme - */ - static isPlatformBrokerAvailable( - config: BrowserConfiguration, - logger: Logger, - nativeExtensionProvider?: NativeMessageHandler, - authenticationScheme?: AuthenticationScheme - ): boolean { - logger.trace("isPlatformBrokerAvailable called"); - if (!config.system.allowPlatformBroker) { - logger.trace( - "isPlatformBrokerAvailable: allowPlatformBroker is not enabled, returning false" - ); - // Developer disabled WAM - return false; - } - - if (!nativeExtensionProvider) { - logger.trace( - "isPlatformBrokerAvailable: Platform extension provider is not initialized, returning false" - ); - // Extension is not available - return false; - } - - if (authenticationScheme) { - switch (authenticationScheme) { - case AuthenticationScheme.BEARER: - case AuthenticationScheme.POP: - logger.trace( - "isPlatformBrokerAvailable: authenticationScheme is supported, returning true" - ); - return true; - default: - logger.trace( - "isPlatformBrokerAvailable: authenticationScheme is not supported, returning false" - ); - return false; - } - } - - return true; + getExtensionName(): string | undefined { + return this.getExtensionId() === + PlatformAuthConstants.PREFERRED_EXTENSION_ID + ? "chrome" + : this.getExtensionId()?.length + ? "unknown" + : undefined; } } diff --git a/lib/msal-browser/src/broker/nativeBroker/PlatformAuthProvider.ts b/lib/msal-browser/src/broker/nativeBroker/PlatformAuthProvider.ts new file mode 100644 index 0000000000..00ff1e0e72 --- /dev/null +++ b/lib/msal-browser/src/broker/nativeBroker/PlatformAuthProvider.ts @@ -0,0 +1,164 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + LoggerOptions, + IPerformanceClient, + Logger, + AuthenticationScheme, + StubPerformanceClient, +} from "@azure/msal-common/browser"; +import { name, version } from "../../packageMetadata.js"; +import { + BrowserConfiguration, + DEFAULT_NATIVE_BROKER_HANDSHAKE_TIMEOUT_MS, +} from "../../config/Configuration.js"; +import { PlatformAuthExtensionHandler } from "./PlatformAuthExtensionHandler.js"; +import { IPlatformAuthHandler } from "./IPlatformAuthHandler.js"; +import { PlatformAuthDOMHandler } from "./PlatformAuthDOMHandler.js"; +import { createNewGuid } from "../../crypto/BrowserCrypto.js"; +import { + BrowserCacheLocation, + PLATFORM_AUTH_DOM_SUPPORT, +} from "../../utils/BrowserConstants.js"; + +/** + * Checks if the platform broker is available in the current environment. + * @param loggerOptions + * @param perfClient + * @returns + */ +export async function isPlatformBrokerAvailable( + loggerOptions?: LoggerOptions, + perfClient?: IPerformanceClient, + correlationId?: string +): Promise { + const logger = new Logger(loggerOptions || {}, name, version); + + logger.trace("isPlatformBrokerAvailable called"); + + const performanceClient = perfClient || new StubPerformanceClient(); + + if (typeof window === "undefined") { + logger.trace("Non-browser environment detected, returning false"); + return false; + } + + return !!(await getPlatformAuthProvider( + logger, + performanceClient, + correlationId || createNewGuid() + )); +} + +export async function getPlatformAuthProvider( + logger: Logger, + performanceClient: IPerformanceClient, + correlationId: string, + nativeBrokerHandshakeTimeout?: number +): Promise { + logger.trace("getPlatformAuthProvider called", correlationId); + + const enablePlatformBrokerDOMSupport = isDomEnabledForPlatformAuth(); + + logger.trace( + "Has client allowed platform auth via DOM API: " + + enablePlatformBrokerDOMSupport + ); + let platformAuthProvider: IPlatformAuthHandler | undefined; + try { + if (enablePlatformBrokerDOMSupport) { + // Check if DOM platform API is supported first + platformAuthProvider = await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + correlationId + ); + } + if (!platformAuthProvider) { + logger.trace( + "Platform auth via DOM API not available, checking for extension" + ); + /* + * If DOM APIs are not available, check if browser extension is available. + * Platform authentication via DOM APIs is preferred over extension APIs. + */ + platformAuthProvider = + await PlatformAuthExtensionHandler.createProvider( + logger, + nativeBrokerHandshakeTimeout || + DEFAULT_NATIVE_BROKER_HANDSHAKE_TIMEOUT_MS, + performanceClient + ); + } + } catch (e) { + logger.trace("Platform auth not available", e as string); + } + return platformAuthProvider; +} + +/** + * Returns true if the DOM API support for platform auth is enabled in session storage + * @returns boolean + * @deprecated + */ +export function isDomEnabledForPlatformAuth(): boolean { + let sessionStorage: Storage | undefined; + try { + sessionStorage = window[BrowserCacheLocation.SessionStorage]; + // Mute errors if it's a non-browser environment or cookies are blocked. + return sessionStorage?.getItem(PLATFORM_AUTH_DOM_SUPPORT) === "true"; + } catch (e) { + return false; + } +} + +/** + * Returns boolean indicating whether or not the request should attempt to use native broker + * @param logger + * @param config + * @param platformAuthProvider + * @param authenticationScheme + */ +export function isPlatformAuthAllowed( + config: BrowserConfiguration, + logger: Logger, + platformAuthProvider?: IPlatformAuthHandler, + authenticationScheme?: AuthenticationScheme +): boolean { + logger.trace("isPlatformAuthAllowed called"); + if (!config.system.allowPlatformBroker) { + logger.trace( + "isPlatformAuthAllowed: allowPlatformBroker is not enabled, returning false" + ); + // Developer disabled WAM + return false; + } + + if (!platformAuthProvider) { + logger.trace( + "isPlatformAuthAllowed: Platform auth provider is not initialized, returning false" + ); + // Platform broker auth providers are not available + return false; + } + + if (authenticationScheme) { + switch (authenticationScheme) { + case AuthenticationScheme.BEARER: + case AuthenticationScheme.POP: + logger.trace( + "isPlatformAuthAllowed: authenticationScheme is supported, returning true" + ); + return true; + default: + logger.trace( + "isPlatformAuthAllowed: authenticationScheme is not supported, returning false" + ); + return false; + } + } + return true; +} diff --git a/lib/msal-browser/src/broker/nativeBroker/NativeRequest.ts b/lib/msal-browser/src/broker/nativeBroker/PlatformAuthRequest.ts similarity index 53% rename from lib/msal-browser/src/broker/nativeBroker/NativeRequest.ts rename to lib/msal-browser/src/broker/nativeBroker/PlatformAuthRequest.ts index 8402d57805..177c6f0ee9 100644 --- a/lib/msal-browser/src/broker/nativeBroker/NativeRequest.ts +++ b/lib/msal-browser/src/broker/nativeBroker/PlatformAuthRequest.ts @@ -9,7 +9,7 @@ import { StoreInCache, StringDict } from "@azure/msal-common/browser"; /** * Token request which native broker will use to acquire tokens */ -export type NativeTokenRequest = { +export type PlatformAuthRequest = { accountId: string; // WAM specific account id used for identification of WAM account. This can be any broker-id eventually clientId: string; authority: string; @@ -40,7 +40,7 @@ export type NativeTokenRequest = { */ export type NativeExtensionRequestBody = { method: NativeExtensionMethod; - request?: NativeTokenRequest; + request?: PlatformAuthRequest; }; /** @@ -52,3 +52,42 @@ export type NativeExtensionRequest = { extensionId?: string; body: NativeExtensionRequestBody; }; + +export type PlatformDOMTokenRequest = { + brokerId: string; + accountId?: string; + clientId: string; + authority: string; + scope: string; + redirectUri: string; + correlationId: string; + isSecurityTokenService: boolean; + state?: string; + /* + * Known optional parameters will go into extraQueryParameters. + * List of known parameters is: + * "prompt", "nonce", "claims", "loginHint", "instanceAware", "windowTitleSubstring", "extendedExpiryToken", "storeInCache", + * ProofOfPossessionParams: "reqCnf", "keyId", "tokenType", "shrClaims", "shrNonce", "resourceRequestMethod", "resourceRequestUri", "signPopToken" + */ + extraParameters?: DOMExtraParameters; + embeddedClientId?: string; + storeInCache?: StoreInCache; // Object of booleans indicating whether to store tokens in the cache or not (default is true) +}; + +export type DOMExtraParameters = StringDict & { + prompt?: string; + nonce?: string; + claims?: string; + loginHint?: string; + instanceAware?: string; + windowTitleSubstring?: string; + extendedExpiryToken?: string; + reqCnf?: string; + keyId?: string; + tokenType?: string; + shrClaims?: string; + shrNonce?: string; + resourceRequestMethod?: string; + resourceRequestUri?: string; + signPopToken?: string; // Set to true only if token request deos not contain a PoP keyId +}; diff --git a/lib/msal-browser/src/broker/nativeBroker/NativeResponse.ts b/lib/msal-browser/src/broker/nativeBroker/PlatformAuthResponse.ts similarity index 70% rename from lib/msal-browser/src/broker/nativeBroker/NativeResponse.ts rename to lib/msal-browser/src/broker/nativeBroker/PlatformAuthResponse.ts index 79392395df..f1fae8e48c 100644 --- a/lib/msal-browser/src/broker/nativeBroker/NativeResponse.ts +++ b/lib/msal-browser/src/broker/nativeBroker/PlatformAuthResponse.ts @@ -15,7 +15,7 @@ export type NativeAccountInfo = { /** * Token response returned by Native Platform */ -export type NativeResponse = { +export type PlatformAuthResponse = { access_token: string; account: NativeAccountInfo; client_info: string; @@ -54,3 +54,27 @@ export type MATS = { http_status?: number; http_event_count?: number; }; + +export type PlatformDOMTokenResponse = { + isSuccess: boolean; + state?: string; + accessToken: string; + expiresIn: number; + account: NativeAccountInfo; + clientInfo: string; + idToken: string; + scopes: string; + proofOfPossessionPayload?: string; + extendedLifetimeToken?: boolean; + error: ErrorResult; + properties?: Record; +}; + +export type ErrorResult = { + code: string; + description?: string; + errorCode: string; + protocolError?: string; + status: string; + properties?: object; +}; diff --git a/lib/msal-browser/src/cache/AccountManager.ts b/lib/msal-browser/src/cache/AccountManager.ts index e5bcd1a526..d3710fa191 100644 --- a/lib/msal-browser/src/cache/AccountManager.ts +++ b/lib/msal-browser/src/cache/AccountManager.ts @@ -15,10 +15,13 @@ export function getAllAccounts( logger: Logger, browserStorage: BrowserCacheManager, isInBrowser: boolean, + correlationId: string, accountFilter?: AccountFilter ): AccountInfo[] { logger.verbose("getAllAccounts called"); - return isInBrowser ? browserStorage.getAllAccounts(accountFilter) : []; + return isInBrowser + ? browserStorage.getAllAccounts(accountFilter || {}, correlationId) + : []; } /** @@ -29,7 +32,8 @@ export function getAllAccounts( export function getAccount( accountFilter: AccountFilter, logger: Logger, - browserStorage: BrowserCacheManager + browserStorage: BrowserCacheManager, + correlationId: string ): AccountInfo | null { logger.trace("getAccount called"); if (Object.keys(accountFilter).length === 0) { @@ -37,8 +41,10 @@ export function getAccount( return null; } - const account: AccountInfo | null = - browserStorage.getAccountInfoFilteredBy(accountFilter); + const account: AccountInfo | null = browserStorage.getAccountInfoFilteredBy( + accountFilter, + correlationId + ); if (account) { logger.verbose( @@ -62,7 +68,8 @@ export function getAccount( export function getAccountByUsername( username: string, logger: Logger, - browserStorage: BrowserCacheManager + browserStorage: BrowserCacheManager, + correlationId: string ): AccountInfo | null { logger.trace("getAccountByUsername called"); if (!username) { @@ -70,9 +77,12 @@ export function getAccountByUsername( return null; } - const account = browserStorage.getAccountInfoFilteredBy({ - username, - }); + const account = browserStorage.getAccountInfoFilteredBy( + { + username, + }, + correlationId + ); if (account) { logger.verbose( "getAccountByUsername: Account matching username found, returning" @@ -99,7 +109,8 @@ export function getAccountByUsername( export function getAccountByHomeId( homeAccountId: string, logger: Logger, - browserStorage: BrowserCacheManager + browserStorage: BrowserCacheManager, + correlationId: string ): AccountInfo | null { logger.trace("getAccountByHomeId called"); if (!homeAccountId) { @@ -107,9 +118,12 @@ export function getAccountByHomeId( return null; } - const account = browserStorage.getAccountInfoFilteredBy({ - homeAccountId, - }); + const account = browserStorage.getAccountInfoFilteredBy( + { + homeAccountId, + }, + correlationId + ); if (account) { logger.verbose( "getAccountByHomeId: Account matching homeAccountId found, returning" @@ -136,7 +150,8 @@ export function getAccountByHomeId( export function getAccountByLocalId( localAccountId: string, logger: Logger, - browserStorage: BrowserCacheManager + browserStorage: BrowserCacheManager, + correlationId: string ): AccountInfo | null { logger.trace("getAccountByLocalId called"); if (!localAccountId) { @@ -144,9 +159,12 @@ export function getAccountByLocalId( return null; } - const account = browserStorage.getAccountInfoFilteredBy({ - localAccountId, - }); + const account = browserStorage.getAccountInfoFilteredBy( + { + localAccountId, + }, + correlationId + ); if (account) { logger.verbose( "getAccountByLocalId: Account matching localAccountId found, returning" @@ -169,16 +187,18 @@ export function getAccountByLocalId( */ export function setActiveAccount( account: AccountInfo | null, - browserStorage: BrowserCacheManager + browserStorage: BrowserCacheManager, + correlationId: string ): void { - browserStorage.setActiveAccount(account); + browserStorage.setActiveAccount(account, correlationId); } /** * Gets the currently active account */ export function getActiveAccount( - browserStorage: BrowserCacheManager + browserStorage: BrowserCacheManager, + correlationId: string ): AccountInfo | null { - return browserStorage.getActiveAccount(); + return browserStorage.getActiveAccount(correlationId); } diff --git a/lib/msal-browser/src/cache/BrowserCacheManager.ts b/lib/msal-browser/src/cache/BrowserCacheManager.ts index 76b506607e..d4e9d71776 100644 --- a/lib/msal-browser/src/cache/BrowserCacheManager.ts +++ b/lib/msal-browser/src/cache/BrowserCacheManager.ts @@ -4,60 +4,55 @@ */ import { - Constants, - PersistentCacheKeys, - StringUtils, - CommonAuthorizationCodeRequest, - ICrypto, - AccountEntity, - IdTokenEntity, AccessTokenEntity, - RefreshTokenEntity, - AppMetadataEntity, - CacheManager, - ServerTelemetryEntity, - ThrottlingEntity, - ProtocolUtils, - Logger, - AuthorityMetadataEntity, - DEFAULT_CRYPTO_IMPLEMENTATION, + AccountEntity, AccountInfo, ActiveAccountFilters, - CcsCredential, - CcsCredentialType, - TokenKeys, - CredentialType, - CacheRecord, + AppMetadataEntity, AuthenticationScheme, - createClientAuthError, - ClientAuthErrorCodes, - PerformanceEvents, + AuthorityMetadataEntity, + CacheError, + CacheErrorCodes, + CacheHelpers, + CacheManager, + CacheRecord, + CommonAuthorizationUrlRequest, + Constants, + createCacheError, + DEFAULT_CRYPTO_IMPLEMENTATION, + ICrypto, + IdTokenEntity, + invokeAsync, IPerformanceClient, + Logger, + PerformanceEvents, + PersistentCacheKeys, + RefreshTokenEntity, + ServerTelemetryEntity, StaticAuthorityOptions, - CacheHelpers, StoreInCache, - CacheError, - invokeAsync, + StringUtils, + ThrottlingEntity, TimeUtils, + TokenKeys, } from "@azure/msal-common/browser"; import { CacheOptions } from "../config/Configuration.js"; import { - createBrowserAuthError, BrowserAuthErrorCodes, + createBrowserAuthError, } from "../error/BrowserAuthError.js"; import { BrowserCacheLocation, - InteractionType, - TemporaryCacheKeys, InMemoryCacheKeys, + INTERACTION_TYPE, StaticCacheKeys, + TemporaryCacheKeys, } from "../utils/BrowserConstants.js"; import { LocalStorage } from "./LocalStorage.js"; import { SessionStorage } from "./SessionStorage.js"; import { MemoryStorage } from "./MemoryStorage.js"; import { IWindowStorage } from "./IWindowStorage.js"; -import { extractBrowserRequestState } from "../utils/BrowserProtocolUtils.js"; -import { NativeTokenRequest } from "../broker/nativeBroker/NativeRequest.js"; +import { PlatformAuthRequest } from "../broker/nativeBroker/PlatformAuthRequest.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; import { SilentRequest } from "../request/SilentRequest.js"; import { SsoSilentRequest } from "../request/SsoSilentRequest.js"; @@ -69,6 +64,8 @@ import { CookieStorage } from "./CookieStorage.js"; import { getAccountKeys, getTokenKeys } from "./CacheHelpers.js"; import { EventType } from "../event/EventType.js"; import { EventHandler } from "../event/EventHandler.js"; +import { clearHash } from "../utils/BrowserUtils.js"; +import { version } from "../packageMetadata.js"; /** * This class implements the cache storage interface for MSAL through browser local or session storage. @@ -88,8 +85,6 @@ export class BrowserCacheManager extends CacheManager { protected cookieStorage: CookieStorage; // Logger instance protected logger: Logger; - // Telemetry perf client - protected performanceClient: IPerformanceClient; // Event Handler private eventHandler: EventHandler; @@ -102,7 +97,13 @@ export class BrowserCacheManager extends CacheManager { eventHandler: EventHandler, staticAuthorityOptions?: StaticAuthorityOptions ) { - super(clientId, cryptoImpl, logger, staticAuthorityOptions); + super( + clientId, + cryptoImpl, + logger, + performanceClient, + staticAuthorityOptions + ); this.cacheConfig = cacheConfig; this.logger = logger; this.internalStorage = new MemoryStorage(); @@ -119,13 +120,34 @@ export class BrowserCacheManager extends CacheManager { performanceClient ); this.cookieStorage = new CookieStorage(); - - this.performanceClient = performanceClient; this.eventHandler = eventHandler; } async initialize(correlationId: string): Promise { await this.browserStorage.initialize(correlationId); + this.trackVersionChanges(correlationId); + } + + /** + * Tracks upgrades and downgrades for telemetry and debugging purposes + */ + private trackVersionChanges(correlationId: string): void { + const previousVersion = this.browserStorage.getItem( + StaticCacheKeys.VERSION + ); + if (previousVersion) { + this.logger.info( + `MSAL.js was last initialized by version: ${previousVersion}` + ); + this.performanceClient.addFields( + { previousLibraryVersion: previousVersion }, + correlationId + ); + } + + if (previousVersion !== version) { + this.setItem(StaticCacheKeys.VERSION, version, correlationId); + } } /** @@ -149,23 +171,140 @@ export class BrowserCacheManager extends CacheManager { } } + /** + * Helper to setItem in browser storage, with cleanup in case of quota errors + * @param key + * @param value + */ + setItem(key: string, value: string, correlationId: string): void { + let accessTokenKeys: Array = []; + const maxRetries = 20; + for (let i = 0; i <= maxRetries; i++) { + try { + this.browserStorage.setItem(key, value); + if (i > 0) { + // Finally update the token keys array with the tokens removed + this.removeAccessTokenKeys( + accessTokenKeys.slice(0, i), + correlationId + ); + } + break; // If setItem succeeds, exit the loop + } catch (e) { + const cacheError = createCacheError(e); + if ( + cacheError.errorCode === + CacheErrorCodes.cacheQuotaExceeded && + i < maxRetries + ) { + if (!accessTokenKeys.length) { + if ( + key === + `${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}` + ) { + // If we are currently trying to set the token keys, use the value we're trying to set + accessTokenKeys = (JSON.parse(value) as TokenKeys) + .accessToken; + } else { + // If token keys have not been initialized, get them + accessTokenKeys = this.getTokenKeys().accessToken; + } + } + if (accessTokenKeys.length <= i) { + // Nothing to remove, rethrow the error + throw cacheError; + } + // When cache quota is exceeded, start removing access tokens until we can successfully set the item + this.removeAccessToken( + accessTokenKeys[i], + correlationId, + false // Don't save token keys yet, do it at the end + ); + } else { + // If the error is not a quota exceeded error, rethrow it + throw cacheError; + } + } + } + } + + /** + * Helper to setUserData in browser storage, with cleanup in case of quota errors + * @param key + * @param value + * @param correlationId + */ + async setUserData( + key: string, + value: string, + correlationId: string, + timestamp: string + ): Promise { + let accessTokenKeys: Array = []; + const maxRetries = 20; + for (let i = 0; i <= maxRetries; i++) { + try { + await invokeAsync( + this.browserStorage.setUserData.bind(this.browserStorage), + PerformanceEvents.SetUserData, + this.logger, + this.performanceClient + )(key, value, correlationId, timestamp); + if (i > 0) { + // Finally update the token keys array with the tokens removed + this.removeAccessTokenKeys( + accessTokenKeys.slice(0, i), + correlationId + ); + } + break; // If setItem succeeds, exit the loop + } catch (e) { + const cacheError = createCacheError(e); + if ( + cacheError.errorCode === + CacheErrorCodes.cacheQuotaExceeded && + i < maxRetries + ) { + if (!accessTokenKeys.length) { + accessTokenKeys = this.getTokenKeys().accessToken; + } + if (accessTokenKeys.length <= i) { + // Nothing left to remove, rethrow the error + throw cacheError; + } + // When cache quota is exceeded, start removing access tokens until we can successfully set the item + this.removeAccessToken( + accessTokenKeys[i], + correlationId, + false // Don't save token keys yet, do it at the end + ); + } else { + // If the error is not a quota exceeded error, rethrow it + throw cacheError; + } + } + } + } + /** * Reads account from cache, deserializes it into an account entity and returns it. * If account is not found from the key, returns null and removes key from map. * @param accountKey * @returns */ - getAccount(accountKey: string): AccountEntity | null { + getAccount( + accountKey: string, + correlationId: string + ): AccountEntity | null { this.logger.trace("BrowserCacheManager.getAccount called"); const serializedAccount = this.browserStorage.getUserData(accountKey); if (!serializedAccount) { - this.removeAccountKeyFromMap(accountKey); + this.removeAccountKeyFromMap(accountKey, correlationId); return null; } const parsedAccount = this.validateAndParseJson(serializedAccount); if (!parsedAccount || !AccountEntity.isAccountEntity(parsedAccount)) { - this.removeAccountKeyFromMap(accountKey); return null; } @@ -185,13 +324,15 @@ export class BrowserCacheManager extends CacheManager { ): Promise { this.logger.trace("BrowserCacheManager.setAccount called"); const key = account.generateAccountKey(); - await invokeAsync( - this.browserStorage.setUserData.bind(this.browserStorage), - PerformanceEvents.SetUserData, - this.logger, - this.performanceClient - )(key, JSON.stringify(account), correlationId); - const wasAdded = this.addAccountKeyToMap(key); + const timestamp = Date.now().toString(); + account.lastUpdatedAt = timestamp; + await this.setUserData( + key, + JSON.stringify(account), + correlationId, + timestamp + ); + const wasAdded = this.addAccountKeyToMap(key, correlationId); /** * @deprecated - Remove this in next major version in favor of more consistent LOGIN event @@ -221,7 +362,7 @@ export class BrowserCacheManager extends CacheManager { * Add a new account to the key map * @param key */ - addAccountKeyToMap(key: string): boolean { + addAccountKeyToMap(key: string, correlationId: string): boolean { this.logger.trace("BrowserCacheManager.addAccountKeyToMap called"); this.logger.tracePii( `BrowserCacheManager.addAccountKeyToMap called with key: ${key}` @@ -230,9 +371,10 @@ export class BrowserCacheManager extends CacheManager { if (accountKeys.indexOf(key) === -1) { // Only add key if it does not already exist in the map accountKeys.push(key); - this.browserStorage.setItem( + this.setItem( StaticCacheKeys.ACCOUNT_KEYS, - JSON.stringify(accountKeys) + JSON.stringify(accountKeys), + correlationId ); this.logger.verbose( "BrowserCacheManager.addAccountKeyToMap account key added" @@ -250,7 +392,7 @@ export class BrowserCacheManager extends CacheManager { * Remove an account from the key map * @param key */ - removeAccountKeyFromMap(key: string): void { + removeAccountKeyFromMap(key: string, correlationId: string): void { this.logger.trace("BrowserCacheManager.removeAccountKeyFromMap called"); this.logger.tracePii( `BrowserCacheManager.removeAccountKeyFromMap called with key: ${key}` @@ -259,10 +401,17 @@ export class BrowserCacheManager extends CacheManager { const removalIndex = accountKeys.indexOf(key); if (removalIndex > -1) { accountKeys.splice(removalIndex, 1); - this.browserStorage.setItem( - StaticCacheKeys.ACCOUNT_KEYS, - JSON.stringify(accountKeys) - ); + if (accountKeys.length === 0) { + // If no keys left, remove the map + this.removeItem(StaticCacheKeys.ACCOUNT_KEYS); + return; + } else { + this.setItem( + StaticCacheKeys.ACCOUNT_KEYS, + JSON.stringify(accountKeys), + correlationId + ); + } this.logger.trace( "BrowserCacheManager.removeAccountKeyFromMap account key removed" ); @@ -277,17 +426,17 @@ export class BrowserCacheManager extends CacheManager { * Extends inherited removeAccount function to include removal of the account key from the map * @param key */ - async removeAccount(key: string): Promise { - void super.removeAccount(key); - this.removeAccountKeyFromMap(key); + removeAccount(key: string, correlationId: string): void { + super.removeAccount(key, correlationId); + this.removeAccountKeyFromMap(key, correlationId); } /** * Removes credentials associated with the provided account * @param account */ - async removeAccountContext(account: AccountEntity): Promise { - await super.removeAccountContext(account); + removeAccountContext(account: AccountEntity, correlationId: string): void { + super.removeAccountContext(account, correlationId); /** * @deprecated - Remove this in next major version in favor of more consistent LOGOUT event @@ -307,170 +456,118 @@ export class BrowserCacheManager extends CacheManager { * Removes given idToken from the cache and from the key map * @param key */ - removeIdToken(key: string): void { - super.removeIdToken(key); - this.removeTokenKey(key, CredentialType.ID_TOKEN); + removeIdToken(key: string, correlationId: string): void { + super.removeIdToken(key, correlationId); + const tokenKeys = this.getTokenKeys(); + const idRemoval = tokenKeys.idToken.indexOf(key); + if (idRemoval > -1) { + this.logger.info("idToken removed from tokenKeys map"); + tokenKeys.idToken.splice(idRemoval, 1); + this.setTokenKeys(tokenKeys, correlationId); + } } /** * Removes given accessToken from the cache and from the key map * @param key */ - async removeAccessToken(key: string): Promise { - void super.removeAccessToken(key); - this.removeTokenKey(key, CredentialType.ACCESS_TOKEN); + removeAccessToken( + key: string, + correlationId: string, + updateTokenKeys: boolean = true + ): void { + super.removeAccessToken(key, correlationId); + updateTokenKeys && this.removeAccessTokenKeys([key], correlationId); } /** - * Removes given refreshToken from the cache and from the key map + * Remove access token key from the key map * @param key + * @param correlationId + * @param tokenKeys */ - removeRefreshToken(key: string): void { - super.removeRefreshToken(key); - this.removeTokenKey(key, CredentialType.REFRESH_TOKEN); - } + removeAccessTokenKeys(keys: Array, correlationId: string): void { + this.logger.trace("removeAccessTokenKey called"); + const tokenKeys = this.getTokenKeys(); + let keysRemoved = 0; + keys.forEach((key) => { + const accessRemoval = tokenKeys.accessToken.indexOf(key); + if (accessRemoval > -1) { + tokenKeys.accessToken.splice(accessRemoval, 1); + keysRemoved++; + } + }); - /** - * Gets the keys for the cached tokens associated with this clientId - * @returns - */ - getTokenKeys(): TokenKeys { - return getTokenKeys(this.clientId, this.browserStorage); + if (keysRemoved > 0) { + this.logger.info( + `removed ${keysRemoved} accessToken keys from tokenKeys map` + ); + this.setTokenKeys(tokenKeys, correlationId); + return; + } } /** - * Adds the given key to the token key map + * Removes given refreshToken from the cache and from the key map * @param key - * @param type */ - addTokenKey(key: string, type: CredentialType): void { - this.logger.trace("BrowserCacheManager addTokenKey called"); + removeRefreshToken(key: string, correlationId: string): void { + super.removeRefreshToken(key, correlationId); const tokenKeys = this.getTokenKeys(); - - switch (type) { - case CredentialType.ID_TOKEN: - if (tokenKeys.idToken.indexOf(key) === -1) { - this.logger.info( - "BrowserCacheManager: addTokenKey - idToken added to map" - ); - tokenKeys.idToken.push(key); - } - break; - case CredentialType.ACCESS_TOKEN: - if (tokenKeys.accessToken.indexOf(key) === -1) { - this.logger.info( - "BrowserCacheManager: addTokenKey - accessToken added to map" - ); - tokenKeys.accessToken.push(key); - } - break; - case CredentialType.REFRESH_TOKEN: - if (tokenKeys.refreshToken.indexOf(key) === -1) { - this.logger.info( - "BrowserCacheManager: addTokenKey - refreshToken added to map" - ); - tokenKeys.refreshToken.push(key); - } - break; - default: - this.logger.error( - `BrowserCacheManager:addTokenKey - CredentialType provided invalid. CredentialType: ${type}` - ); - throw createClientAuthError( - ClientAuthErrorCodes.unexpectedCredentialType - ); + const refreshRemoval = tokenKeys.refreshToken.indexOf(key); + if (refreshRemoval > -1) { + this.logger.info("refreshToken removed from tokenKeys map"); + tokenKeys.refreshToken.splice(refreshRemoval, 1); + this.setTokenKeys(tokenKeys, correlationId); } - - this.browserStorage.setItem( - `${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}`, - JSON.stringify(tokenKeys) - ); } /** - * Removes the given key from the token key map - * @param key - * @param type + * Gets the keys for the cached tokens associated with this clientId + * @returns */ - removeTokenKey(key: string, type: CredentialType): void { - this.logger.trace("BrowserCacheManager removeTokenKey called"); - const tokenKeys = this.getTokenKeys(); + getTokenKeys(): TokenKeys { + return getTokenKeys(this.clientId, this.browserStorage); + } - switch (type) { - case CredentialType.ID_TOKEN: - this.logger.infoPii( - `BrowserCacheManager: removeTokenKey - attempting to remove idToken with key: ${key} from map` - ); - const idRemoval = tokenKeys.idToken.indexOf(key); - if (idRemoval > -1) { - this.logger.info( - "BrowserCacheManager: removeTokenKey - idToken removed from map" - ); - tokenKeys.idToken.splice(idRemoval, 1); - } else { - this.logger.info( - "BrowserCacheManager: removeTokenKey - idToken does not exist in map. Either it was previously removed or it was never added." - ); - } - break; - case CredentialType.ACCESS_TOKEN: - this.logger.infoPii( - `BrowserCacheManager: removeTokenKey - attempting to remove accessToken with key: ${key} from map` - ); - const accessRemoval = tokenKeys.accessToken.indexOf(key); - if (accessRemoval > -1) { - this.logger.info( - "BrowserCacheManager: removeTokenKey - accessToken removed from map" - ); - tokenKeys.accessToken.splice(accessRemoval, 1); - } else { - this.logger.info( - "BrowserCacheManager: removeTokenKey - accessToken does not exist in map. Either it was previously removed or it was never added." - ); - } - break; - case CredentialType.REFRESH_TOKEN: - this.logger.infoPii( - `BrowserCacheManager: removeTokenKey - attempting to remove refreshToken with key: ${key} from map` - ); - const refreshRemoval = tokenKeys.refreshToken.indexOf(key); - if (refreshRemoval > -1) { - this.logger.info( - "BrowserCacheManager: removeTokenKey - refreshToken removed from map" - ); - tokenKeys.refreshToken.splice(refreshRemoval, 1); - } else { - this.logger.info( - "BrowserCacheManager: removeTokenKey - refreshToken does not exist in map. Either it was previously removed or it was never added." - ); - } - break; - default: - this.logger.error( - `BrowserCacheManager:removeTokenKey - CredentialType provided invalid. CredentialType: ${type}` - ); - throw createClientAuthError( - ClientAuthErrorCodes.unexpectedCredentialType - ); + /** + * Stores the token keys in the cache + * @param tokenKeys + * @param correlationId + * @returns + */ + setTokenKeys(tokenKeys: TokenKeys, correlationId: string): void { + if ( + tokenKeys.idToken.length === 0 && + tokenKeys.accessToken.length === 0 && + tokenKeys.refreshToken.length === 0 + ) { + // If no keys left, remove the map + this.removeItem(`${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}`); + return; + } else { + this.setItem( + `${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}`, + JSON.stringify(tokenKeys), + correlationId + ); } - - this.browserStorage.setItem( - `${StaticCacheKeys.TOKEN_KEYS}.${this.clientId}`, - JSON.stringify(tokenKeys) - ); } /** * generates idToken entity from a string * @param idTokenKey */ - getIdTokenCredential(idTokenKey: string): IdTokenEntity | null { + getIdTokenCredential( + idTokenKey: string, + correlationId: string + ): IdTokenEntity | null { const value = this.browserStorage.getUserData(idTokenKey); if (!value) { this.logger.trace( "BrowserCacheManager.getIdTokenCredential: called, no cache hit" ); - this.removeTokenKey(idTokenKey, CredentialType.ID_TOKEN); + this.removeIdToken(idTokenKey, correlationId); return null; } @@ -479,7 +576,6 @@ export class BrowserCacheManager extends CacheManager { this.logger.trace( "BrowserCacheManager.getIdTokenCredential: called, no cache hit" ); - this.removeTokenKey(idTokenKey, CredentialType.ID_TOKEN); return null; } @@ -499,28 +595,40 @@ export class BrowserCacheManager extends CacheManager { ): Promise { this.logger.trace("BrowserCacheManager.setIdTokenCredential called"); const idTokenKey = CacheHelpers.generateCredentialKey(idToken); + const timestamp = Date.now().toString(); + idToken.lastUpdatedAt = timestamp; + + await this.setUserData( + idTokenKey, + JSON.stringify(idToken), + correlationId, + timestamp + ); - await invokeAsync( - this.browserStorage.setUserData.bind(this.browserStorage), - PerformanceEvents.SetUserData, - this.logger, - this.performanceClient - )(idTokenKey, JSON.stringify(idToken), correlationId); - - this.addTokenKey(idTokenKey, CredentialType.ID_TOKEN); + const tokenKeys = this.getTokenKeys(); + if (tokenKeys.idToken.indexOf(idTokenKey) === -1) { + this.logger.info( + "BrowserCacheManager: addTokenKey - idToken added to map" + ); + tokenKeys.idToken.push(idTokenKey); + this.setTokenKeys(tokenKeys, correlationId); + } } /** * generates accessToken entity from a string * @param key */ - getAccessTokenCredential(accessTokenKey: string): AccessTokenEntity | null { + getAccessTokenCredential( + accessTokenKey: string, + correlationId: string + ): AccessTokenEntity | null { const value = this.browserStorage.getUserData(accessTokenKey); if (!value) { this.logger.trace( "BrowserCacheManager.getAccessTokenCredential: called, no cache hit" ); - this.removeTokenKey(accessTokenKey, CredentialType.ACCESS_TOKEN); + this.removeAccessTokenKeys([accessTokenKey], correlationId); return null; } const parsedAccessToken = this.validateAndParseJson(value); @@ -531,7 +639,6 @@ export class BrowserCacheManager extends CacheManager { this.logger.trace( "BrowserCacheManager.getAccessTokenCredential: called, no cache hit" ); - this.removeTokenKey(accessTokenKey, CredentialType.ACCESS_TOKEN); return null; } @@ -553,14 +660,26 @@ export class BrowserCacheManager extends CacheManager { "BrowserCacheManager.setAccessTokenCredential called" ); const accessTokenKey = CacheHelpers.generateCredentialKey(accessToken); - await invokeAsync( - this.browserStorage.setUserData.bind(this.browserStorage), - PerformanceEvents.SetUserData, - this.logger, - this.performanceClient - )(accessTokenKey, JSON.stringify(accessToken), correlationId); - - this.addTokenKey(accessTokenKey, CredentialType.ACCESS_TOKEN); + const timestamp = Date.now().toString(); + accessToken.lastUpdatedAt = timestamp; + + await this.setUserData( + accessTokenKey, + JSON.stringify(accessToken), + correlationId, + timestamp + ); + + const tokenKeys = this.getTokenKeys(); + const index = tokenKeys.accessToken.indexOf(accessTokenKey); + if (index !== -1) { + tokenKeys.accessToken.splice(index, 1); // Remove existing key before pushing to the end + } + this.logger.trace( + `access token ${index === -1 ? "added to" : "updated in"} map` + ); + tokenKeys.accessToken.push(accessTokenKey); + this.setTokenKeys(tokenKeys, correlationId); } /** @@ -568,14 +687,15 @@ export class BrowserCacheManager extends CacheManager { * @param refreshTokenKey */ getRefreshTokenCredential( - refreshTokenKey: string + refreshTokenKey: string, + correlationId: string ): RefreshTokenEntity | null { const value = this.browserStorage.getUserData(refreshTokenKey); if (!value) { this.logger.trace( "BrowserCacheManager.getRefreshTokenCredential: called, no cache hit" ); - this.removeTokenKey(refreshTokenKey, CredentialType.REFRESH_TOKEN); + this.removeRefreshToken(refreshTokenKey, correlationId); return null; } const parsedRefreshToken = this.validateAndParseJson(value); @@ -586,7 +706,6 @@ export class BrowserCacheManager extends CacheManager { this.logger.trace( "BrowserCacheManager.getRefreshTokenCredential: called, no cache hit" ); - this.removeTokenKey(refreshTokenKey, CredentialType.REFRESH_TOKEN); return null; } @@ -609,14 +728,24 @@ export class BrowserCacheManager extends CacheManager { ); const refreshTokenKey = CacheHelpers.generateCredentialKey(refreshToken); - await invokeAsync( - this.browserStorage.setUserData.bind(this.browserStorage), - PerformanceEvents.SetUserData, - this.logger, - this.performanceClient - )(refreshTokenKey, JSON.stringify(refreshToken), correlationId); - - this.addTokenKey(refreshTokenKey, CredentialType.REFRESH_TOKEN); + const timestamp = Date.now().toString(); + refreshToken.lastUpdatedAt = timestamp; + + await this.setUserData( + refreshTokenKey, + JSON.stringify(refreshToken), + correlationId, + timestamp + ); + + const tokenKeys = this.getTokenKeys(); + if (tokenKeys.refreshToken.indexOf(refreshTokenKey) === -1) { + this.logger.info( + "BrowserCacheManager: addTokenKey - refreshToken added to map" + ); + tokenKeys.refreshToken.push(refreshTokenKey); + this.setTokenKeys(tokenKeys, correlationId); + } } /** @@ -651,12 +780,16 @@ export class BrowserCacheManager extends CacheManager { * set appMetadata entity to the platform cache * @param appMetadata */ - setAppMetadata(appMetadata: AppMetadataEntity): void { + setAppMetadata( + appMetadata: AppMetadataEntity, + correlationId: string + ): void { this.logger.trace("BrowserCacheManager.setAppMetadata called"); const appMetadataKey = CacheHelpers.generateAppMetadataKey(appMetadata); - this.browserStorage.setItem( + this.setItem( appMetadataKey, - JSON.stringify(appMetadata) + JSON.stringify(appMetadata), + correlationId ); } @@ -699,12 +832,14 @@ export class BrowserCacheManager extends CacheManager { */ setServerTelemetry( serverTelemetryKey: string, - serverTelemetry: ServerTelemetryEntity + serverTelemetry: ServerTelemetryEntity, + correlationId: string ): void { this.logger.trace("BrowserCacheManager.setServerTelemetry called"); - this.browserStorage.setItem( + this.setItem( serverTelemetryKey, - JSON.stringify(serverTelemetry) + JSON.stringify(serverTelemetry), + correlationId ); } @@ -780,7 +915,7 @@ export class BrowserCacheManager extends CacheManager { /** * Gets the active account */ - getActiveAccount(): AccountInfo | null { + getActiveAccount(correlationId: string): AccountInfo | null { const activeAccountKeyFilters = this.generateCacheKey( PersistentCacheKeys.ACTIVE_ACCOUNT_FILTERS ); @@ -800,11 +935,14 @@ export class BrowserCacheManager extends CacheManager { this.logger.trace( "BrowserCacheManager.getActiveAccount: Active account filters schema found" ); - return this.getAccountInfoFilteredBy({ - homeAccountId: activeAccountValueObj.homeAccountId, - localAccountId: activeAccountValueObj.localAccountId, - tenantId: activeAccountValueObj.tenantId, - }); + return this.getAccountInfoFilteredBy( + { + homeAccountId: activeAccountValueObj.homeAccountId, + localAccountId: activeAccountValueObj.localAccountId, + tenantId: activeAccountValueObj.tenantId, + }, + correlationId + ); } this.logger.trace( "BrowserCacheManager.getActiveAccount: No active account found" @@ -816,7 +954,7 @@ export class BrowserCacheManager extends CacheManager { * Sets the active account's localAccountId in cache * @param account */ - setActiveAccount(account: AccountInfo | null): void { + setActiveAccount(account: AccountInfo | null, correlationId: string): void { const activeAccountKey = this.generateCacheKey( PersistentCacheKeys.ACTIVE_ACCOUNT_FILTERS ); @@ -826,10 +964,12 @@ export class BrowserCacheManager extends CacheManager { homeAccountId: account.homeAccountId, localAccountId: account.localAccountId, tenantId: account.tenantId, + lastUpdatedAt: TimeUtils.nowSeconds().toString(), }; - this.browserStorage.setItem( + this.setItem( activeAccountKey, - JSON.stringify(activeAccountValue) + JSON.stringify(activeAccountValue), + correlationId ); } else { this.logger.verbose( @@ -878,12 +1018,14 @@ export class BrowserCacheManager extends CacheManager { */ setThrottlingCache( throttlingCacheKey: string, - throttlingCache: ThrottlingEntity + throttlingCache: ThrottlingEntity, + correlationId: string ): void { this.logger.trace("BrowserCacheManager.setThrottlingCache called"); - this.browserStorage.setItem( + this.setItem( throttlingCacheKey, - JSON.stringify(throttlingCache) + JSON.stringify(throttlingCache), + correlationId ); } @@ -991,10 +1133,10 @@ export class BrowserCacheManager extends CacheManager { /** * Clears all cache entries created by MSAL. */ - async clear(): Promise { + clear(correlationId: string): void { // Removes all accounts and their credentials - await this.removeAllAccounts(); - this.removeAppMetadata(); + this.removeAllAccounts(correlationId); + this.removeAppMetadata(correlationId); // Remove temp storage first to make sure any cookies are cleared this.temporaryCacheStorage.getKeys().forEach((cacheKey: string) => { @@ -1025,34 +1167,33 @@ export class BrowserCacheManager extends CacheManager { * @param correlationId {string} correlation id * @returns */ - async clearTokensAndKeysWithClaims( - performanceClient: IPerformanceClient, - correlationId: string - ): Promise { - performanceClient.addQueueMeasurement( + clearTokensAndKeysWithClaims(correlationId: string): void { + this.performanceClient.addQueueMeasurement( PerformanceEvents.ClearTokensAndKeysWithClaims, correlationId ); const tokenKeys = this.getTokenKeys(); - - const removedAccessTokens: Array> = []; + let removedAccessTokens = 0; tokenKeys.accessToken.forEach((key: string) => { // if the access token has claims in its key, remove the token key and the token - const credential = this.getAccessTokenCredential(key); + const credential = this.getAccessTokenCredential( + key, + correlationId + ); if ( credential?.requestedClaimsHash && key.includes(credential.requestedClaimsHash.toLowerCase()) ) { - removedAccessTokens.push(this.removeAccessToken(key)); + this.removeAccessToken(key, correlationId); + removedAccessTokens++; } }); - await Promise.all(removedAccessTokens); // warn if any access tokens are removed - if (removedAccessTokens.length > 0) { + if (removedAccessTokens > 0) { this.logger.warning( - `${removedAccessTokens.length} access tokens with claims in the cache keys have been removed from the cache.` + `${removedAccessTokens} access tokens with claims in the cache keys have been removed from the cache.` ); } } @@ -1074,143 +1215,24 @@ export class BrowserCacheManager extends CacheManager { return JSON.stringify(key); } - /** - * Create authorityKey to cache authority - * @param state - */ - generateAuthorityKey(stateString: string): string { - const { - libraryState: { id: stateId }, - } = ProtocolUtils.parseRequestState(this.cryptoImpl, stateString); - - return this.generateCacheKey( - `${TemporaryCacheKeys.AUTHORITY}.${stateId}` - ); - } - - /** - * Create Nonce key to cache nonce - * @param state - */ - generateNonceKey(stateString: string): string { - const { - libraryState: { id: stateId }, - } = ProtocolUtils.parseRequestState(this.cryptoImpl, stateString); - - return this.generateCacheKey( - `${TemporaryCacheKeys.NONCE_IDTOKEN}.${stateId}` - ); - } - - /** - * Creates full cache key for the request state - * @param stateString State string for the request - */ - generateStateKey(stateString: string): string { - // Use the library state id to key temp storage for uniqueness for multiple concurrent requests - const { - libraryState: { id: stateId }, - } = ProtocolUtils.parseRequestState(this.cryptoImpl, stateString); - return this.generateCacheKey( - `${TemporaryCacheKeys.REQUEST_STATE}.${stateId}` - ); - } - - /** - * Gets the cached authority based on the cached state. Returns empty if no cached state found. - */ - getCachedAuthority(cachedState: string): string | null { - const stateCacheKey = this.generateStateKey(cachedState); - const state = this.getTemporaryCache(stateCacheKey); - if (!state) { - return null; - } - - const authorityCacheKey = this.generateAuthorityKey(state); - return this.getTemporaryCache(authorityCacheKey); - } - - /** - * Updates account, authority, and state in cache - * @param serverAuthenticationRequest - * @param account - */ - updateCacheEntries( - state: string, - nonce: string, - authorityInstance: string, - loginHint: string, - account: AccountInfo | null - ): void { - this.logger.trace("BrowserCacheManager.updateCacheEntries called"); - // Cache the request state - const stateCacheKey = this.generateStateKey(state); - this.setTemporaryCache(stateCacheKey, state, false); - - // Cache the nonce - const nonceCacheKey = this.generateNonceKey(state); - this.setTemporaryCache(nonceCacheKey, nonce, false); - - // Cache authorityKey - const authorityCacheKey = this.generateAuthorityKey(state); - this.setTemporaryCache(authorityCacheKey, authorityInstance, false); - - if (account) { - const ccsCredential: CcsCredential = { - credential: account.homeAccountId, - type: CcsCredentialType.HOME_ACCOUNT_ID, - }; - this.setTemporaryCache( - TemporaryCacheKeys.CCS_CREDENTIAL, - JSON.stringify(ccsCredential), - true - ); - } else if (loginHint) { - const ccsCredential: CcsCredential = { - credential: loginHint, - type: CcsCredentialType.UPN, - }; - this.setTemporaryCache( - TemporaryCacheKeys.CCS_CREDENTIAL, - JSON.stringify(ccsCredential), - true - ); - } - } - /** * Reset all temporary cache items * @param state */ - resetRequestCache(state: string): void { + resetRequestCache(): void { this.logger.trace("BrowserCacheManager.resetRequestCache called"); - // check state and remove associated cache items - if (state) { - this.temporaryCacheStorage.getKeys().forEach((key) => { - if (key.indexOf(state) !== -1) { - this.removeTemporaryItem(key); - } - }); - // delete generic interactive request parameters - this.removeTemporaryItem(this.generateStateKey(state)); - this.removeTemporaryItem(this.generateNonceKey(state)); - this.removeTemporaryItem(this.generateAuthorityKey(state)); - } this.removeTemporaryItem( this.generateCacheKey(TemporaryCacheKeys.REQUEST_PARAMS) ); this.removeTemporaryItem( - this.generateCacheKey(TemporaryCacheKeys.ORIGIN_URI) + this.generateCacheKey(TemporaryCacheKeys.VERIFIER) ); this.removeTemporaryItem( - this.generateCacheKey(TemporaryCacheKeys.URL_HASH) - ); - this.removeTemporaryItem( - this.generateCacheKey(TemporaryCacheKeys.CORRELATION_ID) + this.generateCacheKey(TemporaryCacheKeys.ORIGIN_URI) ); this.removeTemporaryItem( - this.generateCacheKey(TemporaryCacheKeys.CCS_CREDENTIAL) + this.generateCacheKey(TemporaryCacheKeys.URL_HASH) ); this.removeTemporaryItem( this.generateCacheKey(TemporaryCacheKeys.NATIVE_REQUEST) @@ -1218,64 +1240,11 @@ export class BrowserCacheManager extends CacheManager { this.setInteractionInProgress(false); } - /** - * Removes temporary cache for the provided state - * @param stateString - */ - cleanRequestByState(stateString: string): void { - this.logger.trace("BrowserCacheManager.cleanRequestByState called"); - // Interaction is completed - remove interaction status. - if (stateString) { - const stateKey = this.generateStateKey(stateString); - const cachedState = this.temporaryCacheStorage.getItem(stateKey); - this.logger.infoPii( - `BrowserCacheManager.cleanRequestByState: Removing temporary cache items for state: ${cachedState}` - ); - this.resetRequestCache(cachedState || Constants.EMPTY_STRING); - } - } - - /** - * Looks in temporary cache for any state values with the provided interactionType and removes all temporary cache items for that state - * Used in scenarios where temp cache needs to be cleaned but state is not known, such as clicking browser back button. - * @param interactionType - */ - cleanRequestByInteractionType(interactionType: InteractionType): void { - this.logger.trace( - "BrowserCacheManager.cleanRequestByInteractionType called" - ); - // Loop through all keys to find state key - this.temporaryCacheStorage.getKeys().forEach((key) => { - // If this key is not the state key, move on - if (key.indexOf(TemporaryCacheKeys.REQUEST_STATE) === -1) { - return; - } - - // Retrieve state value, return if not a valid value - const stateValue = this.temporaryCacheStorage.getItem(key); - if (!stateValue) { - return; - } - // Extract state and ensure it matches given InteractionType, then clean request cache - const parsedState = extractBrowserRequestState( - this.cryptoImpl, - stateValue - ); - if ( - parsedState && - parsedState.interactionType === interactionType - ) { - this.logger.infoPii( - `BrowserCacheManager.cleanRequestByInteractionType: Removing temporary cache items for state: ${stateValue}` - ); - this.resetRequestCache(stateValue); - } - }); - this.setInteractionInProgress(false); - } - - cacheCodeRequest(authCodeRequest: CommonAuthorizationCodeRequest): void { - this.logger.trace("BrowserCacheManager.cacheCodeRequest called"); + cacheAuthorizeRequest( + authCodeRequest: CommonAuthorizationUrlRequest, + codeVerifier?: string + ): void { + this.logger.trace("BrowserCacheManager.cacheAuthorizeRequest called"); const encodedValue = base64Encode(JSON.stringify(authCodeRequest)); this.setTemporaryCache( @@ -1283,12 +1252,21 @@ export class BrowserCacheManager extends CacheManager { encodedValue, true ); + + if (codeVerifier) { + const encodedVerifier = base64Encode(codeVerifier); + this.setTemporaryCache( + TemporaryCacheKeys.VERIFIER, + encodedVerifier, + true + ); + } } /** * Gets the token exchange parameters from the cache. Throws an error if nothing is found. */ - getCachedRequest(state: string): CommonAuthorizationCodeRequest { + getCachedRequest(): [CommonAuthorizationUrlRequest, string] { this.logger.trace("BrowserCacheManager.getCachedRequest called"); // Get token request from cache and parse as TokenExchangeParameters. const encodedTokenRequest = this.getTemporaryCache( @@ -1300,10 +1278,18 @@ export class BrowserCacheManager extends CacheManager { BrowserAuthErrorCodes.noTokenRequestCacheError ); } + const encodedVerifier = this.getTemporaryCache( + TemporaryCacheKeys.VERIFIER, + true + ); - let parsedRequest: CommonAuthorizationCodeRequest; + let parsedRequest: CommonAuthorizationUrlRequest; + let verifier = ""; try { parsedRequest = JSON.parse(base64Decode(encodedTokenRequest)); + if (encodedVerifier) { + verifier = base64Decode(encodedVerifier); + } } catch (e) { this.logger.errorPii(`Attempted to parse: ${encodedTokenRequest}`); this.logger.error( @@ -1313,29 +1299,14 @@ export class BrowserCacheManager extends CacheManager { BrowserAuthErrorCodes.unableToParseTokenRequestCacheError ); } - this.removeTemporaryItem( - this.generateCacheKey(TemporaryCacheKeys.REQUEST_PARAMS) - ); - - // Get cached authority and use if no authority is cached with request. - if (!parsedRequest.authority) { - const authorityCacheKey: string = this.generateAuthorityKey(state); - const cachedAuthority = this.getTemporaryCache(authorityCacheKey); - if (!cachedAuthority) { - throw createBrowserAuthError( - BrowserAuthErrorCodes.noCachedAuthorityError - ); - } - parsedRequest.authority = cachedAuthority; - } - return parsedRequest; + return [parsedRequest, verifier]; } /** * Gets cached native request for redirect flows */ - getCachedNativeRequest(): NativeTokenRequest | null { + getCachedNativeRequest(): PlatformAuthRequest | null { this.logger.trace("BrowserCacheManager.getCachedNativeRequest called"); const cachedRequest = this.getTemporaryCache( TemporaryCacheKeys.NATIVE_REQUEST, @@ -1350,7 +1321,7 @@ export class BrowserCacheManager extends CacheManager { const parsedRequest = this.validateAndParseJson( cachedRequest - ) as NativeTokenRequest; + ) as PlatformAuthRequest; if (!parsedRequest) { this.logger.error( "BrowserCacheManager.getCachedNativeRequest: Unable to parse native request" @@ -1362,7 +1333,7 @@ export class BrowserCacheManager extends CacheManager { } isInteractionInProgress(matchClientId?: boolean): boolean { - const clientId = this.getInteractionInProgress(); + const clientId = this.getInteractionInProgress()?.clientId; if (matchClientId) { return clientId === this.clientId; @@ -1371,12 +1342,30 @@ export class BrowserCacheManager extends CacheManager { } } - getInteractionInProgress(): string | null { + getInteractionInProgress(): { + clientId: string; + type: INTERACTION_TYPE; + } | null { const key = `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`; - return this.getTemporaryCache(key, false); + const value = this.getTemporaryCache(key, false); + try { + return value ? JSON.parse(value) : null; + } catch (e) { + // Remove interaction and other temp keys if interaction status can't be parsed + this.logger.error( + `Cannot parse interaction status. Removing temporary cache items and clearing url hash. Retrying interaction should fix the error` + ); + this.removeTemporaryItem(key); + this.resetRequestCache(); + clearHash(window); + return null; + } } - setInteractionInProgress(inProgress: boolean): void { + setInteractionInProgress( + inProgress: boolean, + type: INTERACTION_TYPE = INTERACTION_TYPE.SIGNIN + ): void { // Ensure we don't overwrite interaction in progress for a different clientId const key = `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`; if (inProgress) { @@ -1386,11 +1375,15 @@ export class BrowserCacheManager extends CacheManager { ); } else { // No interaction is in progress - this.setTemporaryCache(key, this.clientId, false); + this.setTemporaryCache( + key, + JSON.stringify({ clientId: this.clientId, type }), + false + ); } } else if ( !inProgress && - this.getInteractionInProgress() === this.clientId + this.getInteractionInProgress()?.clientId === this.clientId ) { this.removeTemporaryItem(key); } diff --git a/lib/msal-browser/src/cache/ITokenCache.ts b/lib/msal-browser/src/cache/ITokenCache.ts index 1a8ee147c4..9032383d1d 100644 --- a/lib/msal-browser/src/cache/ITokenCache.ts +++ b/lib/msal-browser/src/cache/ITokenCache.ts @@ -3,10 +3,10 @@ * Licensed under the MIT License. */ -import { ExternalTokenResponse } from "@azure/msal-common/browser"; -import { SilentRequest } from "../request/SilentRequest.js"; -import { LoadTokenOptions } from "./TokenCache.js"; -import { AuthenticationResult } from "../response/AuthenticationResult.js"; +import type { ExternalTokenResponse } from "@azure/msal-common/browser"; +import type { SilentRequest } from "../request/SilentRequest.js"; +import type { LoadTokenOptions } from "./TokenCache.js"; +import type { AuthenticationResult } from "../response/AuthenticationResult.js"; export interface ITokenCache { /** diff --git a/lib/msal-browser/src/cache/IWindowStorage.ts b/lib/msal-browser/src/cache/IWindowStorage.ts index 73e76c3323..912985d89f 100644 --- a/lib/msal-browser/src/cache/IWindowStorage.ts +++ b/lib/msal-browser/src/cache/IWindowStorage.ts @@ -29,7 +29,12 @@ export interface IWindowStorage { /** * Setter for sensitive data that may contain PII. */ - setUserData(key: string, value: T, correlationId: string): Promise; + setUserData( + key: string, + value: T, + correlationId: string, + timestamp: string + ): Promise; /** * Removes the item in the window storage object matching the given key. diff --git a/lib/msal-browser/src/cache/LocalStorage.ts b/lib/msal-browser/src/cache/LocalStorage.ts index e55fcf3e18..dd4abd2283 100644 --- a/lib/msal-browser/src/cache/LocalStorage.ts +++ b/lib/msal-browser/src/cache/LocalStorage.ts @@ -47,6 +47,7 @@ type EncryptedData = { id: string; nonce: string; data: string; + lastUpdatedAt: string; }; export class LocalStorage implements IWindowStorage { @@ -77,8 +78,6 @@ export class LocalStorage implements IWindowStorage { } async initialize(correlationId: string): Promise { - this.initialized = true; - const cookies = new CookieStorage(); const cookieString = cookies.getItem(ENCRYPTION_KEY); let parsedCookie = { key: "", id: "" }; @@ -158,6 +157,8 @@ export class LocalStorage implements IWindowStorage { // Register listener for cache updates in other tabs this.broadcast.addEventListener("message", this.updateCache.bind(this)); + + this.initialized = true; } getItem(key: string): string | null { @@ -180,7 +181,8 @@ export class LocalStorage implements IWindowStorage { async setUserData( key: string, value: string, - correlationId: string + correlationId: string, + timestamp: string ): Promise { if (!this.initialized || !this.encryptionCookie) { throw createBrowserAuthError( @@ -199,6 +201,7 @@ export class LocalStorage implements IWindowStorage { id: this.encryptionCookie.id, nonce: nonce, data: data, + lastUpdatedAt: timestamp, }; this.memoryStorage.setItem(key, value); diff --git a/lib/msal-browser/src/cache/TokenCache.ts b/lib/msal-browser/src/cache/TokenCache.ts index 9d6ec5b548..86ce6fd632 100644 --- a/lib/msal-browser/src/cache/TokenCache.ts +++ b/lib/msal-browser/src/cache/TokenCache.ts @@ -22,14 +22,14 @@ import { TimeUtils, } from "@azure/msal-common/browser"; import { BrowserConfiguration } from "../config/Configuration.js"; -import { SilentRequest } from "../request/SilentRequest.js"; +import type { SilentRequest } from "../request/SilentRequest.js"; import { BrowserCacheManager } from "./BrowserCacheManager.js"; -import { ITokenCache } from "./ITokenCache.js"; +import type { ITokenCache } from "./ITokenCache.js"; import { createBrowserAuthError, BrowserAuthErrorCodes, } from "../error/BrowserAuthError.js"; -import { AuthenticationResult } from "../response/AuthenticationResult.js"; +import type { AuthenticationResult } from "../response/AuthenticationResult.js"; import { base64Decode } from "../encode/Base64Decode.js"; import * as BrowserCrypto from "../crypto/BrowserCrypto.js"; @@ -210,6 +210,7 @@ export class TokenCache implements ITokenCache { authority, homeAccountId, base64Decode, + correlationId, idTokenClaims, clientInfo, authority.hostnameAndPort, diff --git a/lib/msal-browser/src/config/Configuration.ts b/lib/msal-browser/src/config/Configuration.ts index 75f352f648..8f5175eeab 100644 --- a/lib/msal-browser/src/config/Configuration.ts +++ b/lib/msal-browser/src/config/Configuration.ts @@ -108,6 +108,11 @@ export type BrowserAuthOptions = { * Flag of whether the STS will send back additional parameters to specify where the tokens should be retrieved from. */ instanceAware?: boolean; + /** + * Flag of whether to encode query parameters + * @deprecated This flag is deprecated and will be removed in the next major version where all extra query params will be encoded by default. + */ + encodeExtraQueryParams?: boolean; }; /** @internal */ @@ -129,23 +134,27 @@ export type CacheOptions = { cacheLocation?: BrowserCacheLocation | string; /** * Used to specify the temporaryCacheLocation user wants to set. Valid values are "localStorage", "sessionStorage" and "memoryStorage". + * @deprecated This option is deprecated and will be removed in the next major version. */ temporaryCacheLocation?: BrowserCacheLocation | string; /** * If set, MSAL stores the auth request state required for validation of the auth flows in the browser cookies. By default this flag is set to false. + * @deprecated This option is deprecated and will be removed in the next major version. */ storeAuthStateInCookie?: boolean; /** * If set, MSAL sets the "Secure" flag on cookies so they can only be sent over HTTPS. By default this flag is set to true. - * @deprecated This option will be removed in a future major version and all cookies set will include the Secure attribute. + * @deprecated This option will be removed in the next major version and all cookies set will include the Secure attribute. */ secureCookies?: boolean; /** * If set, MSAL will attempt to migrate cache entries from older versions on initialization. By default this flag is set to true if cacheLocation is localStorage, otherwise false. + * @deprecated This option is deprecated and will be removed in the next major version. */ cacheMigrationEnabled?: boolean; /** * Flag that determines whether access tokens are stored based on requested claims + * @deprecated This option is deprecated and will be removed in the next major version. */ claimsBasedCachingEnabled?: boolean; }; @@ -296,6 +305,7 @@ export function buildConfiguration( skipAuthorityMetadataCache: false, supportsNestedAppAuth: false, instanceAware: false, + encodeExtraQueryParams: false, }; // Default cache options for browser @@ -377,10 +387,10 @@ export function buildConfiguration( ); } - // Throw an error if user has set allowPlatformBroker to true without being in AAD protocol mode + // Throw an error if user has set allowPlatformBroker to true with OIDC protocol mode if ( userInputAuth?.protocolMode && - userInputAuth.protocolMode !== ProtocolMode.AAD && + userInputAuth.protocolMode === ProtocolMode.OIDC && providedSystemOptions?.allowPlatformBroker ) { throw createClientConfigurationError( diff --git a/lib/msal-browser/src/controllers/IController.ts b/lib/msal-browser/src/controllers/IController.ts index 856f4abc38..05d7bfb7dd 100644 --- a/lib/msal-browser/src/controllers/IController.ts +++ b/lib/msal-browser/src/controllers/IController.ts @@ -29,7 +29,10 @@ import { EventType } from "../event/EventType.js"; export interface IController { // TODO: Make request mandatory in the next major version? - initialize(request?: InitializeApplicationRequest): Promise; + initialize( + request?: InitializeApplicationRequest, + isBroker?: boolean + ): Promise; acquireTokenPopup(request: PopupRequest): Promise; diff --git a/lib/msal-browser/src/controllers/NestedAppAuthController.ts b/lib/msal-browser/src/controllers/NestedAppAuthController.ts index 609302c140..d2d9cc8645 100644 --- a/lib/msal-browser/src/controllers/NestedAppAuthController.ts +++ b/lib/msal-browser/src/controllers/NestedAppAuthController.ts @@ -159,7 +159,11 @@ export class NestedAppAuthController implements IController { * Specific implementation of initialize function for NestedAppAuthController * @returns */ - async initialize(request?: InitializeApplicationRequest): Promise { + async initialize( + request?: InitializeApplicationRequest, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + isBroker?: boolean + ): Promise { const initCorrelationId = request?.correlationId || createNewGuid(); await this.browserStorage.initialize(initCorrelationId); return Promise.resolve(); @@ -225,7 +229,15 @@ export class NestedAppAuthController implements IController { }; // cache the tokens in the response - await this.hydrateCache(result, request); + try { + // cache hydration can fail in JS Runtime scenario that doesn't support full crypto API + await this.hydrateCache(result, request); + } catch (error) { + this.logger.warningPii( + `Failed to hydrate cache. Error: ${error}`, + validRequest.correlationId + ); + } // cache the account context in memory after successful token fetch this.currentAccountContext = { @@ -328,7 +340,15 @@ export class NestedAppAuthController implements IController { ); // cache the tokens in the response - await this.hydrateCache(result, request); + try { + // cache hydration can fail in JS Runtime scenario that doesn't support full crypto API + await this.hydrateCache(result, request); + } catch (error) { + this.logger.warningPii( + `Failed to hydrate cache. Error: ${error}`, + validRequest.correlationId + ); + } // cache the account context in memory after successful token fetch this.currentAccountContext = { @@ -437,7 +457,7 @@ export class NestedAppAuthController implements IController { return result; } - this.logger.error( + this.logger.warning( "Cached tokens are not found for the account, proceeding with silent token request." ); @@ -465,11 +485,14 @@ export class NestedAppAuthController implements IController { const accountContext = this.bridgeProxy.getAccountContext() || this.currentAccountContext; let currentAccount: AccountInfo | null = null; + const correlationId = + request.correlationId || this.browserCrypto.createNewGuid(); if (accountContext) { currentAccount = AccountManager.getAccount( accountContext, this.logger, - this.browserStorage + this.browserStorage, + correlationId ); } @@ -501,9 +524,7 @@ export class NestedAppAuthController implements IController { currentAccount, authRequest, tokenKeys, - currentAccount.tenantId, - this.performanceClient, - authRequest.correlationId + currentAccount.tenantId ); // If there is no access token, log it and return null @@ -523,10 +544,10 @@ export class NestedAppAuthController implements IController { const cachedIdToken = this.browserStorage.getIdToken( currentAccount, + authRequest.correlationId, tokenKeys, currentAccount.tenantId, - this.performanceClient, - authRequest.correlationId + this.performanceClient ); if (!cachedIdToken) { @@ -599,6 +620,7 @@ export class NestedAppAuthController implements IController { CommonAuthorizationUrlRequest, | "requestedClaimsHash" | "responseMode" + | "earJwk" | "codeChallenge" | "codeChallengeMethod" | "platformBroker" @@ -669,10 +691,12 @@ export class NestedAppAuthController implements IController { * @returns Array of AccountInfo objects in cache */ getAllAccounts(accountFilter?: AccountFilter): AccountInfo[] { + const correlationId = this.browserCrypto.createNewGuid(); return AccountManager.getAllAccounts( this.logger, this.browserStorage, this.isBrowserEnv(), + correlationId, accountFilter ); } @@ -683,10 +707,12 @@ export class NestedAppAuthController implements IController { * @returns The first account found in the cache matching the provided filter or null if no account could be found. */ getAccount(accountFilter: AccountFilter): AccountInfo | null { + const correlationId = this.browserCrypto.createNewGuid(); return AccountManager.getAccount( accountFilter, this.logger, - this.browserStorage + this.browserStorage, + correlationId ); } @@ -699,10 +725,12 @@ export class NestedAppAuthController implements IController { * @returns The account object stored in MSAL */ getAccountByUsername(username: string): AccountInfo | null { + const correlationId = this.browserCrypto.createNewGuid(); return AccountManager.getAccountByUsername( username, this.logger, - this.browserStorage + this.browserStorage, + correlationId ); } @@ -714,10 +742,12 @@ export class NestedAppAuthController implements IController { * @returns The account object stored in MSAL */ getAccountByHomeId(homeAccountId: string): AccountInfo | null { + const correlationId = this.browserCrypto.createNewGuid(); return AccountManager.getAccountByHomeId( homeAccountId, this.logger, - this.browserStorage + this.browserStorage, + correlationId ); } @@ -729,10 +759,12 @@ export class NestedAppAuthController implements IController { * @returns The account object stored in MSAL */ getAccountByLocalId(localAccountId: string): AccountInfo | null { + const correlationId = this.browserCrypto.createNewGuid(); return AccountManager.getAccountByLocalId( localAccountId, this.logger, - this.browserStorage + this.browserStorage, + correlationId ); } @@ -745,14 +777,23 @@ export class NestedAppAuthController implements IController { * StandardController uses this to allow the developer to set the active account * in the nested app auth scenario the active account is controlled by the app hosting the nested app */ - return AccountManager.setActiveAccount(account, this.browserStorage); + const correlationId = this.browserCrypto.createNewGuid(); + return AccountManager.setActiveAccount( + account, + this.browserStorage, + correlationId + ); } /** * Gets the currently active account */ getActiveAccount(): AccountInfo | null { - return AccountManager.getActiveAccount(this.browserStorage); + const correlationId = this.browserCrypto.createNewGuid(); + return AccountManager.getActiveAccount( + this.browserStorage, + correlationId + ); } // #endregion @@ -792,6 +833,7 @@ export class NestedAppAuthController implements IController { CommonAuthorizationUrlRequest, | "requestedClaimsHash" | "responseMode" + | "earJwk" | "codeChallenge" | "codeChallengeMethod" | "platformBroker" diff --git a/lib/msal-browser/src/controllers/StandardController.ts b/lib/msal-browser/src/controllers/StandardController.ts index b54faa6f92..8cbda4d184 100644 --- a/lib/msal-browser/src/controllers/StandardController.ts +++ b/lib/msal-browser/src/controllers/StandardController.ts @@ -23,6 +23,7 @@ import { getRequestThumbprint, AccountEntity, invokeAsync, + invoke, createClientAuthError, ClientAuthErrorCodes, AccountFilter, @@ -41,11 +42,11 @@ import { ApiId, BrowserCacheLocation, WrapperSKU, - TemporaryCacheKeys, CacheLookupPolicy, DEFAULT_REQUEST, BrowserConstants, iFrameRenewalPolicies, + INTERACTION_TYPE, } from "../utils/BrowserConstants.js"; import * as BrowserUtils from "../utils/BrowserUtils.js"; import { RedirectRequest } from "../request/RedirectRequest.js"; @@ -63,8 +64,7 @@ import { SilentIframeClient } from "../interaction_client/SilentIframeClient.js" import { SilentRefreshClient } from "../interaction_client/SilentRefreshClient.js"; import { TokenCache } from "../cache/TokenCache.js"; import { ITokenCache } from "../cache/ITokenCache.js"; -import { NativeInteractionClient } from "../interaction_client/NativeInteractionClient.js"; -import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js"; +import { PlatformAuthInteractionClient } from "../interaction_client/PlatformAuthInteractionClient.js"; import { SilentRequest } from "../request/SilentRequest.js"; import { NativeAuthError, @@ -77,7 +77,7 @@ import { BrowserAuthErrorCodes, } from "../error/BrowserAuthError.js"; import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js"; -import { NativeTokenRequest } from "../broker/nativeBroker/NativeRequest.js"; +import { PlatformAuthRequest } from "../broker/nativeBroker/PlatformAuthRequest.js"; import { StandardOperatingContext } from "../operatingcontext/StandardOperatingContext.js"; import { BaseOperatingContext } from "../operatingcontext/BaseOperatingContext.js"; import { IController } from "./IController.js"; @@ -87,6 +87,12 @@ import { createNewGuid } from "../crypto/BrowserCrypto.js"; import { initializeSilentRequest } from "../request/RequestHelpers.js"; import { InitializeApplicationRequest } from "../request/InitializeApplicationRequest.js"; import { generatePkceCodes } from "../crypto/PkceGenerator.js"; +import { + getPlatformAuthProvider, + isPlatformAuthAllowed, +} from "../broker/nativeBroker/PlatformAuthProvider.js"; +import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js"; +import { collectInstanceStats } from "../utils/MsalFrameStatsUtils.js"; function getAccountType( account?: AccountInfo @@ -156,7 +162,7 @@ export class StandardController implements IController { >; // Native Extension Provider - protected nativeExtensionProvider: NativeMessageHandler | undefined; + protected platformAuthProvider: IPlatformAuthHandler | undefined; // Hybrid auth code responses private hybridAuthCodeResponses: Map>; @@ -313,7 +319,10 @@ export class StandardController implements IController { * Initializer function to perform async startup tasks such as connecting to WAM extension * @param request {?InitializeApplicationRequest} correlation id */ - async initialize(request?: InitializeApplicationRequest): Promise { + async initialize( + request?: InitializeApplicationRequest, + isBroker?: boolean + ): Promise { this.logger.trace("initialize called"); if (this.initialized) { this.logger.info( @@ -338,6 +347,13 @@ export class StandardController implements IController { ); this.eventHandler.emitEvent(EventType.INITIALIZE_START); + // Broker applications are initialized twice, so we avoid double-counting it + if (!isBroker) { + try { + this.logMultipleInstances(initMeasurement); + } catch {} + } + await invokeAsync( this.browserStorage.initialize.bind(this.browserStorage), PerformanceEvents.InitializeCache, @@ -348,12 +364,13 @@ export class StandardController implements IController { if (allowPlatformBroker) { try { - this.nativeExtensionProvider = - await NativeMessageHandler.createProvider( - this.logger, - this.config.system.nativeBrokerHandshakeTimeout, - this.performanceClient - ); + // check if platform authentication is available via DOM or browser extension and create relevant handlers + this.platformAuthProvider = await getPlatformAuthProvider( + this.logger, + this.performanceClient, + initCorrelationId, + this.config.system.nativeBrokerHandshakeTimeout + ); } catch (e) { this.logger.verbose(e as string); } @@ -364,7 +381,7 @@ export class StandardController implements IController { "Claims-based caching is disabled. Clearing the previous cache with claims" ); - await invokeAsync( + invoke( this.browserStorage.clearTokensAndKeysWithClaims.bind( this.browserStorage ), @@ -372,7 +389,7 @@ export class StandardController implements IController { this.logger, this.performanceClient, initCorrelationId - )(this.performanceClient, initCorrelationId); + )(initCorrelationId); } this.config.system.asyncPopups && @@ -437,79 +454,99 @@ export class StandardController implements IController { private async handleRedirectPromiseInternal( hash?: string ): Promise { + if (!this.browserStorage.isInteractionInProgress(true)) { + this.logger.info( + "handleRedirectPromise called but there is no interaction in progress, returning null." + ); + return null; + } + + const interactionType = + this.browserStorage.getInteractionInProgress()?.type; + if (interactionType === INTERACTION_TYPE.SIGNOUT) { + this.logger.verbose( + "handleRedirectPromise removing interaction_in_progress flag and returning null after sign-out" + ); + this.browserStorage.setInteractionInProgress(false); + return Promise.resolve(null); + } + const loggedInAccounts = this.getAllAccounts(); - const request: NativeTokenRequest | null = + const platformBrokerRequest: PlatformAuthRequest | null = this.browserStorage.getCachedNativeRequest(); const useNative = - request && - NativeMessageHandler.isPlatformBrokerAvailable( - this.config, - this.logger, - this.nativeExtensionProvider - ) && - this.nativeExtensionProvider && - !hash; - const correlationId = useNative - ? request?.correlationId - : this.browserStorage.getTemporaryCache( - TemporaryCacheKeys.CORRELATION_ID, - true - ) || ""; - const rootMeasurement = this.performanceClient.startMeasurement( - PerformanceEvents.AcquireTokenRedirect, - correlationId - ); + platformBrokerRequest && this.platformAuthProvider && !hash; + + let rootMeasurement: InProgressPerformanceEvent; + this.eventHandler.emitEvent( EventType.HANDLE_REDIRECT_START, InteractionType.Redirect ); let redirectResponse: Promise; - if (useNative && this.nativeExtensionProvider) { - this.logger.trace( - "handleRedirectPromise - acquiring token from native platform" - ); - const nativeClient = new NativeInteractionClient( - this.config, - this.browserStorage, - this.browserCrypto, - this.logger, - this.eventHandler, - this.navigationClient, - ApiId.handleRedirectPromise, - this.performanceClient, - this.nativeExtensionProvider, - request.accountId, - this.nativeInternalStorage, - request.correlationId - ); + try { + if (useNative && this.platformAuthProvider) { + rootMeasurement = this.performanceClient.startMeasurement( + PerformanceEvents.AcquireTokenRedirect, + platformBrokerRequest?.correlationId || "" + ); + this.logger.trace( + "handleRedirectPromise - acquiring token from native platform" + ); + const nativeClient = new PlatformAuthInteractionClient( + this.config, + this.browserStorage, + this.browserCrypto, + this.logger, + this.eventHandler, + this.navigationClient, + ApiId.handleRedirectPromise, + this.performanceClient, + this.platformAuthProvider, + platformBrokerRequest.accountId, + this.nativeInternalStorage, + platformBrokerRequest.correlationId + ); - redirectResponse = invokeAsync( - nativeClient.handleRedirectPromise.bind(nativeClient), - PerformanceEvents.HandleNativeRedirectPromiseMeasurement, - this.logger, - this.performanceClient, - rootMeasurement.event.correlationId - )(this.performanceClient, rootMeasurement.event.correlationId); - } else { - this.logger.trace( - "handleRedirectPromise - acquiring token from web flow" - ); - const redirectClient = this.createRedirectClient(correlationId); - redirectResponse = invokeAsync( - redirectClient.handleRedirectPromise.bind(redirectClient), - PerformanceEvents.HandleRedirectPromiseMeasurement, - this.logger, - this.performanceClient, - rootMeasurement.event.correlationId - )(hash, rootMeasurement); + redirectResponse = invokeAsync( + nativeClient.handleRedirectPromise.bind(nativeClient), + PerformanceEvents.HandleNativeRedirectPromiseMeasurement, + this.logger, + this.performanceClient, + rootMeasurement.event.correlationId + )(this.performanceClient, rootMeasurement.event.correlationId); + } else { + const [standardRequest, codeVerifier] = + this.browserStorage.getCachedRequest(); + const correlationId = standardRequest.correlationId; + // Reset rootMeasurement now that we have correlationId + rootMeasurement = this.performanceClient.startMeasurement( + PerformanceEvents.AcquireTokenRedirect, + correlationId + ); + this.logger.trace( + "handleRedirectPromise - acquiring token from web flow" + ); + const redirectClient = this.createRedirectClient(correlationId); + redirectResponse = invokeAsync( + redirectClient.handleRedirectPromise.bind(redirectClient), + PerformanceEvents.HandleRedirectPromiseMeasurement, + this.logger, + this.performanceClient, + rootMeasurement.event.correlationId + )(hash, standardRequest, codeVerifier, rootMeasurement); + } + } catch (e) { + this.browserStorage.resetRequestCache(); + throw e; } return redirectResponse .then((result: AuthenticationResult | null) => { if (result) { + this.browserStorage.resetRequestCache(); // Emit login event if number of accounts change - const isLoggingIn = loggedInAccounts.length < this.getAllAccounts().length; if (isLoggingIn) { @@ -555,6 +592,7 @@ export class StandardController implements IController { return result; }) .catch((e) => { + this.browserStorage.resetRequestCache(); const eventError = e as EventError; // Emit login event if there is an account if (loggedInAccounts.length > 0) { @@ -647,7 +685,10 @@ export class StandardController implements IController { const isLoggedIn = this.getAllAccounts().length > 0; try { BrowserUtils.redirectPreflightCheck(this.initialized, this.config); - this.browserStorage.setInteractionInProgress(true); + this.browserStorage.setInteractionInProgress( + true, + INTERACTION_TYPE.SIGNIN + ); if (isLoggedIn) { this.eventHandler.emitEvent( @@ -666,10 +707,10 @@ export class StandardController implements IController { let result: Promise; if ( - this.nativeExtensionProvider && + this.platformAuthProvider && this.canUsePlatformBroker(request) ) { - const nativeClient = new NativeInteractionClient( + const nativeClient = new PlatformAuthInteractionClient( this.config, this.browserStorage, this.browserCrypto, @@ -678,7 +719,7 @@ export class StandardController implements IController { this.navigationClient, ApiId.acquireTokenRedirect, this.performanceClient, - this.nativeExtensionProvider, + this.platformAuthProvider, this.getNativeAccountId(request), this.nativeInternalStorage, correlationId @@ -690,7 +731,7 @@ export class StandardController implements IController { e instanceof NativeAuthError && isFatalNativeAuthError(e) ) { - this.nativeExtensionProvider = undefined; // If extension gets uninstalled during session prevent future requests from continuing to attempt + this.platformAuthProvider = undefined; // If extension gets uninstalled during session prevent future requests from continuing to attempt const redirectClient = this.createRedirectClient(correlationId); return redirectClient.acquireToken(request); @@ -702,7 +743,6 @@ export class StandardController implements IController { this.createRedirectClient(correlationId); return redirectClient.acquireToken(request); } - this.browserStorage.setInteractionInProgress(false); throw e; }); } else { @@ -712,6 +752,7 @@ export class StandardController implements IController { return await result; } catch (e) { + this.browserStorage.resetRequestCache(); atrMeasurement.end({ success: false }, e); if (isLoggedIn) { this.eventHandler.emitEvent( @@ -758,7 +799,10 @@ export class StandardController implements IController { try { this.logger.verbose("acquireTokenPopup called", correlationId); preflightCheck(this.initialized, atPopupMeasurement); - this.browserStorage.setInteractionInProgress(true); + this.browserStorage.setInteractionInProgress( + true, + INTERACTION_TYPE.SIGNIN + ); } catch (e) { // Since this function is syncronous we need to reject return Promise.reject(e); @@ -792,7 +836,6 @@ export class StandardController implements IController { ApiId.acquireTokenPopup ) .then((response) => { - this.browserStorage.setInteractionInProgress(false); atPopupMeasurement.end({ success: true, isNativeBroker: true, @@ -805,7 +848,7 @@ export class StandardController implements IController { e instanceof NativeAuthError && isFatalNativeAuthError(e) ) { - this.nativeExtensionProvider = undefined; // If extension gets uninstalled during session prevent future requests from continuing to attempt + this.platformAuthProvider = undefined; // If extension gets uninstalled during session prevent future requests from continuing to attempt const popupClient = this.createPopupClient(correlationId); return popupClient.acquireToken(request, pkce); @@ -817,7 +860,6 @@ export class StandardController implements IController { this.createPopupClient(correlationId); return popupClient.acquireToken(request, pkce); } - this.browserStorage.setInteractionInProgress(false); throw e; }); } else { @@ -881,11 +923,12 @@ export class StandardController implements IController { // Since this function is syncronous we need to reject return Promise.reject(e); }) - .finally( - () => - this.config.system.asyncPopups && - this.preGeneratePkceCodes(correlationId) - ); + .finally(async () => { + this.browserStorage.setInteractionInProgress(false); + if (this.config.system.asyncPopups) { + await this.preGeneratePkceCodes(correlationId); + } + }); } private trackPageVisibilityWithMeasurement(): void { @@ -964,7 +1007,7 @@ export class StandardController implements IController { ).catch((e: AuthError) => { // If native token acquisition fails for availability reasons fallback to standard flow if (e instanceof NativeAuthError && isFatalNativeAuthError(e)) { - this.nativeExtensionProvider = undefined; // If extension gets uninstalled during session prevent future requests from continuing to attempt + this.platformAuthProvider = undefined; // If extension gets uninstalled during session prevent future requests from continuing to attempt const silentIframeClient = this.createSilentIframeClient( validRequest.correlationId ); @@ -1121,7 +1164,7 @@ export class StandardController implements IController { e instanceof NativeAuthError && isFatalNativeAuthError(e) ) { - this.nativeExtensionProvider = undefined; // If extension gets uninstalled during session prevent future requests from continuing to attempt + this.platformAuthProvider = undefined; // If extension gets uninstalled during session prevent future requests from continuing to attempt } throw e; }); @@ -1340,7 +1383,10 @@ export class StandardController implements IController { async logoutRedirect(logoutRequest?: EndSessionRequest): Promise { const correlationId = this.getRequestCorrelationId(logoutRequest); BrowserUtils.redirectPreflightCheck(this.initialized, this.config); - this.browserStorage.setInteractionInProgress(true); + this.browserStorage.setInteractionInProgress( + true, + INTERACTION_TYPE.SIGNOUT + ); const redirectClient = this.createRedirectClient(correlationId); return redirectClient.logout(logoutRequest); @@ -1354,10 +1400,15 @@ export class StandardController implements IController { try { const correlationId = this.getRequestCorrelationId(logoutRequest); BrowserUtils.preflightCheck(this.initialized); - this.browserStorage.setInteractionInProgress(true); + this.browserStorage.setInteractionInProgress( + true, + INTERACTION_TYPE.SIGNOUT + ); const popupClient = this.createPopupClient(correlationId); - return popupClient.logout(logoutRequest); + return popupClient.logout(logoutRequest).finally(() => { + this.browserStorage.setInteractionInProgress(false); + }); } catch (e) { // Since this function is syncronous we need to reject return Promise.reject(e); @@ -1388,10 +1439,12 @@ export class StandardController implements IController { * @returns Array of AccountInfo objects in cache */ getAllAccounts(accountFilter?: AccountFilter): AccountInfo[] { + const correlationId = this.getRequestCorrelationId(); return AccountManager.getAllAccounts( this.logger, this.browserStorage, this.isBrowserEnvironment, + correlationId, accountFilter ); } @@ -1402,10 +1455,12 @@ export class StandardController implements IController { * @returns The first account found in the cache matching the provided filter or null if no account could be found. */ getAccount(accountFilter: AccountFilter): AccountInfo | null { + const correlationId = this.getRequestCorrelationId(); return AccountManager.getAccount( accountFilter, this.logger, - this.browserStorage + this.browserStorage, + correlationId ); } @@ -1418,10 +1473,12 @@ export class StandardController implements IController { * @returns The account object stored in MSAL */ getAccountByUsername(username: string): AccountInfo | null { + const correlationId = this.getRequestCorrelationId(); return AccountManager.getAccountByUsername( username, this.logger, - this.browserStorage + this.browserStorage, + correlationId ); } @@ -1433,10 +1490,12 @@ export class StandardController implements IController { * @returns The account object stored in MSAL */ getAccountByHomeId(homeAccountId: string): AccountInfo | null { + const correlationId = this.getRequestCorrelationId(); return AccountManager.getAccountByHomeId( homeAccountId, this.logger, - this.browserStorage + this.browserStorage, + correlationId ); } @@ -1448,10 +1507,12 @@ export class StandardController implements IController { * @returns The account object stored in MSAL */ getAccountByLocalId(localAccountId: string): AccountInfo | null { + const correlationId = this.getRequestCorrelationId(); return AccountManager.getAccountByLocalId( localAccountId, this.logger, - this.browserStorage + this.browserStorage, + correlationId ); } @@ -1460,14 +1521,23 @@ export class StandardController implements IController { * @param account */ setActiveAccount(account: AccountInfo | null): void { - AccountManager.setActiveAccount(account, this.browserStorage); + const correlationId = this.getRequestCorrelationId(); + AccountManager.setActiveAccount( + account, + this.browserStorage, + correlationId + ); } /** * Gets the currently active account */ getActiveAccount(): AccountInfo | null { - return AccountManager.getActiveAccount(this.browserStorage); + const correlationId = this.getRequestCorrelationId(); + return AccountManager.getActiveAccount( + this.browserStorage, + correlationId + ); } // #endregion @@ -1523,13 +1593,13 @@ export class StandardController implements IController { cacheLookupPolicy?: CacheLookupPolicy ): Promise { this.logger.trace("acquireTokenNative called"); - if (!this.nativeExtensionProvider) { + if (!this.platformAuthProvider) { throw createBrowserAuthError( BrowserAuthErrorCodes.nativeConnectionNotEstablished ); } - const nativeClient = new NativeInteractionClient( + const nativeClient = new PlatformAuthInteractionClient( this.config, this.browserStorage, this.browserCrypto, @@ -1538,7 +1608,7 @@ export class StandardController implements IController { this.navigationClient, apiId, this.performanceClient, - this.nativeExtensionProvider, + this.platformAuthProvider, accountId || this.getNativeAccountId(request), this.nativeInternalStorage, request.correlationId @@ -1556,16 +1626,23 @@ export class StandardController implements IController { accountId?: string ): boolean { this.logger.trace("canUsePlatformBroker called"); + if (!this.platformAuthProvider) { + this.logger.trace( + "canUsePlatformBroker: platform broker unavilable, returning false" + ); + return false; + } + if ( - !NativeMessageHandler.isPlatformBrokerAvailable( + !isPlatformAuthAllowed( this.config, this.logger, - this.nativeExtensionProvider, + this.platformAuthProvider, request.authenticationScheme ) ) { this.logger.trace( - "canUsePlatformBroker: isPlatformBrokerAvailable returned false, returning false" + "canUsePlatformBroker: isBrokerAvailable returned false, returning false" ); return false; } @@ -1630,7 +1707,7 @@ export class StandardController implements IController { this.navigationClient, this.performanceClient, this.nativeInternalStorage, - this.nativeExtensionProvider, + this.platformAuthProvider, correlationId ); } @@ -1649,7 +1726,7 @@ export class StandardController implements IController { this.navigationClient, this.performanceClient, this.nativeInternalStorage, - this.nativeExtensionProvider, + this.platformAuthProvider, correlationId ); } @@ -1671,7 +1748,7 @@ export class StandardController implements IController { ApiId.ssoSilent, this.performanceClient, this.nativeInternalStorage, - this.nativeExtensionProvider, + this.platformAuthProvider, correlationId ); } @@ -1690,7 +1767,7 @@ export class StandardController implements IController { this.eventHandler, this.navigationClient, this.performanceClient, - this.nativeExtensionProvider, + this.platformAuthProvider, correlationId ); } @@ -1709,7 +1786,7 @@ export class StandardController implements IController { this.eventHandler, this.navigationClient, this.performanceClient, - this.nativeExtensionProvider, + this.platformAuthProvider, correlationId ); } @@ -1729,7 +1806,7 @@ export class StandardController implements IController { this.navigationClient, ApiId.acquireTokenByCode, this.performanceClient, - this.nativeExtensionProvider, + this.platformAuthProvider, correlationId ); } @@ -2241,10 +2318,10 @@ export class StandardController implements IController { ): Promise { // if the cache policy is set to access_token only, we should not be hitting the native layer yet if ( - NativeMessageHandler.isPlatformBrokerAvailable( + isPlatformAuthAllowed( this.config, this.logger, - this.nativeExtensionProvider, + this.platformAuthProvider, silentRequest.authenticationScheme ) && silentRequest.account.nativeAccountId @@ -2263,8 +2340,7 @@ export class StandardController implements IController { this.logger.verbose( "acquireTokenSilent - native platform unavailable, falling back to web flow" ); - this.nativeExtensionProvider = undefined; // Prevent future requests from continuing to attempt - + this.platformAuthProvider = undefined; // Prevent future requests from continuing to attempt // Cache will not contain tokens, given that previous WAM requests succeeded. Skip cache and RT renewal and go straight to iframe renewal throw createClientAuthError( ClientAuthErrorCodes.tokenRefreshRequired @@ -2347,6 +2423,30 @@ export class StandardController implements IController { ); return res; } + + private logMultipleInstances( + performanceEvent: InProgressPerformanceEvent + ): void { + const clientId = this.config.auth.clientId; + + if (!window) return; + // @ts-ignore + window.msal = window.msal || {}; + // @ts-ignore + window.msal.clientIds = window.msal.clientIds || []; + + // @ts-ignore + const clientIds: string[] = window.msal.clientIds; + + if (clientIds.length > 0) { + this.logger.verbose( + "There is already an instance of MSAL.js in the window." + ); + } + // @ts-ignore + window.msal.clientIds.push(clientId); + collectInstanceStats(clientId, performanceEvent, this.logger); + } } /** diff --git a/lib/msal-browser/src/controllers/UnknownOperatingContextController.ts b/lib/msal-browser/src/controllers/UnknownOperatingContextController.ts index 4ea6330785..06487b78b3 100644 --- a/lib/msal-browser/src/controllers/UnknownOperatingContextController.ts +++ b/lib/msal-browser/src/controllers/UnknownOperatingContextController.ts @@ -186,6 +186,7 @@ export class UnknownOperatingContextController implements IController { Omit< CommonAuthorizationUrlRequest, | "responseMode" + | "earJwk" | "codeChallenge" | "codeChallengeMethod" | "requestedClaimsHash" @@ -293,6 +294,7 @@ export class UnknownOperatingContextController implements IController { Omit< CommonAuthorizationUrlRequest, | "responseMode" + | "earJwk" | "codeChallenge" | "codeChallengeMethod" | "requestedClaimsHash" diff --git a/lib/msal-browser/src/crypto/BrowserCrypto.ts b/lib/msal-browser/src/crypto/BrowserCrypto.ts index f281920218..a49efbaf2d 100644 --- a/lib/msal-browser/src/crypto/BrowserCrypto.ts +++ b/lib/msal-browser/src/crypto/BrowserCrypto.ts @@ -12,8 +12,8 @@ import { PerformanceEvents, } from "@azure/msal-common/browser"; import { KEY_FORMAT_JWK } from "../utils/BrowserConstants.js"; -import { urlEncodeArr } from "../encode/Base64Encode.js"; -import { base64DecToArr } from "../encode/Base64Decode.js"; +import { base64Encode, urlEncodeArr } from "../encode/Base64Encode.js"; +import { base64Decode, base64DecToArr } from "../encode/Base64Decode.js"; /** * This file defines functions used by the browser library to perform cryptography operations such as @@ -226,6 +226,95 @@ export async function sign( ) as Promise; } +/** + * Generates Base64 encoded jwk used in the Encrypted Authorize Response (EAR) flow + */ +export async function generateEarKey(): Promise { + const key = await generateBaseKey(); + const keyStr = urlEncodeArr(new Uint8Array(key)); + + const jwk = { + alg: "dir", + kty: "oct", + k: keyStr, + }; + + return base64Encode(JSON.stringify(jwk)); +} + +/** + * Parses earJwk for encryption key and returns CryptoKey object + * @param earJwk + * @returns + */ +export async function importEarKey(earJwk: string): Promise { + const b64DecodedJwk = base64Decode(earJwk); + const jwkJson = JSON.parse(b64DecodedJwk); + const rawKey = jwkJson.k; + const keyBuffer = base64DecToArr(rawKey); + + return window.crypto.subtle.importKey(RAW, keyBuffer, AES_GCM, false, [ + DECRYPT, + ]); +} + +/** + * Decrypt ear_jwe response returned in the Encrypted Authorize Response (EAR) flow + * @param earJwk + * @param earJwe + * @returns + */ +export async function decryptEarResponse( + earJwk: string, + earJwe: string +): Promise { + const earJweParts = earJwe.split("."); + if (earJweParts.length !== 5) { + throw createBrowserAuthError( + BrowserAuthErrorCodes.failedToDecryptEarResponse, + "jwe_length" + ); + } + + const key = await importEarKey(earJwk).catch(() => { + throw createBrowserAuthError( + BrowserAuthErrorCodes.failedToDecryptEarResponse, + "import_key" + ); + }); + + try { + const header = new TextEncoder().encode(earJweParts[0]); + const iv = base64DecToArr(earJweParts[2]); + const ciphertext = base64DecToArr(earJweParts[3]); + const tag = base64DecToArr(earJweParts[4]); + const tagLengthBits = tag.byteLength * 8; + + // Concat ciphertext and tag + const encryptedData = new Uint8Array(ciphertext.length + tag.length); + encryptedData.set(ciphertext); + encryptedData.set(tag, ciphertext.length); + + const decryptedData = await window.crypto.subtle.decrypt( + { + name: AES_GCM, + iv: iv, + tagLength: tagLengthBits, + additionalData: header, + }, + key, + encryptedData + ); + + return new TextDecoder().decode(decryptedData); + } catch (e) { + throw createBrowserAuthError( + BrowserAuthErrorCodes.failedToDecryptEarResponse, + "decrypt" + ); + } +} + /** * Generates symmetric base encryption key. This may be stored as all encryption/decryption keys will be derived from this one. */ diff --git a/lib/msal-browser/src/crypto/CryptoOps.ts b/lib/msal-browser/src/crypto/CryptoOps.ts index fc30dfd56d..a0ebbbd7bf 100644 --- a/lib/msal-browser/src/crypto/CryptoOps.ts +++ b/lib/msal-browser/src/crypto/CryptoOps.ts @@ -4,6 +4,8 @@ */ import { + ClientAuthErrorCodes, + createClientAuthError, ICrypto, IPerformanceClient, JoseHeader, @@ -168,10 +170,14 @@ export class CryptoOps implements ICrypto { * Removes cryptographic keypair from key store matching the keyId passed in * @param kid */ - async removeTokenBindingKey(kid: string): Promise { + async removeTokenBindingKey(kid: string): Promise { await this.cache.removeItem(kid); const keyFound = await this.cache.containsKey(kid); - return !keyFound; + if (keyFound) { + throw createClientAuthError( + ClientAuthErrorCodes.bindingKeyNotRemoved + ); + } } /** diff --git a/lib/msal-browser/src/crypto/SignedHttpRequest.ts b/lib/msal-browser/src/crypto/SignedHttpRequest.ts index 0627ffdbe8..173abfe6fb 100644 --- a/lib/msal-browser/src/crypto/SignedHttpRequest.ts +++ b/lib/msal-browser/src/crypto/SignedHttpRequest.ts @@ -5,6 +5,8 @@ import { CryptoOps } from "./CryptoOps.js"; import { + ClientAuthError, + ClientAuthErrorCodes, Logger, LoggerOptions, PopTokenGenerator, @@ -71,6 +73,22 @@ export class SignedHttpRequest { * @returns If keys are properly deleted */ async removeKeys(publicKeyThumbprint: string): Promise { - return this.cryptoOps.removeTokenBindingKey(publicKeyThumbprint); + return this.cryptoOps + .removeTokenBindingKey(publicKeyThumbprint) + .then(() => true) + .catch((error) => { + /* + * @deprecated - To maintain public API signature, we return false if the error is due to the key still being present in indexedDB. + */ + if ( + error instanceof ClientAuthError && + error.errorCode === + ClientAuthErrorCodes.bindingKeyNotRemoved + ) { + return false; + } + + throw error; + }); } } diff --git a/lib/msal-browser/src/custom_auth/CustomAuthActionInputs.ts b/lib/msal-browser/src/custom_auth/CustomAuthActionInputs.ts new file mode 100644 index 0000000000..e90c5ff1f4 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/CustomAuthActionInputs.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { UserAccountAttributes } from "./UserAccountAttributes.js"; + +export type CustomAuthActionInputs = { + correlationId?: string; +}; + +export type AccountRetrievalInputs = CustomAuthActionInputs; + +export type SignInInputs = CustomAuthActionInputs & { + username: string; + password?: string; + scopes?: Array; +}; + +export type SignUpInputs = CustomAuthActionInputs & { + username: string; + password?: string; + attributes?: UserAccountAttributes; +}; + +export type ResetPasswordInputs = CustomAuthActionInputs & { + username: string; +}; + +export type AccessTokenRetrievalInputs = { + forceRefresh: boolean; + scopes?: Array; +}; + +export type SignInWithContinuationTokenInputs = { + scopes?: Array; +}; diff --git a/lib/msal-browser/src/custom_auth/CustomAuthConstants.ts b/lib/msal-browser/src/custom_auth/CustomAuthConstants.ts new file mode 100644 index 0000000000..ba8d7281b5 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/CustomAuthConstants.ts @@ -0,0 +1,50 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { Constants } from "@azure/msal-common/browser"; +import { version } from "../packageMetadata.js"; + +export const GrantType = { + PASSWORD: "password", + OOB: "oob", + CONTINUATION_TOKEN: "continuation_token", + REDIRECT: "redirect", + ATTRIBUTES: "attributes", +} as const; + +export const ChallengeType = { + PASSWORD: "password", + OOB: "oob", + REDIRECT: "redirect", +} as const; + +export const DefaultScopes = [ + Constants.OPENID_SCOPE, + Constants.PROFILE_SCOPE, + Constants.OFFLINE_ACCESS_SCOPE, +] as const; + +export const HttpHeaderKeys = { + CONTENT_TYPE: "Content-Type", + X_MS_REQUEST_ID: "x-ms-request-id", +} as const; + +export const DefaultPackageInfo = { + SKU: "msal.browser", + VERSION: version, + OS: "", + CPU: "", +} as const; + +export const ResetPasswordPollStatus = { + IN_PROGRESS: "in_progress", + SUCCEEDED: "succeeded", + FAILED: "failed", + NOT_STARTED: "not_started", +} as const; + +export const DefaultCustomAuthApiCodeLength = -1; // Default value indicating that the code length is not specified +export const DefaultCustomAuthApiCodeResendIntervalInSec = 300; // seconds +export const PasswordResetPollingTimeoutInMs = 300000; // milliseconds diff --git a/lib/msal-browser/src/custom_auth/CustomAuthPublicClientApplication.ts b/lib/msal-browser/src/custom_auth/CustomAuthPublicClientApplication.ts new file mode 100644 index 0000000000..bd0a16f9b7 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/CustomAuthPublicClientApplication.ts @@ -0,0 +1,158 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { GetAccountResult } from "./get_account/auth_flow/result/GetAccountResult.js"; +import { SignInResult } from "./sign_in/auth_flow/result/SignInResult.js"; +import { SignUpResult } from "./sign_up/auth_flow/result/SignUpResult.js"; +import { ICustomAuthStandardController } from "./controller/ICustomAuthStandardController.js"; +import { CustomAuthStandardController } from "./controller/CustomAuthStandardController.js"; +import { ICustomAuthPublicClientApplication } from "./ICustomAuthPublicClientApplication.js"; +import { + AccountRetrievalInputs, + SignInInputs, + SignUpInputs, + ResetPasswordInputs, +} from "./CustomAuthActionInputs.js"; +import { CustomAuthConfiguration } from "./configuration/CustomAuthConfiguration.js"; +import { CustomAuthOperatingContext } from "./operating_context/CustomAuthOperatingContext.js"; +import { ResetPasswordStartResult } from "./reset_password/auth_flow/result/ResetPasswordStartResult.js"; +import { InvalidConfigurationError } from "./core/error/InvalidConfigurationError.js"; +import { ChallengeType } from "./CustomAuthConstants.js"; +import { PublicClientApplication } from "../app/PublicClientApplication.js"; +import { + InvalidAuthority, + InvalidChallengeType, + MissingConfiguration, +} from "./core/error/InvalidConfigurationErrorCodes.js"; + +export class CustomAuthPublicClientApplication + extends PublicClientApplication + implements ICustomAuthPublicClientApplication +{ + private readonly customAuthController: ICustomAuthStandardController; + + /** + * Creates a new instance of a PublicClientApplication with the given configuration and controller to start Native authentication flows + * @param {CustomAuthConfiguration} config - A configuration object for the PublicClientApplication instance + * @returns {Promise} - A promise that resolves to a CustomAuthPublicClientApplication instance + */ + static async create( + config: CustomAuthConfiguration + ): Promise { + CustomAuthPublicClientApplication.validateConfig(config); + + const customAuthController = new CustomAuthStandardController( + new CustomAuthOperatingContext(config) + ); + + await customAuthController.initialize(); + + const app = new CustomAuthPublicClientApplication( + config, + customAuthController + ); + + return app; + } + + private constructor( + config: CustomAuthConfiguration, + controller: ICustomAuthStandardController + ) { + super(config, controller); + + this.customAuthController = controller; + } + + /** + * Gets the current account from the browser cache. + * @param {AccountRetrievalInputs} accountRetrievalInputs?:AccountRetrievalInputs + * @returns {GetAccountResult} - The result of the get account operation + */ + getCurrentAccount( + accountRetrievalInputs?: AccountRetrievalInputs + ): GetAccountResult { + return this.customAuthController.getCurrentAccount( + accountRetrievalInputs + ); + } + + /** + * Initiates the sign-in flow. + * This method results in sign-in completion, or extra actions (password, code, etc.) required to complete the sign-in. + * Create result with error details if any exception thrown. + * @param {SignInInputs} signInInputs - Inputs for the sign-in flow + * @returns {Promise} - A promise that resolves to SignInResult + */ + signIn(signInInputs: SignInInputs): Promise { + return this.customAuthController.signIn(signInInputs); + } + + /** + * Initiates the sign-up flow. + * This method results in sign-up completion, or extra actions (password, code, etc.) required to complete the sign-up. + * Create result with error details if any exception thrown. + * @param {SignUpInputs} signUpInputs + * @returns {Promise} - A promise that resolves to SignUpResult + */ + signUp(signUpInputs: SignUpInputs): Promise { + return this.customAuthController.signUp(signUpInputs); + } + + /** + * Initiates the reset password flow. + * This method results in triggering extra action (submit code) to complete the reset password. + * Create result with error details if any exception thrown. + * @param {ResetPasswordInputs} resetPasswordInputs - Inputs for the reset password flow + * @returns {Promise} - A promise that resolves to ResetPasswordStartResult + */ + resetPassword( + resetPasswordInputs: ResetPasswordInputs + ): Promise { + return this.customAuthController.resetPassword(resetPasswordInputs); + } + + /** + * Validates the configuration to ensure it is a valid CustomAuthConfiguration object. + * @param {CustomAuthConfiguration} config - The configuration object for the PublicClientApplication. + * @returns {void} + */ + private static validateConfig(config: CustomAuthConfiguration): void { + // Ensure the configuration object has a valid CIAM authority URL. + if (!config) { + throw new InvalidConfigurationError( + MissingConfiguration, + "The configuration is missing." + ); + } + + if (!config.auth?.authority) { + throw new InvalidConfigurationError( + InvalidAuthority, + `The authority URL '${config.auth?.authority}' is not set.` + ); + } + + const challengeTypes = config.customAuth.challengeTypes; + + if (!!challengeTypes && challengeTypes.length > 0) { + challengeTypes.forEach((challengeType) => { + const lowerCaseChallengeType = challengeType.toLowerCase(); + if ( + lowerCaseChallengeType !== ChallengeType.PASSWORD && + lowerCaseChallengeType !== ChallengeType.OOB && + lowerCaseChallengeType !== ChallengeType.REDIRECT + ) { + throw new InvalidConfigurationError( + InvalidChallengeType, + `Challenge type ${challengeType} in the configuration are not valid. Supported challenge types are ${Object.values( + ChallengeType + )}` + ); + } + }); + } + } +} diff --git a/lib/msal-browser/src/custom_auth/ICustomAuthPublicClientApplication.ts b/lib/msal-browser/src/custom_auth/ICustomAuthPublicClientApplication.ts new file mode 100644 index 0000000000..2baa2d63e0 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/ICustomAuthPublicClientApplication.ts @@ -0,0 +1,51 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { GetAccountResult } from "./get_account/auth_flow/result/GetAccountResult.js"; +import { SignInResult } from "./sign_in/auth_flow/result/SignInResult.js"; +import { SignUpResult } from "./sign_up/auth_flow/result/SignUpResult.js"; +import { + AccountRetrievalInputs, + ResetPasswordInputs, + SignInInputs, + SignUpInputs, +} from "./CustomAuthActionInputs.js"; +import { ResetPasswordStartResult } from "./reset_password/auth_flow/result/ResetPasswordStartResult.js"; +import { IPublicClientApplication } from "../app/IPublicClientApplication.js"; + +export interface ICustomAuthPublicClientApplication + extends IPublicClientApplication { + /** + * Gets the current account from the cache. + * @param {AccountRetrievalInputs} accountRetrievalInputs - Inputs for getting the current cached account + * @returns {GetAccountResult} The result of the operation + */ + getCurrentAccount( + accountRetrievalInputs?: AccountRetrievalInputs + ): GetAccountResult; + + /** + * Initiates the sign-in flow. + * @param {SignInInputs} signInInputs - Inputs for the sign-in flow + * @returns {Promise} A promise that resolves to SignInResult + */ + signIn(signInInputs: SignInInputs): Promise; + + /** + * Initiates the sign-up flow. + * @param {SignUpInputs} signUpInputs - Inputs for the sign-up flow + * @returns {Promise} A promise that resolves to SignUpResult + */ + signUp(signUpInputs: SignUpInputs): Promise; + + /** + * Initiates the reset password flow. + * @param {ResetPasswordInputs} resetPasswordInputs - Inputs for the reset password flow + * @returns {Promise} A promise that resolves to ResetPasswordStartResult + */ + resetPassword( + resetPasswordInputs: ResetPasswordInputs + ): Promise; +} diff --git a/lib/msal-browser/src/custom_auth/UserAccountAttributes.ts b/lib/msal-browser/src/custom_auth/UserAccountAttributes.ts new file mode 100644 index 0000000000..6e3213f1b4 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/UserAccountAttributes.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export type UserAccountAttributes = Record & { + city?: string; + country?: string; + displayName?: string; + givenName?: string; + jobTitle?: string; + postalCode?: string; + state?: string; + streetAddress?: string; + surname?: string; +}; diff --git a/lib/msal-browser/src/custom_auth/configuration/CustomAuthConfiguration.ts b/lib/msal-browser/src/custom_auth/configuration/CustomAuthConfiguration.ts new file mode 100644 index 0000000000..daddb13188 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/configuration/CustomAuthConfiguration.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + BrowserConfiguration, + Configuration, +} from "../../config/Configuration.js"; + +export type CustomAuthOptions = { + challengeTypes?: Array; + authApiProxyUrl: string; +}; + +export type CustomAuthConfiguration = Configuration & { + customAuth: CustomAuthOptions; +}; + +export type CustomAuthBrowserConfiguration = BrowserConfiguration & { + customAuth: CustomAuthOptions; +}; diff --git a/lib/msal-browser/src/custom_auth/controller/CustomAuthStandardController.ts b/lib/msal-browser/src/custom_auth/controller/CustomAuthStandardController.ts new file mode 100644 index 0000000000..102c337f6a --- /dev/null +++ b/lib/msal-browser/src/custom_auth/controller/CustomAuthStandardController.ts @@ -0,0 +1,509 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { GetAccountResult } from "../get_account/auth_flow/result/GetAccountResult.js"; +import { SignInResult } from "../sign_in/auth_flow/result/SignInResult.js"; +import { SignUpResult } from "../sign_up/auth_flow/result/SignUpResult.js"; +import { + SignInStartParams, + SignInSubmitPasswordParams, +} from "../sign_in/interaction_client/parameter/SignInParams.js"; +import { SignInClient } from "../sign_in/interaction_client/SignInClient.js"; +import { + AccountRetrievalInputs, + SignInInputs, + SignUpInputs, + ResetPasswordInputs, + CustomAuthActionInputs, +} from "../CustomAuthActionInputs.js"; +import { CustomAuthBrowserConfiguration } from "../configuration/CustomAuthConfiguration.js"; +import { CustomAuthOperatingContext } from "../operating_context/CustomAuthOperatingContext.js"; +import { ICustomAuthStandardController } from "./ICustomAuthStandardController.js"; +import { CustomAuthAccountData } from "../get_account/auth_flow/CustomAuthAccountData.js"; +import { UnexpectedError } from "../core/error/UnexpectedError.js"; +import { ResetPasswordStartResult } from "../reset_password/auth_flow/result/ResetPasswordStartResult.js"; +import { CustomAuthAuthority } from "../core/CustomAuthAuthority.js"; +import { DefaultPackageInfo } from "../CustomAuthConstants.js"; +import { + SIGN_IN_CODE_SEND_RESULT_TYPE, + SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE, +} from "../sign_in/interaction_client/result/SignInActionResult.js"; +import { SignUpClient } from "../sign_up/interaction_client/SignUpClient.js"; +import { CustomAuthInterationClientFactory } from "../core/interaction_client/CustomAuthInterationClientFactory.js"; +import { + SIGN_UP_CODE_REQUIRED_RESULT_TYPE, + SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE, +} from "../sign_up/interaction_client/result/SignUpActionResult.js"; +import { ICustomAuthApiClient } from "../core/network_client/custom_auth_api/ICustomAuthApiClient.js"; +import { CustomAuthApiClient } from "../core/network_client/custom_auth_api/CustomAuthApiClient.js"; +import { FetchHttpClient } from "../core/network_client/http_client/FetchHttpClient.js"; +import { ResetPasswordClient } from "../reset_password/interaction_client/ResetPasswordClient.js"; +import { NoCachedAccountFoundError } from "../core/error/NoCachedAccountFoundError.js"; +import { + ensureArgumentIsNotEmptyString, + ensureArgumentIsNotNullOrUndefined, +} from "../core/utils/ArgumentValidator.js"; +import { UserAlreadySignedInError } from "../core/error/UserAlreadySignedInError.js"; +import { CustomAuthSilentCacheClient } from "../get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { UnsupportedEnvironmentError } from "../core/error/UnsupportedEnvironmentError.js"; +import { SignInCodeRequiredState } from "../sign_in/auth_flow/state/SignInCodeRequiredState.js"; +import { SignInPasswordRequiredState } from "../sign_in/auth_flow/state/SignInPasswordRequiredState.js"; +import { SignInCompletedState } from "../sign_in/auth_flow/state/SignInCompletedState.js"; +import { SignUpCodeRequiredState } from "../sign_up/auth_flow/state/SignUpCodeRequiredState.js"; +import { SignUpPasswordRequiredState } from "../sign_up/auth_flow/state/SignUpPasswordRequiredState.js"; +import { ResetPasswordCodeRequiredState } from "../reset_password/auth_flow/state/ResetPasswordCodeRequiredState.js"; +import { StandardController } from "../../controllers/StandardController.js"; + +/* + * Controller for standard native auth operations. + */ +export class CustomAuthStandardController + extends StandardController + implements ICustomAuthStandardController +{ + private readonly signInClient: SignInClient; + private readonly signUpClient: SignUpClient; + private readonly resetPasswordClient: ResetPasswordClient; + private readonly cacheClient: CustomAuthSilentCacheClient; + private readonly customAuthConfig: CustomAuthBrowserConfiguration; + private readonly authority: CustomAuthAuthority; + + /* + * Constructor for CustomAuthStandardController. + * @param operatingContext - The operating context for the controller. + * @param customAuthApiClient - The client to use for custom auth API operations. + */ + constructor( + operatingContext: CustomAuthOperatingContext, + customAuthApiClient?: ICustomAuthApiClient + ) { + super(operatingContext); + + if (!this.isBrowserEnvironment) { + this.logger.verbose( + "The SDK can only be used in a browser environment." + ); + throw new UnsupportedEnvironmentError(); + } + + this.logger = this.logger.clone( + DefaultPackageInfo.SKU, + DefaultPackageInfo.VERSION + ); + this.customAuthConfig = operatingContext.getCustomAuthConfig(); + + this.authority = new CustomAuthAuthority( + this.customAuthConfig.auth.authority, + this.customAuthConfig, + this.networkClient, + this.browserStorage, + this.logger, + this.customAuthConfig.customAuth?.authApiProxyUrl + ); + + const interactionClientFactory = new CustomAuthInterationClientFactory( + this.customAuthConfig, + this.browserStorage, + this.browserCrypto, + this.logger, + this.eventHandler, + this.navigationClient, + this.performanceClient, + customAuthApiClient ?? + new CustomAuthApiClient( + this.authority.getCustomAuthApiDomain(), + this.customAuthConfig.auth.clientId, + new FetchHttpClient(this.logger) + ), + this.authority + ); + + this.signInClient = interactionClientFactory.create(SignInClient); + this.signUpClient = interactionClientFactory.create(SignUpClient); + this.resetPasswordClient = + interactionClientFactory.create(ResetPasswordClient); + this.cacheClient = interactionClientFactory.create( + CustomAuthSilentCacheClient + ); + } + + /* + * Gets the current account from the cache. + * @param accountRetrievalInputs - Inputs for getting the current cached account + * @returns {GetAccountResult} The account result + */ + getCurrentAccount( + accountRetrievalInputs?: AccountRetrievalInputs + ): GetAccountResult { + const correlationId = this.getCorrelationId(accountRetrievalInputs); + try { + this.logger.verbose("Getting current account data.", correlationId); + + const account = this.cacheClient.getCurrentAccount(correlationId); + + if (account) { + this.logger.verbose("Account data found.", correlationId); + + return new GetAccountResult( + new CustomAuthAccountData( + account, + this.customAuthConfig, + this.cacheClient, + this.logger, + correlationId + ) + ); + } + + throw new NoCachedAccountFoundError(correlationId); + } catch (error) { + this.logger.errorPii( + `An error occurred during getting current account: ${error}`, + correlationId + ); + + return GetAccountResult.createWithError(error); + } + } + + /* + * Signs the user in. + * @param signInInputs - Inputs for signing in the user. + * @returns {Promise} The result of the operation. + */ + async signIn(signInInputs: SignInInputs): Promise { + const correlationId = this.getCorrelationId(signInInputs); + + try { + ensureArgumentIsNotNullOrUndefined( + "signInInputs", + signInInputs, + correlationId + ); + + ensureArgumentIsNotEmptyString( + "signInInputs.username", + signInInputs.username, + correlationId + ); + this.ensureUserNotSignedIn(correlationId); + + // start the signin flow + const signInStartParams: SignInStartParams = { + clientId: this.customAuthConfig.auth.clientId, + correlationId: correlationId, + challengeType: + this.customAuthConfig.customAuth.challengeTypes ?? [], + username: signInInputs.username, + password: signInInputs.password, + }; + + this.logger.verbose( + `Starting sign-in flow ${ + !!signInInputs.password ? "with" : "without" + } password.`, + correlationId + ); + + const startResult = await this.signInClient.start( + signInStartParams + ); + + this.logger.verbose("Sign-in flow started.", correlationId); + + if (startResult.type === SIGN_IN_CODE_SEND_RESULT_TYPE) { + // require code + this.logger.verbose( + "Code required for sign-in.", + correlationId + ); + + return new SignInResult( + new SignInCodeRequiredState({ + correlationId: startResult.correlationId, + continuationToken: startResult.continuationToken, + logger: this.logger, + config: this.customAuthConfig, + signInClient: this.signInClient, + cacheClient: this.cacheClient, + username: signInInputs.username, + codeLength: startResult.codeLength, + scopes: signInInputs.scopes ?? [], + }) + ); + } else if ( + startResult.type === SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE + ) { + // require password + this.logger.verbose( + "Password required for sign-in.", + correlationId + ); + + if (!signInInputs.password) { + this.logger.verbose( + "Password required but not provided. Returning password required state.", + correlationId + ); + + return new SignInResult( + new SignInPasswordRequiredState({ + correlationId: startResult.correlationId, + continuationToken: startResult.continuationToken, + logger: this.logger, + config: this.customAuthConfig, + signInClient: this.signInClient, + cacheClient: this.cacheClient, + username: signInInputs.username, + scopes: signInInputs.scopes ?? [], + }) + ); + } + + this.logger.verbose( + "Submitting password for sign-in.", + correlationId + ); + + // if the password is provided, then try to get token silently. + const submitPasswordParams: SignInSubmitPasswordParams = { + clientId: this.customAuthConfig.auth.clientId, + correlationId: correlationId, + challengeType: + this.customAuthConfig.customAuth.challengeTypes ?? [], + scopes: signInInputs.scopes ?? [], + continuationToken: startResult.continuationToken, + password: signInInputs.password, + username: signInInputs.username, + }; + + const completedResult = await this.signInClient.submitPassword( + submitPasswordParams + ); + + this.logger.verbose("Sign-in flow completed.", correlationId); + + const accountInfo = new CustomAuthAccountData( + completedResult.authenticationResult.account, + this.customAuthConfig, + this.cacheClient, + this.logger, + correlationId + ); + + return new SignInResult( + new SignInCompletedState(), + accountInfo + ); + } + + this.logger.error( + "Unexpected sign-in result type. Returning error.", + correlationId + ); + + throw new UnexpectedError( + "Unknow sign-in result type", + correlationId + ); + } catch (error) { + this.logger.errorPii( + `An error occurred during starting sign-in: ${error}`, + correlationId + ); + + return SignInResult.createWithError(error); + } + } + + /* + * Signs the user up. + * @param signUpInputs - Inputs for signing up the user. + * @returns {Promise} The result of the operation + */ + async signUp(signUpInputs: SignUpInputs): Promise { + const correlationId = this.getCorrelationId(signUpInputs); + + try { + ensureArgumentIsNotNullOrUndefined( + "signUpInputs", + signUpInputs, + correlationId + ); + + ensureArgumentIsNotEmptyString( + "signUpInputs.username", + signUpInputs.username, + correlationId + ); + this.ensureUserNotSignedIn(correlationId); + + this.logger.verbose( + `Starting sign-up flow${ + !!signUpInputs.password + ? ` with ${ + !!signUpInputs.attributes + ? "password and attributes" + : "password" + }` + : "" + }.`, + correlationId + ); + + const startResult = await this.signUpClient.start({ + clientId: this.customAuthConfig.auth.clientId, + correlationId: correlationId, + challengeType: + this.customAuthConfig.customAuth.challengeTypes ?? [], + username: signUpInputs.username, + password: signUpInputs.password, + attributes: signUpInputs.attributes, + }); + + this.logger.verbose("Sign-up flow started.", correlationId); + + if (startResult.type === SIGN_UP_CODE_REQUIRED_RESULT_TYPE) { + // Code required + this.logger.verbose( + "Code required for sign-up.", + correlationId + ); + + return new SignUpResult( + new SignUpCodeRequiredState({ + correlationId: startResult.correlationId, + continuationToken: startResult.continuationToken, + logger: this.logger, + config: this.customAuthConfig, + signInClient: this.signInClient, + signUpClient: this.signUpClient, + cacheClient: this.cacheClient, + username: signUpInputs.username, + codeLength: startResult.codeLength, + codeResendInterval: startResult.interval, + }) + ); + } else if ( + startResult.type === SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE + ) { + // Password required + this.logger.verbose( + "Password required for sign-up.", + correlationId + ); + + return new SignUpResult( + new SignUpPasswordRequiredState({ + correlationId: startResult.correlationId, + continuationToken: startResult.continuationToken, + logger: this.logger, + config: this.customAuthConfig, + signInClient: this.signInClient, + signUpClient: this.signUpClient, + cacheClient: this.cacheClient, + username: signUpInputs.username, + }) + ); + } + + this.logger.error( + "Unexpected sign-up result type. Returning error.", + correlationId + ); + + throw new UnexpectedError( + "Unknown sign-up result type", + correlationId + ); + } catch (error) { + this.logger.errorPii( + `An error occurred during starting sign-up: ${error}`, + correlationId + ); + + return SignUpResult.createWithError(error); + } + } + + /* + * Resets the user's password. + * @param resetPasswordInputs - Inputs for resetting the user's password. + * @returns {Promise} The result of the operation. + */ + async resetPassword( + resetPasswordInputs: ResetPasswordInputs + ): Promise { + const correlationId = this.getCorrelationId(resetPasswordInputs); + + try { + ensureArgumentIsNotNullOrUndefined( + "resetPasswordInputs", + resetPasswordInputs, + correlationId + ); + + ensureArgumentIsNotEmptyString( + "resetPasswordInputs.username", + resetPasswordInputs.username, + correlationId + ); + this.ensureUserNotSignedIn(correlationId); + + this.logger.verbose("Starting password-reset flow.", correlationId); + + const startResult = await this.resetPasswordClient.start({ + clientId: this.customAuthConfig.auth.clientId, + correlationId: correlationId, + challengeType: + this.customAuthConfig.customAuth.challengeTypes ?? [], + username: resetPasswordInputs.username, + }); + + this.logger.verbose("Password-reset flow started.", correlationId); + + return new ResetPasswordStartResult( + new ResetPasswordCodeRequiredState({ + correlationId: startResult.correlationId, + continuationToken: startResult.continuationToken, + logger: this.logger, + config: this.customAuthConfig, + signInClient: this.signInClient, + resetPasswordClient: this.resetPasswordClient, + cacheClient: this.cacheClient, + username: resetPasswordInputs.username, + codeLength: startResult.codeLength, + }) + ); + } catch (error) { + this.logger.errorPii( + `An error occurred during starting reset-password: ${error}`, + correlationId + ); + + return ResetPasswordStartResult.createWithError(error); + } + } + + private getCorrelationId( + actionInputs: CustomAuthActionInputs | undefined + ): string { + return ( + actionInputs?.correlationId || this.browserCrypto.createNewGuid() + ); + } + + private ensureUserNotSignedIn(correlationId: string): void { + const account = this.getCurrentAccount({ + correlationId: correlationId, + }); + + if (account && !!account.data) { + this.logger.error("User has already signed in.", correlationId); + + throw new UserAlreadySignedInError(correlationId); + } + } +} diff --git a/lib/msal-browser/src/custom_auth/controller/ICustomAuthStandardController.ts b/lib/msal-browser/src/custom_auth/controller/ICustomAuthStandardController.ts new file mode 100644 index 0000000000..9be4ae79ae --- /dev/null +++ b/lib/msal-browser/src/custom_auth/controller/ICustomAuthStandardController.ts @@ -0,0 +1,53 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { GetAccountResult } from "../get_account/auth_flow/result/GetAccountResult.js"; +import { SignInResult } from "../sign_in/auth_flow/result/SignInResult.js"; +import { SignUpResult } from "../sign_up/auth_flow/result/SignUpResult.js"; +import { + AccountRetrievalInputs, + ResetPasswordInputs, + SignInInputs, + SignUpInputs, +} from "../CustomAuthActionInputs.js"; +import { ResetPasswordStartResult } from "../reset_password/auth_flow/result/ResetPasswordStartResult.js"; +import { IController } from "../../controllers/IController.js"; + +/* + * Controller interface for standard authentication operations. + */ +export interface ICustomAuthStandardController extends IController { + /* + * Gets the current account from the cache. + * @param accountRetrievalInputs - Inputs for getting the current cached account + * @returns - The result of the operation + */ + getCurrentAccount( + accountRetrievalInputs?: AccountRetrievalInputs + ): GetAccountResult; + + /* + * Signs the current user out. + * @param signInInputs - Inputs for signing in. + * @returns The result of the operation. + */ + signIn(signInInputs: SignInInputs): Promise; + + /* + * Signs the current user up. + * @param signUpInputs - Inputs for signing up. + * @returns The result of the operation. + */ + signUp(signUpInputs: SignUpInputs): Promise; + + /* + * Resets the password for the current user. + * @param resetPasswordInputs - Inputs for resetting the password. + * @returns The result of the operation. + */ + resetPassword( + resetPasswordInputs: ResetPasswordInputs + ): Promise; +} diff --git a/lib/msal-browser/src/custom_auth/core/CustomAuthAuthority.ts b/lib/msal-browser/src/custom_auth/core/CustomAuthAuthority.ts new file mode 100644 index 0000000000..886460b568 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/CustomAuthAuthority.ts @@ -0,0 +1,112 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Authority, + AuthorityOptions, + INetworkModule, + Logger, +} from "@azure/msal-common/browser"; +import * as CustomAuthApiEndpoint from "./network_client/custom_auth_api/CustomAuthApiEndpoint.js"; +import { buildUrl } from "./utils/UrlUtils.js"; +import { BrowserConfiguration } from "../../config/Configuration.js"; +import { BrowserCacheManager } from "../../cache/BrowserCacheManager.js"; + +/** + * Authority class which can be used to create an authority object for Custom Auth features. + */ +export class CustomAuthAuthority extends Authority { + /** + * Constructor for the Custom Auth Authority. + * @param authority - The authority URL for the authority. + * @param networkInterface - The network interface implementation to make requests. + * @param cacheManager - The cache manager. + * @param authorityOptions - The options for the authority. + * @param logger - The logger for the authority. + * @param customAuthProxyDomain - The custom auth proxy domain. + */ + constructor( + authority: string, + config: BrowserConfiguration, + networkInterface: INetworkModule, + cacheManager: BrowserCacheManager, + logger: Logger, + private customAuthProxyDomain?: string + ) { + const ciamAuthorityUrl = + CustomAuthAuthority.transformCIAMAuthority(authority); + + const authorityOptions: AuthorityOptions = { + protocolMode: config.auth.protocolMode, + OIDCOptions: config.auth.OIDCOptions, + knownAuthorities: config.auth.knownAuthorities, + cloudDiscoveryMetadata: config.auth.cloudDiscoveryMetadata, + authorityMetadata: config.auth.authorityMetadata, + skipAuthorityMetadataCache: config.auth.skipAuthorityMetadataCache, + }; + + super( + ciamAuthorityUrl, + networkInterface, + cacheManager, + authorityOptions, + logger, + "" + ); + + // Set the metadata for the authority + const metadataEntity = { + aliases: [this.hostnameAndPort], + preferred_cache: this.getPreferredCache(), + preferred_network: this.hostnameAndPort, + canonical_authority: this.canonicalAuthority, + authorization_endpoint: "", + token_endpoint: this.tokenEndpoint, + end_session_endpoint: "", + issuer: "", + aliasesFromNetwork: false, + endpointsFromNetwork: false, + /* + * give max value to make sure it doesn't expire, + * as we only initiate the authority metadata entity once and it doesn't change + */ + expiresAt: Number.MAX_SAFE_INTEGER, + jwks_uri: "", + }; + const cacheKey = this.cacheManager.generateAuthorityMetadataCacheKey( + metadataEntity.preferred_cache + ); + cacheManager.setAuthorityMetadata(cacheKey, metadataEntity); + } + + /** + * Gets the custom auth endpoint. + * The open id configuration doesn't have the correct endpoint for the auth APIs. + * We need to generate the endpoint manually based on the authority URL. + * @returns The custom auth endpoint + */ + getCustomAuthApiDomain(): string { + /* + * The customAuthProxyDomain is used to resolve the CORS issue when calling the auth APIs. + * If the customAuthProxyDomain is not provided, we will generate the auth API domain based on the authority URL. + */ + return !this.customAuthProxyDomain + ? this.canonicalAuthority + : this.customAuthProxyDomain; + } + + override getPreferredCache(): string { + return this.canonicalAuthorityUrlComponents.HostNameAndPort; + } + + override get tokenEndpoint(): string { + const endpointUrl = buildUrl( + this.getCustomAuthApiDomain(), + CustomAuthApiEndpoint.SIGNIN_TOKEN + ); + + return endpointUrl.href; + } +} diff --git a/lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowErrorBase.ts b/lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowErrorBase.ts new file mode 100644 index 0000000000..2ceebef5a5 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowErrorBase.ts @@ -0,0 +1,140 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + CustomAuthApiError, + RedirectError, +} from "../error/CustomAuthApiError.js"; +import { CustomAuthError } from "../error/CustomAuthError.js"; +import { NoCachedAccountFoundError } from "../error/NoCachedAccountFoundError.js"; +import { InvalidArgumentError } from "../error/InvalidArgumentError.js"; +import * as CustomAuthApiErrorCode from "../network_client/custom_auth_api/types/ApiErrorCodes.js"; +import * as CustomAuthApiSuberror from "../network_client/custom_auth_api/types/ApiSuberrors.js"; +/** + * Base class for all auth flow errors. + */ +export abstract class AuthFlowErrorBase { + constructor(public errorData: CustomAuthError) {} + + protected isUserNotFoundError(): boolean { + return this.errorData.error === CustomAuthApiErrorCode.USER_NOT_FOUND; + } + + protected isUserInvalidError(): boolean { + return ( + (this.errorData instanceof InvalidArgumentError && + this.errorData.errorDescription?.includes("username")) || + (this.errorData instanceof CustomAuthApiError && + !!this.errorData.errorDescription?.includes( + "username parameter is empty or not valid" + ) && + !!this.errorData.errorCodes?.includes(90100)) + ); + } + + protected isUnsupportedChallengeTypeError(): boolean { + return ( + (this.errorData.error === CustomAuthApiErrorCode.INVALID_REQUEST && + (this.errorData.errorDescription?.includes( + "The challenge_type list parameter contains an unsupported challenge type" + ) ?? + false)) || + this.errorData.error === + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE + ); + } + + protected isPasswordIncorrectError(): boolean { + const isIncorrectPassword = + this.errorData.error === CustomAuthApiErrorCode.INVALID_GRANT && + this.errorData instanceof CustomAuthApiError && + (this.errorData.errorCodes ?? []).includes(50126); + + const isPasswordEmpty = + this.errorData instanceof InvalidArgumentError && + this.errorData.errorDescription?.includes("password") === true; + + return isIncorrectPassword || isPasswordEmpty; + } + + protected isInvalidCodeError(): boolean { + return ( + (this.errorData.error === CustomAuthApiErrorCode.INVALID_GRANT && + this.errorData instanceof CustomAuthApiError && + this.errorData.subError === + CustomAuthApiSuberror.INVALID_OOB_VALUE) || + (this.errorData instanceof InvalidArgumentError && + this.errorData.errorDescription?.includes("code") === true) + ); + } + + protected isRedirectError(): boolean { + return this.errorData instanceof RedirectError; + } + + protected isInvalidNewPasswordError(): boolean { + const invalidPasswordSubErrors = new Set([ + CustomAuthApiSuberror.PASSWORD_BANNED, + CustomAuthApiSuberror.PASSWORD_IS_INVALID, + CustomAuthApiSuberror.PASSWORD_RECENTLY_USED, + CustomAuthApiSuberror.PASSWORD_TOO_LONG, + CustomAuthApiSuberror.PASSWORD_TOO_SHORT, + CustomAuthApiSuberror.PASSWORD_TOO_WEAK, + ]); + + return ( + this.errorData instanceof CustomAuthApiError && + this.errorData.error === CustomAuthApiErrorCode.INVALID_GRANT && + invalidPasswordSubErrors.has(this.errorData.subError ?? "") + ); + } + + protected isUserAlreadyExistsError(): boolean { + return ( + this.errorData instanceof CustomAuthApiError && + this.errorData.error === CustomAuthApiErrorCode.USER_ALREADY_EXISTS + ); + } + + protected isAttributeRequiredError(): boolean { + return ( + this.errorData instanceof CustomAuthApiError && + this.errorData.error === CustomAuthApiErrorCode.ATTRIBUTES_REQUIRED + ); + } + + protected isAttributeValidationFailedError(): boolean { + return ( + (this.errorData instanceof CustomAuthApiError && + this.errorData.error === CustomAuthApiErrorCode.INVALID_GRANT && + this.errorData.subError === + CustomAuthApiSuberror.ATTRIBUTE_VALIATION_FAILED) || + (this.errorData instanceof InvalidArgumentError && + this.errorData.errorDescription?.includes("attributes") === + true) + ); + } + + protected isNoCachedAccountFoundError(): boolean { + return this.errorData instanceof NoCachedAccountFoundError; + } + + protected isTokenExpiredError(): boolean { + return ( + this.errorData instanceof CustomAuthApiError && + this.errorData.error === CustomAuthApiErrorCode.EXPIRED_TOKEN + ); + } +} + +export abstract class AuthActionErrorBase extends AuthFlowErrorBase { + /** + * Checks if the error is due to the expired continuation token. + * @returns {boolean} True if the error is due to the expired continuation token, false otherwise. + */ + isTokenExpired(): boolean { + return this.isTokenExpiredError(); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowResultBase.ts b/lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowResultBase.ts new file mode 100644 index 0000000000..4edc3b934b --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowResultBase.ts @@ -0,0 +1,55 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthError } from "@azure/msal-common/browser"; +import { CustomAuthError } from "../error/CustomAuthError.js"; +import { MsalCustomAuthError } from "../error/MsalCustomAuthError.js"; +import { UnexpectedError } from "../error/UnexpectedError.js"; +import { AuthFlowErrorBase } from "./AuthFlowErrorBase.js"; +import { AuthFlowStateBase } from "./AuthFlowState.js"; + +/* + * Base class for a result of an authentication operation. + * @typeParam TState - The type of the auth flow state. + * @typeParam TError - The type of error. + * @typeParam TData - The type of the result data. + */ +export abstract class AuthFlowResultBase< + TState extends AuthFlowStateBase, + TError extends AuthFlowErrorBase, + TData = void +> { + /* + *constructor for ResultBase + * @param state - The state. + * @param data - The result data. + */ + constructor(public state: TState, public data?: TData) {} + + /* + * The error that occurred during the authentication operation. + */ + error?: TError; + + /* + * Creates a CustomAuthError with an error. + * @param error - The error that occurred. + * @returns The auth error. + */ + protected static createErrorData(error: unknown): CustomAuthError { + if (error instanceof CustomAuthError) { + return error; + } else if (error instanceof AuthError) { + return new MsalCustomAuthError( + error.errorCode, + error.errorMessage, + error.subError, + error.correlationId + ); + } else { + return new UnexpectedError(error); + } + } +} diff --git a/lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowState.ts b/lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowState.ts new file mode 100644 index 0000000000..8224f1ce2c --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/auth_flow/AuthFlowState.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { InvalidArgumentError } from "../error/InvalidArgumentError.js"; +import { CustomAuthBrowserConfiguration } from "../../configuration/CustomAuthConfiguration.js"; +import { Logger } from "@azure/msal-common/browser"; +import { ensureArgumentIsNotEmptyString } from "../utils/ArgumentValidator.js"; +import { DefaultCustomAuthApiCodeLength } from "../../CustomAuthConstants.js"; + +export interface AuthFlowActionRequiredStateParameters { + correlationId: string; + logger: Logger; + config: CustomAuthBrowserConfiguration; + continuationToken?: string; +} + +/** + * Base class for the state of an authentication flow. + */ +export abstract class AuthFlowStateBase {} + +/** + * Base class for the action requried state in an authentication flow. + */ +export abstract class AuthFlowActionRequiredStateBase< + TParameter extends AuthFlowActionRequiredStateParameters +> extends AuthFlowStateBase { + /** + * Creates a new instance of AuthFlowActionRequiredStateBase. + * @param stateParameters The parameters for the auth state. + */ + protected constructor(protected readonly stateParameters: TParameter) { + ensureArgumentIsNotEmptyString( + "correlationId", + stateParameters.correlationId + ); + + super(); + } + + protected ensureCodeIsValid(code: string, codeLength: number): void { + if ( + codeLength !== DefaultCustomAuthApiCodeLength && + (!code || code.length !== codeLength) + ) { + this.stateParameters.logger.error( + "Code parameter is not provided or invalid for authentication flow.", + this.stateParameters.correlationId + ); + + throw new InvalidArgumentError( + "code", + this.stateParameters.correlationId + ); + } + } + + protected ensurePasswordIsNotEmpty(password: string): void { + if (!password) { + this.stateParameters.logger.error( + "Password parameter is not provided for authentication flow.", + this.stateParameters.correlationId + ); + + throw new InvalidArgumentError( + "password", + this.stateParameters.correlationId + ); + } + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/CustomAuthApiError.ts b/lib/msal-browser/src/custom_auth/core/error/CustomAuthApiError.ts new file mode 100644 index 0000000000..d0dbf33629 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/CustomAuthApiError.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { UserAttribute } from "../network_client/custom_auth_api/types/ApiErrorResponseTypes.js"; +import { CustomAuthError } from "./CustomAuthError.js"; + +/** + * Error when no required authentication method by Microsoft Entra is supported + */ +export class RedirectError extends CustomAuthError { + constructor(correlationId?: string) { + super( + "redirect", + "No required authentication method by Microsoft Entra is supported, a fallback to the web-based authentication flow is needed.", + correlationId + ); + Object.setPrototypeOf(this, RedirectError.prototype); + } +} + +/** + * Custom Auth API error. + */ +export class CustomAuthApiError extends CustomAuthError { + constructor( + error: string, + errorDescription: string, + correlationId?: string, + errorCodes?: Array, + subError?: string, + public attributes?: Array, + public continuationToken?: string, + public traceId?: string, + public timestamp?: string + ) { + super(error, errorDescription, correlationId, errorCodes, subError); + Object.setPrototypeOf(this, CustomAuthApiError.prototype); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/CustomAuthError.ts b/lib/msal-browser/src/custom_auth/core/error/CustomAuthError.ts new file mode 100644 index 0000000000..16578d080d --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/CustomAuthError.ts @@ -0,0 +1,20 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export class CustomAuthError extends Error { + constructor( + public error: string, + public errorDescription?: string, + public correlationId?: string, + public errorCodes?: Array, + public subError?: string + ) { + super(`${error}: ${errorDescription ?? ""}`); + Object.setPrototypeOf(this, CustomAuthError.prototype); + + this.errorCodes = errorCodes ?? []; + this.subError = subError ?? ""; + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/HttpError.ts b/lib/msal-browser/src/custom_auth/core/error/HttpError.ts new file mode 100644 index 0000000000..e49500a9b4 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/HttpError.ts @@ -0,0 +1,13 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthError } from "./CustomAuthError.js"; + +export class HttpError extends CustomAuthError { + constructor(error: string, message: string, correlationId?: string) { + super(error, message, correlationId); + Object.setPrototypeOf(this, HttpError.prototype); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/HttpErrorCodes.ts b/lib/msal-browser/src/custom_auth/core/error/HttpErrorCodes.ts new file mode 100644 index 0000000000..a78da3450e --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/HttpErrorCodes.ts @@ -0,0 +1,7 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export const NoNetworkConnectivity = "no_network_connectivity"; +export const FailedSendRequest = "failed_send_request"; diff --git a/lib/msal-browser/src/custom_auth/core/error/InvalidArgumentError.ts b/lib/msal-browser/src/custom_auth/core/error/InvalidArgumentError.ts new file mode 100644 index 0000000000..7ba7ce0cab --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/InvalidArgumentError.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthError } from "./CustomAuthError.js"; + +export class InvalidArgumentError extends CustomAuthError { + constructor(argName: string, correlationId?: string) { + const errorDescription = `The argument '${argName}' is invalid.`; + + super("invalid_argument", errorDescription, correlationId); + Object.setPrototypeOf(this, InvalidArgumentError.prototype); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/InvalidConfigurationError.ts b/lib/msal-browser/src/custom_auth/core/error/InvalidConfigurationError.ts new file mode 100644 index 0000000000..42121f706e --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/InvalidConfigurationError.ts @@ -0,0 +1,13 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthError } from "./CustomAuthError.js"; + +export class InvalidConfigurationError extends CustomAuthError { + constructor(error: string, message: string, correlationId?: string) { + super(error, message, correlationId); + Object.setPrototypeOf(this, InvalidConfigurationError.prototype); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/InvalidConfigurationErrorCodes.ts b/lib/msal-browser/src/custom_auth/core/error/InvalidConfigurationErrorCodes.ts new file mode 100644 index 0000000000..c0f2ae6c4c --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/InvalidConfigurationErrorCodes.ts @@ -0,0 +1,8 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export const MissingConfiguration = "missing_configuration"; +export const InvalidAuthority = "invalid_authority"; +export const InvalidChallengeType = "invalid_challenge_type"; diff --git a/lib/msal-browser/src/custom_auth/core/error/MethodNotImplementedError.ts b/lib/msal-browser/src/custom_auth/core/error/MethodNotImplementedError.ts new file mode 100644 index 0000000000..05f24ec0ca --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/MethodNotImplementedError.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthError } from "./CustomAuthError.js"; + +export class MethodNotImplementedError extends CustomAuthError { + constructor(method: string, correlationId?: string) { + const errorDescription = `The method '${method}' is not implemented, please do not use.`; + + super("method_not_implemented", errorDescription, correlationId); + Object.setPrototypeOf(this, MethodNotImplementedError.prototype); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/MsalCustomAuthError.ts b/lib/msal-browser/src/custom_auth/core/error/MsalCustomAuthError.ts new file mode 100644 index 0000000000..d9bcc04104 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/MsalCustomAuthError.ts @@ -0,0 +1,22 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthError } from "./CustomAuthError.js"; + +export class MsalCustomAuthError extends CustomAuthError { + subError: string | undefined; + + constructor( + error: string, + errorDescription?: string, + subError?: string, + correlationId?: string + ) { + super(error, errorDescription, correlationId); + Object.setPrototypeOf(this, MsalCustomAuthError.prototype); + + this.subError = subError || ""; + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/NoCachedAccountFoundError.ts b/lib/msal-browser/src/custom_auth/core/error/NoCachedAccountFoundError.ts new file mode 100644 index 0000000000..65bc14ae7d --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/NoCachedAccountFoundError.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthError } from "./CustomAuthError.js"; + +export class NoCachedAccountFoundError extends CustomAuthError { + constructor(correlationId?: string) { + super( + "no_cached_account_found", + "No account found in the cache", + correlationId + ); + Object.setPrototypeOf(this, NoCachedAccountFoundError.prototype); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/ParsedUrlError.ts b/lib/msal-browser/src/custom_auth/core/error/ParsedUrlError.ts new file mode 100644 index 0000000000..c8dca0a9e1 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/ParsedUrlError.ts @@ -0,0 +1,13 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthError } from "./CustomAuthError.js"; + +export class ParsedUrlError extends CustomAuthError { + constructor(error: string, message: string, correlationId?: string) { + super(error, message, correlationId); + Object.setPrototypeOf(this, ParsedUrlError.prototype); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/ParsedUrlErrorCodes.ts b/lib/msal-browser/src/custom_auth/core/error/ParsedUrlErrorCodes.ts new file mode 100644 index 0000000000..7c1f0dc10c --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/ParsedUrlErrorCodes.ts @@ -0,0 +1,6 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export const InvalidUrl = "invalid_url"; diff --git a/lib/msal-browser/src/custom_auth/core/error/UnexpectedError.ts b/lib/msal-browser/src/custom_auth/core/error/UnexpectedError.ts new file mode 100644 index 0000000000..d84c6a1e36 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/UnexpectedError.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthError } from "./CustomAuthError.js"; + +export class UnexpectedError extends CustomAuthError { + constructor(errorData: unknown, correlationId?: string) { + let errorDescription: string; + + if (errorData instanceof Error) { + errorDescription = errorData.message; + } else if (typeof errorData === "string") { + errorDescription = errorData; + } else if (typeof errorData === "object" && errorData !== null) { + errorDescription = JSON.stringify(errorData); + } else { + errorDescription = "An unexpected error occurred."; + } + + super("unexpected_error", errorDescription, correlationId); + Object.setPrototypeOf(this, UnexpectedError.prototype); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/UnsupportedEnvironmentError.ts b/lib/msal-browser/src/custom_auth/core/error/UnsupportedEnvironmentError.ts new file mode 100644 index 0000000000..8952238b49 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/UnsupportedEnvironmentError.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthError } from "./CustomAuthError.js"; + +export class UnsupportedEnvironmentError extends CustomAuthError { + constructor(correlationId?: string) { + super( + "unsupported_env", + "The current environment is not browser", + correlationId + ); + Object.setPrototypeOf(this, UnsupportedEnvironmentError.prototype); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/UserAccountAttributeError.ts b/lib/msal-browser/src/custom_auth/core/error/UserAccountAttributeError.ts new file mode 100644 index 0000000000..4ef2610d5f --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/UserAccountAttributeError.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthError } from "./CustomAuthError.js"; + +export class UserAccountAttributeError extends CustomAuthError { + constructor(error: string, attributeName: string, attributeValue: string) { + const errorDescription = `Failed to set attribute '${attributeName}' with value '${attributeValue}'`; + + super(error, errorDescription); + Object.setPrototypeOf(this, UserAccountAttributeError.prototype); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/error/UserAccountAttributeErrorCodes.ts b/lib/msal-browser/src/custom_auth/core/error/UserAccountAttributeErrorCodes.ts new file mode 100644 index 0000000000..9e552a0236 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/UserAccountAttributeErrorCodes.ts @@ -0,0 +1,6 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export const InvalidAttributeErrorCode = "invalid_attribute"; diff --git a/lib/msal-browser/src/custom_auth/core/error/UserAlreadySignedInError.ts b/lib/msal-browser/src/custom_auth/core/error/UserAlreadySignedInError.ts new file mode 100644 index 0000000000..b2556b04ff --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/error/UserAlreadySignedInError.ts @@ -0,0 +1,17 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthError } from "./CustomAuthError.js"; + +export class UserAlreadySignedInError extends CustomAuthError { + constructor(correlationId?: string) { + super( + "user_already_signed_in", + "The user has already signed in.", + correlationId + ); + Object.setPrototypeOf(this, UserAlreadySignedInError.prototype); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/interaction_client/CustomAuthInteractionClientBase.ts b/lib/msal-browser/src/custom_auth/core/interaction_client/CustomAuthInteractionClientBase.ts new file mode 100644 index 0000000000..efafd5bd43 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/interaction_client/CustomAuthInteractionClientBase.ts @@ -0,0 +1,92 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ICustomAuthApiClient } from "../network_client/custom_auth_api/ICustomAuthApiClient.js"; +import { MethodNotImplementedError } from "../error/MethodNotImplementedError.js"; +import { CustomAuthAuthority } from "../CustomAuthAuthority.js"; +import { ChallengeType } from "../../CustomAuthConstants.js"; +import { StandardInteractionClient } from "../../../interaction_client/StandardInteractionClient.js"; +import { BrowserConfiguration } from "../../../config/Configuration.js"; +import { BrowserCacheManager } from "../../../cache/BrowserCacheManager.js"; +import { + Constants, + ICrypto, + IPerformanceClient, + Logger, +} from "@azure/msal-common/browser"; +import { EventHandler } from "../../../event/EventHandler.js"; +import { INavigationClient } from "../../../navigation/INavigationClient.js"; +import { RedirectRequest } from "../../../request/RedirectRequest.js"; +import { PopupRequest } from "../../../request/PopupRequest.js"; +import { SsoSilentRequest } from "../../../request/SsoSilentRequest.js"; +import { EndSessionRequest } from "../../../request/EndSessionRequest.js"; +import { ClearCacheRequest } from "../../../request/ClearCacheRequest.js"; +import { AuthenticationResult } from "../../../response/AuthenticationResult.js"; + +export abstract class CustomAuthInteractionClientBase extends StandardInteractionClient { + constructor( + config: BrowserConfiguration, + storageImpl: BrowserCacheManager, + browserCrypto: ICrypto, + logger: Logger, + eventHandler: EventHandler, + navigationClient: INavigationClient, + performanceClient: IPerformanceClient, + protected customAuthApiClient: ICustomAuthApiClient, + protected customAuthAuthority: CustomAuthAuthority + ) { + super( + config, + storageImpl, + browserCrypto, + logger, + eventHandler, + navigationClient, + performanceClient + ); + } + + protected getChallengeTypes( + configuredChallengeTypes: string[] | undefined + ): string { + const challengeType = configuredChallengeTypes ?? []; + if ( + !challengeType.some( + (type) => type.toLowerCase() === ChallengeType.REDIRECT + ) + ) { + challengeType.push(ChallengeType.REDIRECT); + } + return challengeType.join(" "); + } + + protected getScopes(scopes: string[] | undefined): string[] { + if (!!scopes && scopes.length > 0) { + scopes; + } + + return [ + Constants.OPENID_SCOPE, + Constants.PROFILE_SCOPE, + Constants.OFFLINE_ACCESS_SCOPE, + ]; + } + + // It is not necessary to implement this method from base class. + acquireToken( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + request: RedirectRequest | PopupRequest | SsoSilentRequest + ): Promise { + throw new MethodNotImplementedError("SignInClient.acquireToken"); + } + + // It is not necessary to implement this method from base class. + logout( + // eslint-disable-next-line @typescript-eslint/no-unused-vars + request: EndSessionRequest | ClearCacheRequest | undefined + ): Promise { + throw new MethodNotImplementedError("SignInClient.logout"); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/interaction_client/CustomAuthInterationClientFactory.ts b/lib/msal-browser/src/custom_auth/core/interaction_client/CustomAuthInterationClientFactory.ts new file mode 100644 index 0000000000..ede64542ab --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/interaction_client/CustomAuthInterationClientFactory.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ICustomAuthApiClient } from "../network_client/custom_auth_api/ICustomAuthApiClient.js"; +import { CustomAuthAuthority } from "../CustomAuthAuthority.js"; +import { CustomAuthInteractionClientBase } from "./CustomAuthInteractionClientBase.js"; +import { BrowserConfiguration } from "../../../config/Configuration.js"; +import { BrowserCacheManager } from "../../../cache/BrowserCacheManager.js"; +import { + ICrypto, + IPerformanceClient, + Logger, +} from "@azure/msal-common/browser"; +import { EventHandler } from "../../../event/EventHandler.js"; +import { INavigationClient } from "../../../navigation/INavigationClient.js"; + +export class CustomAuthInterationClientFactory { + constructor( + private config: BrowserConfiguration, + private storageImpl: BrowserCacheManager, + private browserCrypto: ICrypto, + private logger: Logger, + private eventHandler: EventHandler, + private navigationClient: INavigationClient, + private performanceClient: IPerformanceClient, + private customAuthApiClient: ICustomAuthApiClient, + private customAuthAuthority: CustomAuthAuthority + ) {} + + create( + clientConstructor: new ( + config: BrowserConfiguration, + storageImpl: BrowserCacheManager, + browserCrypto: ICrypto, + logger: Logger, + eventHandler: EventHandler, + navigationClient: INavigationClient, + performanceClient: IPerformanceClient, + customAuthApiClient: ICustomAuthApiClient, + customAuthAuthority: CustomAuthAuthority + ) => TClient + ): TClient { + return new clientConstructor( + this.config, + this.storageImpl, + this.browserCrypto, + this.logger, + this.eventHandler, + this.navigationClient, + this.performanceClient, + this.customAuthApiClient, + this.customAuthAuthority + ); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/BaseApiClient.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/BaseApiClient.ts new file mode 100644 index 0000000000..687c747c87 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/BaseApiClient.ts @@ -0,0 +1,168 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + ChallengeType, + DefaultPackageInfo, + HttpHeaderKeys, +} from "../../../CustomAuthConstants.js"; +import { IHttpClient } from "../http_client/IHttpClient.js"; +import * as CustomAuthApiErrorCode from "./types/ApiErrorCodes.js"; +import { buildUrl, parseUrl } from "../../utils/UrlUtils.js"; +import { + CustomAuthApiError, + RedirectError, +} from "../../error/CustomAuthApiError.js"; +import { + AADServerParamKeys, + ServerTelemetryManager, +} from "@azure/msal-common/browser"; +import { ApiErrorResponse } from "./types/ApiErrorResponseTypes.js"; + +export abstract class BaseApiClient { + private readonly baseRequestUrl: URL; + + constructor( + baseUrl: string, + private readonly clientId: string, + private httpClient: IHttpClient + ) { + this.baseRequestUrl = parseUrl( + !baseUrl.endsWith("/") ? `${baseUrl}/` : baseUrl + ); + } + + protected async request( + endpoint: string, + data: Record, + telemetryManager: ServerTelemetryManager, + correlationId: string + ): Promise { + const formData = new URLSearchParams({ + client_id: this.clientId, + ...data, + }); + const headers = this.getCommonHeaders(correlationId, telemetryManager); + const url = buildUrl(this.baseRequestUrl.href, endpoint); + + let response: Response; + + try { + response = await this.httpClient.post(url, formData, headers); + } catch (e) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.HTTP_REQUEST_FAILED, + `Failed to perform '${endpoint}' request: ${e}`, + correlationId + ); + } + + return this.handleApiResponse(response, correlationId); + } + + protected ensureContinuationTokenIsValid( + continuationToken: string | undefined, + correlationId: string + ): void { + if (!continuationToken) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.CONTINUATION_TOKEN_MISSING, + "Continuation token is missing in the response body", + correlationId + ); + } + } + + private readResponseCorrelationId( + response: Response, + requestCorrelationId: string + ): string { + return ( + response.headers.get(HttpHeaderKeys.X_MS_REQUEST_ID) || + requestCorrelationId + ); + } + + private getCommonHeaders( + correlationId: string, + telemetryManager: ServerTelemetryManager + ): Record { + return { + [HttpHeaderKeys.CONTENT_TYPE]: "application/x-www-form-urlencoded", + [AADServerParamKeys.X_CLIENT_SKU]: DefaultPackageInfo.SKU, + [AADServerParamKeys.X_CLIENT_VER]: DefaultPackageInfo.VERSION, + [AADServerParamKeys.X_CLIENT_OS]: DefaultPackageInfo.OS, + [AADServerParamKeys.X_CLIENT_CPU]: DefaultPackageInfo.CPU, + [AADServerParamKeys.X_CLIENT_CURR_TELEM]: + telemetryManager.generateCurrentRequestHeaderValue(), + [AADServerParamKeys.X_CLIENT_LAST_TELEM]: + telemetryManager.generateLastRequestHeaderValue(), + [AADServerParamKeys.CLIENT_REQUEST_ID]: correlationId, + }; + } + + private async handleApiResponse( + response: Response | undefined, + requestCorrelationId: string + ): Promise { + if (!response) { + throw new CustomAuthApiError( + "empty_response", + "Response is empty", + requestCorrelationId + ); + } + + const correlationId = this.readResponseCorrelationId( + response, + requestCorrelationId + ); + + const responseData = await response.json(); + + if (response.ok) { + // Ensure the response doesn't have redirect challenge type + if ( + typeof responseData === "object" && + responseData.challenge_type === ChallengeType.REDIRECT + ) { + throw new RedirectError(correlationId); + } + + return { + ...responseData, + correlation_id: correlationId, + }; + } + + const responseError = responseData as ApiErrorResponse; + + if (!responseError) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_RESPONSE_BODY, + "Response error body is empty or invalid", + correlationId + ); + } + + const attributes = + !!responseError.required_attributes && + responseError.required_attributes.length > 0 + ? responseError.required_attributes + : responseError.invalid_attributes ?? []; + + throw new CustomAuthApiError( + responseError.error, + responseError.error_description, + responseError.correlation_id, + responseError.error_codes, + responseError.suberror, + attributes, + responseError.continuation_token, + responseError.trace_id, + responseError.timestamp + ); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.ts new file mode 100644 index 0000000000..6ff2ffeac5 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.ts @@ -0,0 +1,38 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ResetPasswordApiClient } from "./ResetPasswordApiClient.js"; +import { SignupApiClient } from "./SignupApiClient.js"; +import { SignInApiClient } from "./SignInApiClient.js"; +import { ICustomAuthApiClient } from "./ICustomAuthApiClient.js"; +import { IHttpClient } from "../http_client/IHttpClient.js"; + +export class CustomAuthApiClient implements ICustomAuthApiClient { + signInApi: SignInApiClient; + signUpApi: SignupApiClient; + resetPasswordApi: ResetPasswordApiClient; + + constructor( + customAuthApiBaseUrl: string, + clientId: string, + httpClient: IHttpClient + ) { + this.signInApi = new SignInApiClient( + customAuthApiBaseUrl, + clientId, + httpClient + ); + this.signUpApi = new SignupApiClient( + customAuthApiBaseUrl, + clientId, + httpClient + ); + this.resetPasswordApi = new ResetPasswordApiClient( + customAuthApiBaseUrl, + clientId, + httpClient + ); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiEndpoint.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiEndpoint.ts new file mode 100644 index 0000000000..4b98d345d4 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiEndpoint.ts @@ -0,0 +1,18 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export const SIGNIN_INITIATE = "/oauth2/v2.0/initiate"; +export const SIGNIN_CHALLENGE = "/oauth2/v2.0/challenge"; +export const SIGNIN_TOKEN = "/oauth2/v2.0/token"; + +export const SIGNUP_START = "/signup/v1.0/start"; +export const SIGNUP_CHALLENGE = "/signup/v1.0/challenge"; +export const SIGNUP_CONTINUE = "/signup/v1.0/continue"; + +export const RESET_PWD_START = "/resetpassword/v1.0/start"; +export const RESET_PWD_CHALLENGE = "/resetpassword/v1.0/challenge"; +export const RESET_PWD_CONTINUE = "/resetpassword/v1.0/continue"; +export const RESET_PWD_SUBMIT = "/resetpassword/v1.0/submit"; +export const RESET_PWD_POLL = "/resetpassword/v1.0/poll_completion"; diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/ICustomAuthApiClient.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/ICustomAuthApiClient.ts new file mode 100644 index 0000000000..6d4cad1186 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/ICustomAuthApiClient.ts @@ -0,0 +1,13 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ResetPasswordApiClient } from "./ResetPasswordApiClient.js"; +import { SignupApiClient } from "./SignupApiClient.js"; +import { SignInApiClient } from "./SignInApiClient.js"; +export interface ICustomAuthApiClient { + signInApi: SignInApiClient; + signUpApi: SignupApiClient; + resetPasswordApi: ResetPasswordApiClient; +} diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/ResetPasswordApiClient.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/ResetPasswordApiClient.ts new file mode 100644 index 0000000000..11b711ba16 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/ResetPasswordApiClient.ts @@ -0,0 +1,172 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + GrantType, + ResetPasswordPollStatus, +} from "../../../CustomAuthConstants.js"; +import { CustomAuthApiError } from "../../error/CustomAuthApiError.js"; +import { BaseApiClient } from "./BaseApiClient.js"; +import * as CustomAuthApiEndpoint from "./CustomAuthApiEndpoint.js"; +import * as CustomAuthApiErrorCode from "./types/ApiErrorCodes.js"; +import { + ResetPasswordChallengeRequest, + ResetPasswordContinueRequest, + ResetPasswordPollCompletionRequest, + ResetPasswordStartRequest, + ResetPasswordSubmitRequest, +} from "./types/ApiRequestTypes.js"; +import { + ResetPasswordChallengeResponse, + ResetPasswordContinueResponse, + ResetPasswordPollCompletionResponse, + ResetPasswordStartResponse, + ResetPasswordSubmitResponse, +} from "./types/ApiResponseTypes.js"; + +export class ResetPasswordApiClient extends BaseApiClient { + /** + * Start the password reset flow + */ + async start( + params: ResetPasswordStartRequest + ): Promise { + const result = await this.request( + CustomAuthApiEndpoint.RESET_PWD_START, + { + challenge_type: params.challenge_type, + username: params.username, + }, + params.telemetryManager, + params.correlationId + ); + + this.ensureContinuationTokenIsValid( + result.continuation_token, + params.correlationId + ); + + return result; + } + + /** + * Request a challenge (OTP) to be sent to the user's email + * @param ChallengeResetPasswordRequest Parameters for the challenge request + */ + async requestChallenge( + params: ResetPasswordChallengeRequest + ): Promise { + const result = await this.request( + CustomAuthApiEndpoint.RESET_PWD_CHALLENGE, + { + challenge_type: params.challenge_type, + continuation_token: params.continuation_token, + }, + params.telemetryManager, + params.correlationId + ); + + this.ensureContinuationTokenIsValid( + result.continuation_token, + params.correlationId + ); + + return result; + } + + /** + * Submit the code for verification + * @param ContinueResetPasswordRequest Token from previous response + */ + async continueWithCode( + params: ResetPasswordContinueRequest + ): Promise { + const result = await this.request( + CustomAuthApiEndpoint.RESET_PWD_CONTINUE, + { + continuation_token: params.continuation_token, + grant_type: GrantType.OOB, + oob: params.oob, + }, + params.telemetryManager, + params.correlationId + ); + + this.ensureContinuationTokenIsValid( + result.continuation_token, + params.correlationId + ); + + return result; + } + + /** + * Submit the new password + * @param SubmitResetPasswordResponse Token from previous response + */ + async submitNewPassword( + params: ResetPasswordSubmitRequest + ): Promise { + const result = await this.request( + CustomAuthApiEndpoint.RESET_PWD_SUBMIT, + { + continuation_token: params.continuation_token, + new_password: params.new_password, + }, + params.telemetryManager, + params.correlationId + ); + + this.ensureContinuationTokenIsValid( + result.continuation_token, + params.correlationId + ); + + if (result.poll_interval === 0) { + result.poll_interval = 2; + } + + return result; + } + + /** + * Poll for password reset completion status + * @param continuationToken Token from previous response + */ + async pollCompletion( + params: ResetPasswordPollCompletionRequest + ): Promise { + const result = await this.request( + CustomAuthApiEndpoint.RESET_PWD_POLL, + { + continuation_token: params.continuation_token, + }, + params.telemetryManager, + params.correlationId + ); + + this.ensurePollStatusIsValid(result.status, params.correlationId); + + return result; + } + + protected ensurePollStatusIsValid( + status: string, + correlationId: string + ): void { + if ( + status !== ResetPasswordPollStatus.FAILED && + status !== ResetPasswordPollStatus.IN_PROGRESS && + status !== ResetPasswordPollStatus.SUCCEEDED && + status !== ResetPasswordPollStatus.NOT_STARTED + ) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_POLL_STATUS, + `The poll status '${status}' for password reset is invalid`, + correlationId + ); + } + } +} diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/SignInApiClient.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/SignInApiClient.ts new file mode 100644 index 0000000000..13599d9595 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/SignInApiClient.ts @@ -0,0 +1,186 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ServerTelemetryManager } from "@azure/msal-common/browser"; +import { GrantType } from "../../../CustomAuthConstants.js"; +import { CustomAuthApiError } from "../../error/CustomAuthApiError.js"; +import { BaseApiClient } from "./BaseApiClient.js"; +import * as CustomAuthApiEndpoint from "./CustomAuthApiEndpoint.js"; +import * as CustomAuthApiErrorCode from "./types/ApiErrorCodes.js"; +import { + SignInChallengeRequest, + SignInContinuationTokenRequest, + SignInInitiateRequest, + SignInOobTokenRequest, + SignInPasswordTokenRequest, +} from "./types/ApiRequestTypes.js"; +import { + SignInChallengeResponse, + SignInInitiateResponse, + SignInTokenResponse, +} from "./types/ApiResponseTypes.js"; + +export class SignInApiClient extends BaseApiClient { + /** + * Initiates the sign-in flow + * @param username User's email + * @param authMethod 'email-otp' | 'email-password' + */ + async initiate( + params: SignInInitiateRequest + ): Promise { + const result = await this.request( + CustomAuthApiEndpoint.SIGNIN_INITIATE, + { + username: params.username, + challenge_type: params.challenge_type, + }, + params.telemetryManager, + params.correlationId + ); + + this.ensureContinuationTokenIsValid( + result.continuation_token, + params.correlationId + ); + + return result; + } + + /** + * Requests authentication challenge (OTP or password validation) + * @param continuationToken Token from initiate response + * @param authMethod 'email-otp' | 'email-password' + */ + async requestChallenge( + params: SignInChallengeRequest + ): Promise { + const result = await this.request( + CustomAuthApiEndpoint.SIGNIN_CHALLENGE, + { + continuation_token: params.continuation_token, + challenge_type: params.challenge_type, + }, + params.telemetryManager, + params.correlationId + ); + + this.ensureContinuationTokenIsValid( + result.continuation_token, + params.correlationId + ); + + return result; + } + + /** + * Requests security tokens using either password or OTP + * @param continuationToken Token from challenge response + * @param credentials Password or OTP + * @param authMethod 'email-otp' | 'email-password' + */ + async requestTokensWithPassword( + params: SignInPasswordTokenRequest + ): Promise { + return this.requestTokens( + { + continuation_token: params.continuation_token, + grant_type: GrantType.PASSWORD, + scope: params.scope, + password: params.password, + }, + params.telemetryManager, + params.correlationId + ); + } + + async requestTokensWithOob( + params: SignInOobTokenRequest + ): Promise { + return this.requestTokens( + { + continuation_token: params.continuation_token, + scope: params.scope, + oob: params.oob, + grant_type: GrantType.OOB, + }, + params.telemetryManager, + params.correlationId + ); + } + + async requestTokenWithContinuationToken( + params: SignInContinuationTokenRequest + ): Promise { + return this.requestTokens( + { + continuation_token: params.continuation_token, + username: params.username, + scope: params.scope, + grant_type: GrantType.CONTINUATION_TOKEN, + client_info: true, + }, + params.telemetryManager, + params.correlationId + ); + } + + private async requestTokens( + requestData: Record, + telemetryManager: ServerTelemetryManager, + correlationId: string + ): Promise { + // The client_info parameter is required for MSAL to return the uid and utid in the response. + requestData.client_info = true; + + const result = await this.request( + CustomAuthApiEndpoint.SIGNIN_TOKEN, + requestData, + telemetryManager, + correlationId + ); + + SignInApiClient.ensureTokenResponseIsValid(result); + + return result; + } + + private static ensureTokenResponseIsValid( + tokenResponse: SignInTokenResponse + ): void { + let errorCode = ""; + let errorDescription = ""; + + if (!tokenResponse.access_token) { + errorCode = CustomAuthApiErrorCode.ACCESS_TOKEN_MISSING; + errorDescription = "Access token is missing in the response body"; + } else if (!tokenResponse.id_token) { + errorCode = CustomAuthApiErrorCode.ID_TOKEN_MISSING; + errorDescription = "Id token is missing in the response body"; + } else if (!tokenResponse.refresh_token) { + errorCode = CustomAuthApiErrorCode.REFRESH_TOKEN_MISSING; + errorDescription = "Refresh token is missing in the response body"; + } else if (!tokenResponse.expires_in || tokenResponse.expires_in <= 0) { + errorCode = CustomAuthApiErrorCode.INVALID_EXPIRES_IN; + errorDescription = "Expires in is invalid in the response body"; + } else if (tokenResponse.token_type !== "Bearer") { + errorCode = CustomAuthApiErrorCode.INVALID_TOKEN_TYPE; + errorDescription = `Token type '${tokenResponse.token_type}' is invalid in the response body`; + } else if (!tokenResponse.client_info) { + errorCode = CustomAuthApiErrorCode.CLIENT_INFO_MISSING; + errorDescription = "Client info is missing in the response body"; + } + + if (!errorCode && !errorDescription) { + return; + } + + throw new CustomAuthApiError( + errorCode, + errorDescription, + tokenResponse.correlation_id + ); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/SignupApiClient.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/SignupApiClient.ts new file mode 100644 index 0000000000..8b5b226237 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/SignupApiClient.ts @@ -0,0 +1,141 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { GrantType } from "../../../CustomAuthConstants.js"; +import { BaseApiClient } from "./BaseApiClient.js"; +import * as CustomAuthApiEndpoint from "./CustomAuthApiEndpoint.js"; +import { + SignUpChallengeRequest, + SignUpContinueWithAttributesRequest, + SignUpContinueWithOobRequest, + SignUpContinueWithPasswordRequest, + SignUpStartRequest, +} from "./types/ApiRequestTypes.js"; +import { + SignUpChallengeResponse, + SignUpContinueResponse, + SignUpStartResponse, +} from "./types/ApiResponseTypes.js"; + +export class SignupApiClient extends BaseApiClient { + /** + * Start the sign-up flow + */ + async start(params: SignUpStartRequest): Promise { + const result = await this.request( + CustomAuthApiEndpoint.SIGNUP_START, + { + username: params.username, + ...(params.password && { password: params.password }), + ...(params.attributes && { + attributes: JSON.stringify(params.attributes), + }), + challenge_type: params.challenge_type, + }, + params.telemetryManager, + params.correlationId + ); + + this.ensureContinuationTokenIsValid( + result.continuation_token, + params.correlationId + ); + + return result; + } + + /** + * Request challenge (e.g., OTP) + */ + async requestChallenge( + params: SignUpChallengeRequest + ): Promise { + const result = await this.request( + CustomAuthApiEndpoint.SIGNUP_CHALLENGE, + { + continuation_token: params.continuation_token, + challenge_type: params.challenge_type, + }, + params.telemetryManager, + params.correlationId + ); + + this.ensureContinuationTokenIsValid( + result.continuation_token, + params.correlationId + ); + + return result; + } + + /** + * Continue sign-up flow with code. + */ + async continueWithCode( + params: SignUpContinueWithOobRequest + ): Promise { + const result = await this.request( + CustomAuthApiEndpoint.SIGNUP_CONTINUE, + { + continuation_token: params.continuation_token, + grant_type: GrantType.OOB, + oob: params.oob, + }, + params.telemetryManager, + params.correlationId + ); + + this.ensureContinuationTokenIsValid( + result.continuation_token, + params.correlationId + ); + + return result; + } + + async continueWithPassword( + params: SignUpContinueWithPasswordRequest + ): Promise { + const result = await this.request( + CustomAuthApiEndpoint.SIGNUP_CONTINUE, + { + continuation_token: params.continuation_token, + grant_type: GrantType.PASSWORD, + password: params.password, + }, + params.telemetryManager, + params.correlationId + ); + + this.ensureContinuationTokenIsValid( + result.continuation_token, + params.correlationId + ); + + return result; + } + + async continueWithAttributes( + params: SignUpContinueWithAttributesRequest + ): Promise { + const result = await this.request( + CustomAuthApiEndpoint.SIGNUP_CONTINUE, + { + continuation_token: params.continuation_token, + grant_type: GrantType.ATTRIBUTES, + attributes: JSON.stringify(params.attributes), + }, + params.telemetryManager, + params.correlationId + ); + + this.ensureContinuationTokenIsValid( + result.continuation_token, + params.correlationId + ); + + return result; + } +} diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorCodes.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorCodes.ts new file mode 100644 index 0000000000..0a3250a26c --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorCodes.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export const CONTINUATION_TOKEN_MISSING = "continuation_token_missing"; +export const INVALID_RESPONSE_BODY = "invalid_response_body"; +export const EMPTY_RESPONSE = "empty_response"; +export const UNSUPPORTED_CHALLENGE_TYPE = "unsupported_challenge_type"; +export const ACCESS_TOKEN_MISSING = "access_token_missing"; +export const ID_TOKEN_MISSING = "id_token_missing"; +export const REFRESH_TOKEN_MISSING = "refresh_token_missing"; +export const INVALID_EXPIRES_IN = "invalid_expires_in"; +export const INVALID_TOKEN_TYPE = "invalid_token_type"; +export const HTTP_REQUEST_FAILED = "http_request_failed"; +export const INVALID_REQUEST = "invalid_request"; +export const USER_NOT_FOUND = "user_not_found"; +export const INVALID_GRANT = "invalid_grant"; +export const CREDENTIAL_REQUIRED = "credential_required"; +export const ATTRIBUTES_REQUIRED = "attributes_required"; +export const USER_ALREADY_EXISTS = "user_already_exists"; +export const INVALID_POLL_STATUS = "invalid_poll_status"; +export const PASSWORD_CHANGE_FAILED = "password_change_failed"; +export const PASSWORD_RESET_TIMEOUT = "password_reset_timeout"; +export const CLIENT_INFO_MISSING = "client_info_missing"; +export const EXPIRED_TOKEN = "expired_token"; diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorResponseTypes.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorResponseTypes.ts new file mode 100644 index 0000000000..da24fdf37b --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorResponseTypes.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export interface InvalidAttribute { + name: string; + reason: string; +} + +/** + * Detailed error interface for Microsoft Entra signup errors + */ +export interface ApiErrorResponse { + error: string; + error_description: string; + correlation_id: string; + error_codes?: number[]; + suberror?: string; + continuation_token?: string; + timestamp?: string; + trace_id?: string; + required_attributes?: Array; + invalid_attributes?: Array; +} + +export interface UserAttribute { + name: string; + type?: string; + required?: boolean; + options?: UserAttributeOption; +} + +export interface UserAttributeOption { + regex?: string; +} diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiRequestTypes.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiRequestTypes.ts new file mode 100644 index 0000000000..30d85b4756 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiRequestTypes.ts @@ -0,0 +1,91 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ApiRequestBase } from "./ApiTypesBase.js"; + +/* Sign-in API request types */ +export interface SignInInitiateRequest extends ApiRequestBase { + challenge_type: string; + username: string; +} + +export interface SignInChallengeRequest extends ApiRequestBase { + challenge_type: string; + continuation_token: string; +} + +interface SignInTokenRequestBase extends ApiRequestBase { + continuation_token: string; + scope: string; +} + +export interface SignInPasswordTokenRequest extends SignInTokenRequestBase { + password: string; +} + +export interface SignInOobTokenRequest extends SignInTokenRequestBase { + oob: string; +} + +export interface SignInContinuationTokenRequest extends SignInTokenRequestBase { + username: string; +} + +/* Sign-up API request types */ +export interface SignUpStartRequest extends ApiRequestBase { + username: string; + challenge_type: string; + password?: string; + attributes?: Record; +} + +export interface SignUpChallengeRequest extends ApiRequestBase { + continuation_token: string; + challenge_type: string; +} + +interface SignUpContinueRequestBase extends ApiRequestBase { + continuation_token: string; +} + +export interface SignUpContinueWithOobRequest + extends SignUpContinueRequestBase { + oob: string; +} + +export interface SignUpContinueWithPasswordRequest + extends SignUpContinueRequestBase { + password: string; +} + +export interface SignUpContinueWithAttributesRequest + extends SignUpContinueRequestBase { + attributes: Record; +} + +/* Reset password API request types */ +export interface ResetPasswordStartRequest extends ApiRequestBase { + challenge_type: string; + username: string; +} + +export interface ResetPasswordChallengeRequest extends ApiRequestBase { + challenge_type: string; + continuation_token: string; +} + +export interface ResetPasswordContinueRequest extends ApiRequestBase { + continuation_token: string; + oob: string; +} + +export interface ResetPasswordSubmitRequest extends ApiRequestBase { + continuation_token: string; + new_password: string; +} + +export interface ResetPasswordPollCompletionRequest extends ApiRequestBase { + continuation_token: string; +} diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiResponseTypes.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiResponseTypes.ts new file mode 100644 index 0000000000..2910f01063 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiResponseTypes.ts @@ -0,0 +1,66 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ApiResponseBase } from "./ApiTypesBase.js"; + +interface ContinuousResponse extends ApiResponseBase { + continuation_token?: string; +} + +interface InitiateResponse extends ContinuousResponse { + challenge_type?: string; +} + +interface ChallengeResponse extends ApiResponseBase { + continuation_token?: string; + challenge_type?: string; + binding_method?: string; + challenge_channel?: string; + challenge_target_label?: string; + code_length?: number; +} + +/* Sign-in API response types */ +export type SignInInitiateResponse = InitiateResponse; + +export type SignInChallengeResponse = ChallengeResponse; + +export interface SignInTokenResponse extends ApiResponseBase { + token_type: "Bearer"; + scope: string; + expires_in: number; + access_token: string; + refresh_token: string; + id_token: string; + client_info: string; + ext_expires_in?: number; +} + +/* Sign-up API response types */ +export type SignUpStartResponse = InitiateResponse; + +export interface SignUpChallengeResponse extends ChallengeResponse { + interval?: number; +} + +export type SignUpContinueResponse = InitiateResponse; + +/* Reset password API response types */ +export type ResetPasswordStartResponse = InitiateResponse; + +export type ResetPasswordChallengeResponse = ChallengeResponse; + +export interface ResetPasswordContinueResponse extends ContinuousResponse { + expires_in: number; +} + +export interface ResetPasswordSubmitResponse extends ContinuousResponse { + poll_interval: number; +} + +export interface ResetPasswordPollCompletionResponse + extends ContinuousResponse { + status: string; +} diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiSuberrors.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiSuberrors.ts new file mode 100644 index 0000000000..a233a9aa4d --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiSuberrors.ts @@ -0,0 +1,14 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export const PASSWORD_TOO_WEAK = "password_too_weak"; +export const PASSWORD_TOO_SHORT = "password_too_short"; +export const PASSWORD_TOO_LONG = "password_too_long"; +export const PASSWORD_RECENTLY_USED = "password_recently_used"; +export const PASSWORD_BANNED = "password_banned"; +export const PASSWORD_IS_INVALID = "password_is_invalid"; +export const INVALID_OOB_VALUE = "invalid_oob_value"; +export const ATTRIBUTE_VALIATION_FAILED = "attribute_validation_failed"; +export const NATIVEAUTHAPI_DISABLED = "nativeauthapi_disabled"; diff --git a/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiTypesBase.ts b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiTypesBase.ts new file mode 100644 index 0000000000..35d8eb8b56 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/custom_auth_api/types/ApiTypesBase.ts @@ -0,0 +1,15 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ServerTelemetryManager } from "@azure/msal-common/browser"; + +export type ApiRequestBase = { + correlationId: string; + telemetryManager: ServerTelemetryManager; +}; + +export type ApiResponseBase = { + correlation_id: string; +}; diff --git a/lib/msal-browser/src/custom_auth/core/network_client/http_client/FetchHttpClient.ts b/lib/msal-browser/src/custom_auth/core/network_client/http_client/FetchHttpClient.ts new file mode 100644 index 0000000000..f750802e90 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/http_client/FetchHttpClient.ts @@ -0,0 +1,86 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { HttpMethod, IHttpClient, RequestBody } from "./IHttpClient.js"; +import { HttpError } from "../../error/HttpError.js"; +import { AADServerParamKeys, Logger } from "@azure/msal-common/browser"; +import { + FailedSendRequest, + NoNetworkConnectivity, +} from "../../error/HttpErrorCodes.js"; + +/** + * Implementation of IHttpClient using fetch. + */ +export class FetchHttpClient implements IHttpClient { + constructor(private logger: Logger) {} + + async sendAsync( + url: string | URL, + options: RequestInit + ): Promise { + const headers = options.headers as Record; + const correlationId = + headers?.[AADServerParamKeys.CLIENT_REQUEST_ID] || undefined; + + try { + this.logger.verbosePii(`Sending request to ${url}`, correlationId); + + const startTime = performance.now(); + const response = await fetch(url, options); + const endTime = performance.now(); + + this.logger.verbosePii( + `Request to '${url}' completed in ${ + endTime - startTime + }ms with status code ${response.status}`, + correlationId + ); + + return response; + } catch (e) { + this.logger.errorPii( + `Failed to send request to ${url}: ${e}`, + correlationId + ); + + if (!window.navigator.onLine) { + throw new HttpError( + NoNetworkConnectivity, + `No network connectivity: ${e}`, + correlationId + ); + } + + throw new HttpError( + FailedSendRequest, + `Failed to send request: ${e}`, + correlationId + ); + } + } + + async post( + url: string | URL, + body: RequestBody, + headers: Record = {} + ): Promise { + return this.sendAsync(url, { + method: HttpMethod.POST, + headers, + body, + }); + } + + async get( + url: string | URL, + headers: Record = {} + ): Promise { + return this.sendAsync(url, { + method: HttpMethod.GET, + headers, + }); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/network_client/http_client/IHttpClient.ts b/lib/msal-browser/src/custom_auth/core/network_client/http_client/IHttpClient.ts new file mode 100644 index 0000000000..43f4a77771 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/network_client/http_client/IHttpClient.ts @@ -0,0 +1,54 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export type RequestBody = + | string + | ArrayBuffer + | DataView + | Blob + | File + | URLSearchParams + | FormData + | ReadableStream; +/** + * Interface for HTTP client. + */ +export interface IHttpClient { + /** + * Sends a request. + * @param url The URL to send the request to. + * @param options Additional fetch options. + */ + sendAsync(url: string | URL, options: RequestInit): Promise; + + /** + * Sends a POST request. + * @param url The URL to send the request to. + * @param body The body of the request. + * @param headers Optional headers for the request. + */ + post( + url: string | URL, + body: RequestBody, + headers?: Record + ): Promise; + + /** + * Sends a GET request. + * @param url The URL to send the request to. + * @param headers Optional headers for the request. + */ + get(url: string | URL, headers?: Record): Promise; +} + +/** + * Represents an HTTP method type. + */ +export const HttpMethod = { + GET: "GET", + POST: "POST", + PUT: "PUT", + DELETE: "DELETE", +} as const; diff --git a/lib/msal-browser/src/custom_auth/core/telemetry/PublicApiId.ts b/lib/msal-browser/src/custom_auth/core/telemetry/PublicApiId.ts new file mode 100644 index 0000000000..487cf49bbc --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/telemetry/PublicApiId.ts @@ -0,0 +1,37 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/* + * The public API ids should be claim in the MSAL telemtry tracker. + * All the following ids are hardcoded; so we need to find a way to claim them in the future and update them here. + */ + +// Sign in +export const SIGN_IN_WITH_CODE_START = 100001; +export const SIGN_IN_WITH_PASSWORD_START = 100002; +export const SIGN_IN_SUBMIT_CODE = 100003; +export const SIGN_IN_SUBMIT_PASSWORD = 100004; +export const SIGN_IN_RESEND_CODE = 100005; +export const SIGN_IN_AFTER_SIGN_UP = 100006; +export const SIGN_IN_AFTER_PASSWORD_RESET = 100007; + +// Sign up +export const SIGN_UP_WITH_PASSWORD_START = 100021; +export const SIGN_UP_START = 100022; +export const SIGN_UP_SUBMIT_CODE = 100023; +export const SIGN_UP_SUBMIT_PASSWORD = 100024; +export const SIGN_UP_SUBMIT_ATTRIBUTES = 100025; +export const SIGN_UP_RESEND_CODE = 100026; + +// Password reset +export const PASSWORD_RESET_START = 100041; +export const PASSWORD_RESET_SUBMIT_CODE = 100042; +export const PASSWORD_RESET_SUBMIT_PASSWORD = 100043; +export const PASSWORD_RESET_RESEND_CODE = 100044; + +// Get account +export const ACCOUNT_GET_ACCOUNT = 100061; +export const ACCOUNT_SIGN_OUT = 100062; +export const ACCOUNT_GET_ACCESS_TOKEN = 100063; diff --git a/lib/msal-browser/src/custom_auth/core/utils/ArgumentValidator.ts b/lib/msal-browser/src/custom_auth/core/utils/ArgumentValidator.ts new file mode 100644 index 0000000000..2d5e385ea4 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/utils/ArgumentValidator.ts @@ -0,0 +1,26 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { InvalidArgumentError } from "../error/InvalidArgumentError.js"; + +export function ensureArgumentIsNotNullOrUndefined( + argName: string, + argValue: T | undefined | null, + correlationId?: string +): asserts argValue is T { + if (argValue === null || argValue === undefined) { + throw new InvalidArgumentError(argName, correlationId); + } +} + +export function ensureArgumentIsNotEmptyString( + argName: string, + argValue: string | undefined, + correlationId?: string +): void { + if (!argValue || argValue.trim() === "") { + throw new InvalidArgumentError(argName, correlationId); + } +} diff --git a/lib/msal-browser/src/custom_auth/core/utils/UrlUtils.ts b/lib/msal-browser/src/custom_auth/core/utils/UrlUtils.ts new file mode 100644 index 0000000000..dde2235ca2 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/core/utils/UrlUtils.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ParsedUrlError } from "../error/ParsedUrlError.js"; +import { InvalidUrl } from "../error/ParsedUrlErrorCodes.js"; + +export function parseUrl(url: string): URL { + try { + return new URL(url); + } catch (e) { + throw new ParsedUrlError( + InvalidUrl, + `The URL "${url}" is invalid: ${e}` + ); + } +} + +export function buildUrl(baseUrl: string, path: string): URL { + const newBaseUrl = !baseUrl.endsWith("/") ? `${baseUrl}/` : baseUrl; + const newPath = path.startsWith("/") ? path.slice(1) : path; + const url = new URL(newPath, newBaseUrl); + return url; +} diff --git a/lib/msal-browser/src/custom_auth/get_account/auth_flow/CustomAuthAccountData.ts b/lib/msal-browser/src/custom_auth/get_account/auth_flow/CustomAuthAccountData.ts new file mode 100644 index 0000000000..e7ae2cf763 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/get_account/auth_flow/CustomAuthAccountData.ts @@ -0,0 +1,185 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthBrowserConfiguration } from "../../configuration/CustomAuthConfiguration.js"; +import { SignOutResult } from "./result/SignOutResult.js"; +import { GetAccessTokenResult } from "./result/GetAccessTokenResult.js"; +import { CustomAuthSilentCacheClient } from "../interaction_client/CustomAuthSilentCacheClient.js"; +import { NoCachedAccountFoundError } from "../../core/error/NoCachedAccountFoundError.js"; +import { DefaultScopes } from "../../CustomAuthConstants.js"; +import { AccessTokenRetrievalInputs } from "../../CustomAuthActionInputs.js"; +import { + AccountInfo, + AuthenticationScheme, + CommonSilentFlowRequest, + Logger, + TokenClaims, +} from "@azure/msal-common/browser"; +import { SilentRequest } from "../../../request/SilentRequest.js"; +import { + ensureArgumentIsNotEmptyString, + ensureArgumentIsNotNullOrUndefined, +} from "../../core/utils/ArgumentValidator.js"; + +/* + * Account information. + */ +export class CustomAuthAccountData { + constructor( + private readonly account: AccountInfo, + private readonly config: CustomAuthBrowserConfiguration, + private readonly cacheClient: CustomAuthSilentCacheClient, + private readonly logger: Logger, + private readonly correlationId: string + ) { + ensureArgumentIsNotEmptyString("correlationId", correlationId); + ensureArgumentIsNotNullOrUndefined("account", account, correlationId); + } + + /** + * This method triggers a sign-out operation, + * which removes the current account info and its tokens from browser cache. + * If sign-out successfully, redirect the page to postLogoutRedirectUri if provided in the configuration. + * @returns {Promise} The result of the SignOut operation. + */ + async signOut(): Promise { + try { + const currentAccount = this.cacheClient.getCurrentAccount( + this.correlationId + ); + + if (!currentAccount) { + throw new NoCachedAccountFoundError(this.correlationId); + } + + this.logger.verbose("Signing out user", this.correlationId); + + await this.cacheClient.logout({ + correlationId: this.correlationId, + account: currentAccount, + }); + + this.logger.verbose("User signed out", this.correlationId); + + return new SignOutResult(); + } catch (error) { + this.logger.errorPii( + `An error occurred during sign out: ${error}`, + this.correlationId + ); + + return SignOutResult.createWithError(error); + } + } + + getAccount(): AccountInfo { + return this.account; + } + + /** + * Gets the raw id-token of current account. + * Idtoken is only issued if openid scope is present in the scopes parameter when requesting for tokens, + * otherwise will return undefined from the response. + * @returns {string|undefined} The account id-token. + */ + getIdToken(): string | undefined { + return this.account.idToken; + } + + /** + * Gets the id token claims extracted from raw IdToken of current account. + * @returns {AuthTokenClaims|undefined} The token claims. + */ + getClaims(): AuthTokenClaims | undefined { + return this.account.idTokenClaims; + } + + /** + * Gets the access token of current account from browser cache if it is not expired, + * otherwise renew the token using cached refresh token if valid. + * If no refresh token is found or it is expired, then throws error. + * @param {AccessTokenRetrievalInputs} accessTokenRetrievalInputs - The inputs for retrieving the access token. + * @returns {Promise} The result of the operation. + */ + async getAccessToken( + accessTokenRetrievalInputs: AccessTokenRetrievalInputs + ): Promise { + try { + ensureArgumentIsNotNullOrUndefined( + "accessTokenRetrievalInputs", + accessTokenRetrievalInputs, + this.correlationId + ); + + this.logger.verbose("Getting current account.", this.correlationId); + + const currentAccount = this.cacheClient.getCurrentAccount( + this.account.username + ); + + if (!currentAccount) { + throw new NoCachedAccountFoundError(this.correlationId); + } + + this.logger.verbose("Getting access token.", this.correlationId); + + const newScopes = + accessTokenRetrievalInputs.scopes && + accessTokenRetrievalInputs.scopes.length > 0 + ? accessTokenRetrievalInputs.scopes + : [...DefaultScopes]; + const commonSilentFlowRequest = this.createCommonSilentFlowRequest( + currentAccount, + accessTokenRetrievalInputs.forceRefresh, + newScopes + ); + const result = await this.cacheClient.acquireToken( + commonSilentFlowRequest + ); + + this.logger.verbose( + "Successfully got access token from cache.", + this.correlationId + ); + + return new GetAccessTokenResult(result); + } catch (error) { + this.logger.error( + "Failed to get access token from cache.", + this.correlationId + ); + + return GetAccessTokenResult.createWithError(error); + } + } + + private createCommonSilentFlowRequest( + accountInfo: AccountInfo, + forceRefresh: boolean = false, + requestScopes: Array + ): CommonSilentFlowRequest { + const silentRequest: SilentRequest = { + authority: this.config.auth.authority, + correlationId: this.correlationId, + scopes: requestScopes || [], + account: accountInfo, + forceRefresh: forceRefresh || false, + storeInCache: { + idToken: true, + accessToken: true, + refreshToken: true, + }, + }; + + return { + ...silentRequest, + authenticationScheme: AuthenticationScheme.BEARER, + } as CommonSilentFlowRequest; + } +} + +export type AuthTokenClaims = TokenClaims & { + [key: string]: string | number | string[] | object | undefined | unknown; +}; diff --git a/lib/msal-browser/src/custom_auth/get_account/auth_flow/error_type/GetAccountError.ts b/lib/msal-browser/src/custom_auth/get_account/auth_flow/error_type/GetAccountError.ts new file mode 100644 index 0000000000..33d0a865d2 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/get_account/auth_flow/error_type/GetAccountError.ts @@ -0,0 +1,45 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowErrorBase } from "../../../core/auth_flow/AuthFlowErrorBase.js"; + +/** + * The error class for get account errors. + */ +export class GetAccountError extends AuthFlowErrorBase { + /** + * Checks if the error is due to no cached account found. + * @returns true if the error is due to no cached account found, false otherwise. + */ + isCurrentAccountNotFound(): boolean { + return this.isNoCachedAccountFoundError(); + } +} + +/** + * The error class for sign-out errors. + */ +export class SignOutError extends AuthFlowErrorBase { + /** + * Checks if the error is due to the user is not signed in. + * @returns true if the error is due to the user is not signed in, false otherwise. + */ + isUserNotSignedIn(): boolean { + return this.isNoCachedAccountFoundError(); + } +} + +/** + * The error class for getting the current account access token errors. + */ +export class GetCurrentAccountAccessTokenError extends AuthFlowErrorBase { + /** + * Checks if the error is due to no cached account found. + * @returns true if the error is due to no cached account found, false otherwise. + */ + isCurrentAccountNotFound(): boolean { + return this.isNoCachedAccountFoundError(); + } +} diff --git a/lib/msal-browser/src/custom_auth/get_account/auth_flow/result/GetAccessTokenResult.ts b/lib/msal-browser/src/custom_auth/get_account/auth_flow/result/GetAccessTokenResult.ts new file mode 100644 index 0000000000..b0699df999 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/get_account/auth_flow/result/GetAccessTokenResult.ts @@ -0,0 +1,72 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthenticationResult } from "../../../../response/AuthenticationResult.js"; +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { GetCurrentAccountAccessTokenError } from "../error_type/GetAccountError.js"; +import { + GetAccessTokenCompletedState, + GetAccessTokenFailedState, +} from "../state/GetAccessTokenState.js"; + +/* + * Result of getting an access token. + */ +export class GetAccessTokenResult extends AuthFlowResultBase< + GetAccessTokenResultState, + GetCurrentAccountAccessTokenError, + AuthenticationResult +> { + /** + * Creates a new instance of GetAccessTokenResult. + * @param resultData The result data of the access token. + */ + constructor(resultData?: AuthenticationResult) { + super(new GetAccessTokenCompletedState(), resultData); + } + + /** + * Creates a new instance of GetAccessTokenResult with an error. + * @param error The error that occurred. + * @return {GetAccessTokenResult} The result with the error. + */ + static createWithError(error: unknown): GetAccessTokenResult { + const result = new GetAccessTokenResult(); + result.error = new GetCurrentAccountAccessTokenError( + GetAccessTokenResult.createErrorData(error) + ); + result.state = new GetAccessTokenFailedState(); + + return result; + } + + /** + * Checks if the result is completed. + */ + isCompleted(): this is GetAccessTokenResult & { + state: GetAccessTokenCompletedState; + } { + return this.state instanceof GetAccessTokenCompletedState; + } + + /** + * Checks if the result is failed. + */ + isFailed(): this is GetAccessTokenResult & { + state: GetAccessTokenFailedState; + } { + return this.state instanceof GetAccessTokenFailedState; + } +} + +/** + * The possible states for the GetAccessTokenResult. + * This includes: + * - GetAccessTokenCompletedState: The access token was successfully retrieved. + * - GetAccessTokenFailedState: The access token retrieval failed. + */ +export type GetAccessTokenResultState = + | GetAccessTokenCompletedState + | GetAccessTokenFailedState; diff --git a/lib/msal-browser/src/custom_auth/get_account/auth_flow/result/GetAccountResult.ts b/lib/msal-browser/src/custom_auth/get_account/auth_flow/result/GetAccountResult.ts new file mode 100644 index 0000000000..e1950f33b4 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/get_account/auth_flow/result/GetAccountResult.ts @@ -0,0 +1,69 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { CustomAuthAccountData } from "../CustomAuthAccountData.js"; +import { GetAccountError } from "../error_type/GetAccountError.js"; +import { + GetAccountCompletedState, + GetAccountFailedState, +} from "../state/GetAccountState.js"; + +/* + * Result of getting an account. + */ +export class GetAccountResult extends AuthFlowResultBase< + GetAccountResultState, + GetAccountError, + CustomAuthAccountData +> { + /** + * Creates a new instance of GetAccountResult. + * @param resultData The result data. + */ + constructor(resultData?: CustomAuthAccountData) { + super(new GetAccountCompletedState(), resultData); + } + + /** + * Creates a new instance of GetAccountResult with an error. + * @param error The error data. + */ + static createWithError(error: unknown): GetAccountResult { + const result = new GetAccountResult(); + result.error = new GetAccountError( + GetAccountResult.createErrorData(error) + ); + result.state = new GetAccountFailedState(); + + return result; + } + + /** + * Checks if the result is in a completed state. + */ + isCompleted(): this is GetAccountResult & { + state: GetAccountCompletedState; + } { + return this.state instanceof GetAccountCompletedState; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is GetAccountResult & { state: GetAccountFailedState } { + return this.state instanceof GetAccountFailedState; + } +} + +/** + * The possible states for the GetAccountResult. + * This includes: + * - GetAccountCompletedState: The account was successfully retrieved. + * - GetAccountFailedState: The account retrieval failed. + */ +export type GetAccountResultState = + | GetAccountCompletedState + | GetAccountFailedState; diff --git a/lib/msal-browser/src/custom_auth/get_account/auth_flow/result/SignOutResult.ts b/lib/msal-browser/src/custom_auth/get_account/auth_flow/result/SignOutResult.ts new file mode 100644 index 0000000000..001e87da6a --- /dev/null +++ b/lib/msal-browser/src/custom_auth/get_account/auth_flow/result/SignOutResult.ts @@ -0,0 +1,62 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { SignOutError } from "../error_type/GetAccountError.js"; +import { + SignOutCompletedState, + SignOutFailedState, +} from "../state/SignOutState.js"; + +/* + * Result of a sign-out operation. + */ +export class SignOutResult extends AuthFlowResultBase< + SignOutResultState, + SignOutError, + void +> { + /** + * Creates a new instance of SignOutResult. + * @param state The state of the result. + */ + constructor() { + super(new SignOutCompletedState()); + } + + /** + * Creates a new instance of SignOutResult with an error. + * @param error The error that occurred during the sign-out operation. + */ + static createWithError(error: unknown): SignOutResult { + const result = new SignOutResult(); + result.error = new SignOutError(SignOutResult.createErrorData(error)); + result.state = new SignOutFailedState(); + + return result; + } + + /** + * Checks if the sign-out operation is completed. + */ + isCompleted(): this is SignOutResult & { state: SignOutCompletedState } { + return this.state instanceof SignOutCompletedState; + } + + /** + * Checks if the sign-out operation failed. + */ + isFailed(): this is SignOutResult & { state: SignOutFailedState } { + return this.state instanceof SignOutFailedState; + } +} + +/** + * The possible states for the SignOutResult. + * This includes: + * - SignOutCompletedState: The sign-out operation was successful. + * - SignOutFailedState: The sign-out operation failed. + */ +export type SignOutResultState = SignOutCompletedState | SignOutFailedState; diff --git a/lib/msal-browser/src/custom_auth/get_account/auth_flow/state/GetAccessTokenState.ts b/lib/msal-browser/src/custom_auth/get_account/auth_flow/state/GetAccessTokenState.ts new file mode 100644 index 0000000000..77a2bc91ce --- /dev/null +++ b/lib/msal-browser/src/custom_auth/get_account/auth_flow/state/GetAccessTokenState.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js"; + +/** + * The completed state of the get access token flow. + */ +export class GetAccessTokenCompletedState extends AuthFlowStateBase {} + +/** + * The failed state of the get access token flow. + */ +export class GetAccessTokenFailedState extends AuthFlowStateBase {} diff --git a/lib/msal-browser/src/custom_auth/get_account/auth_flow/state/GetAccountState.ts b/lib/msal-browser/src/custom_auth/get_account/auth_flow/state/GetAccountState.ts new file mode 100644 index 0000000000..9489a06f51 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/get_account/auth_flow/state/GetAccountState.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js"; + +/** + * The completed state of the get account flow. + */ +export class GetAccountCompletedState extends AuthFlowStateBase {} + +/** + * The failed state of the get account flow. + */ +export class GetAccountFailedState extends AuthFlowStateBase {} diff --git a/lib/msal-browser/src/custom_auth/get_account/auth_flow/state/SignOutState.ts b/lib/msal-browser/src/custom_auth/get_account/auth_flow/state/SignOutState.ts new file mode 100644 index 0000000000..ef679df9d5 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/get_account/auth_flow/state/SignOutState.ts @@ -0,0 +1,16 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js"; + +/** + * The completed state of the sign-out flow. + */ +export class SignOutCompletedState extends AuthFlowStateBase {} + +/** + * The failed state of the sign-out flow. + */ +export class SignOutFailedState extends AuthFlowStateBase {} diff --git a/lib/msal-browser/src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.ts b/lib/msal-browser/src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.ts new file mode 100644 index 0000000000..32310451b5 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.ts @@ -0,0 +1,215 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthAuthority } from "../../core/CustomAuthAuthority.js"; +import { DefaultPackageInfo } from "../../CustomAuthConstants.js"; +import * as PublicApiId from "../../core/telemetry/PublicApiId.js"; +import { CustomAuthInteractionClientBase } from "../../core/interaction_client/CustomAuthInteractionClientBase.js"; +import { + AccountInfo, + ClientAuthError, + ClientAuthErrorCodes, + ClientConfiguration, + CommonSilentFlowRequest, + RefreshTokenClient, + ServerTelemetryManager, + SilentFlowClient, + UrlString, +} from "@azure/msal-common/browser"; +import { AuthenticationResult } from "../../../response/AuthenticationResult.js"; +import { ClearCacheRequest } from "../../../request/ClearCacheRequest.js"; +import { ApiId } from "../../../utils/BrowserConstants.js"; +import { getCurrentUri } from "../../../utils/BrowserUtils.js"; + +export class CustomAuthSilentCacheClient extends CustomAuthInteractionClientBase { + /** + * Acquires a token from the cache if it is not expired. Otherwise, makes a request to renew the token. + * If forceRresh is set to false, then looks up the access token in cache first. + * If access token is expired or not found, then uses refresh token to get a new access token. + * If no refresh token is found or it is expired, then throws error. + * If forceRefresh is set to true, then skips token cache lookup and fetches a new token using refresh token + * If no refresh token is found or it is expired, then throws error. + * @param silentRequest The silent request object. + * @returns {Promise} The promise that resolves to an AuthenticationResult. + */ + override async acquireToken( + silentRequest: CommonSilentFlowRequest + ): Promise { + const telemetryManager = this.initializeServerTelemetryManager( + PublicApiId.ACCOUNT_GET_ACCESS_TOKEN + ); + const clientConfig = this.getCustomAuthClientConfiguration( + telemetryManager, + this.customAuthAuthority + ); + const silentFlowClient = new SilentFlowClient( + clientConfig, + this.performanceClient + ); + + try { + this.logger.verbose( + "Starting silent flow to acquire token from cache", + this.correlationId + ); + + const result = await silentFlowClient.acquireCachedToken( + silentRequest + ); + + this.logger.verbose( + "Silent flow to acquire token from cache is completed and token is found", + this.correlationId + ); + + return result[0] as AuthenticationResult; + } catch (error) { + if ( + error instanceof ClientAuthError && + error.errorCode === ClientAuthErrorCodes.tokenRefreshRequired + ) { + this.logger.verbose( + "Token refresh is required to acquire token silently", + this.correlationId + ); + + const refreshTokenClient = new RefreshTokenClient( + clientConfig, + this.performanceClient + ); + + this.logger.verbose( + "Starting refresh flow to refresh token", + this.correlationId + ); + + const refreshTokenResult = + await refreshTokenClient.acquireTokenByRefreshToken( + silentRequest + ); + + this.logger.verbose( + "Refresh flow to refresh token is completed", + this.correlationId + ); + + return refreshTokenResult as AuthenticationResult; + } + + throw error; + } + } + + override async logout(logoutRequest?: ClearCacheRequest): Promise { + const validLogoutRequest = this.initializeLogoutRequest(logoutRequest); + + // Clear the cache + this.logger.verbose( + "Start to clear the cache", + logoutRequest?.correlationId + ); + await this.clearCacheOnLogout( + validLogoutRequest.correlationId, + validLogoutRequest?.account + ); + this.logger.verbose("Cache cleared", logoutRequest?.correlationId); + + const postLogoutRedirectUri = this.config.auth.postLogoutRedirectUri; + + if (postLogoutRedirectUri) { + const absoluteRedirectUri = UrlString.getAbsoluteUrl( + postLogoutRedirectUri, + getCurrentUri() + ); + + this.logger.verbose( + "Post logout redirect uri is set, redirecting to uri", + logoutRequest?.correlationId + ); + + // Redirect to post logout redirect uri + await this.navigationClient.navigateExternal(absoluteRedirectUri, { + apiId: ApiId.logout, + timeout: this.config.system.redirectNavigationTimeout, + noHistory: false, + }); + } + } + + getCurrentAccount(correlationId: string): AccountInfo | null { + let account: AccountInfo | null = null; + + this.logger.verbose( + "Getting the first account from cache.", + correlationId + ); + + const allAccounts = this.browserStorage.getAllAccounts( + {}, + correlationId + ); + + if (allAccounts.length > 0) { + if (allAccounts.length !== 1) { + this.logger.warning( + "Multiple accounts found in cache. This is not supported in the Native Auth scenario.", + correlationId + ); + } + + account = allAccounts[0]; + } + + if (account) { + this.logger.verbose("Account data found.", correlationId); + } else { + this.logger.verbose("No account data found.", correlationId); + } + + return account; + } + + private getCustomAuthClientConfiguration( + serverTelemetryManager: ServerTelemetryManager, + customAuthAuthority: CustomAuthAuthority + ): ClientConfiguration { + const logger = this.config.system.loggerOptions; + + return { + authOptions: { + clientId: this.config.auth.clientId, + authority: customAuthAuthority, + clientCapabilities: this.config.auth.clientCapabilities, + redirectUri: this.config.auth.redirectUri, + }, + systemOptions: { + tokenRenewalOffsetSeconds: + this.config.system.tokenRenewalOffsetSeconds, + preventCorsPreflight: true, + }, + loggerOptions: { + loggerCallback: logger.loggerCallback, + piiLoggingEnabled: logger.piiLoggingEnabled, + logLevel: logger.logLevel, + correlationId: this.correlationId, + }, + cacheOptions: { + claimsBasedCachingEnabled: + this.config.cache.claimsBasedCachingEnabled, + }, + cryptoInterface: this.browserCrypto, + networkInterface: this.networkClient, + storageInterface: this.browserStorage, + serverTelemetryManager: serverTelemetryManager, + libraryInfo: { + sku: DefaultPackageInfo.SKU, + version: DefaultPackageInfo.VERSION, + cpu: DefaultPackageInfo.CPU, + os: DefaultPackageInfo.OS, + }, + telemetry: this.config.telemetry, + }; + } +} diff --git a/lib/msal-browser/src/custom_auth/index.ts b/lib/msal-browser/src/custom_auth/index.ts new file mode 100644 index 0000000000..6cf025cf81 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/index.ts @@ -0,0 +1,185 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * @packageDocumentation + * @module @azure/msal-browser/custom-auth + */ + +/** + * This file is the entrypoint when importing with the custom-auth subpath e.g. "import { someExport } from @azure/msal-browser/custom-auth" + * Additional exports should be added to the applicable exports-*.ts files + */ + +// Application and Controller +export { CustomAuthPublicClientApplication } from "./CustomAuthPublicClientApplication.js"; +export { ICustomAuthPublicClientApplication } from "./ICustomAuthPublicClientApplication.js"; + +// Configuration +export { CustomAuthConfiguration } from "./configuration/CustomAuthConfiguration.js"; + +// Account Data +export { CustomAuthAccountData } from "./get_account/auth_flow/CustomAuthAccountData.js"; + +// Operation Inputs +export { + SignInInputs, + SignUpInputs, + ResetPasswordInputs, + AccountRetrievalInputs, + AccessTokenRetrievalInputs, + SignInWithContinuationTokenInputs, +} from "./CustomAuthActionInputs.js"; + +// Operation Base State +export { AuthFlowStateBase } from "./core/auth_flow/AuthFlowState.js"; +export { AuthFlowActionRequiredStateBase } from "./core/auth_flow/AuthFlowState.js"; + +// Sign-in State +export { SignInState } from "./sign_in/auth_flow/state/SignInState.js"; +export { SignInCodeRequiredState } from "./sign_in/auth_flow/state/SignInCodeRequiredState.js"; +export { SignInContinuationState } from "./sign_in/auth_flow/state/SignInContinuationState.js"; +export { SignInPasswordRequiredState } from "./sign_in/auth_flow/state/SignInPasswordRequiredState.js"; +export { SignInCompletedState } from "./sign_in/auth_flow/state/SignInCompletedState.js"; +export { SignInFailedState } from "./sign_in/auth_flow/state/SignInFailedState.js"; + +// Sign-in Results +export { + SignInResult, + SignInResultState, +} from "./sign_in/auth_flow/result/SignInResult.js"; +export { SignInSubmitCodeResult } from "./sign_in/auth_flow/result/SignInSubmitCodeResult.js"; +export { + SignInResendCodeResult, + SignInResendCodeResultState, +} from "./sign_in/auth_flow/result/SignInResendCodeResult.js"; +export { SignInSubmitPasswordResult } from "./sign_in/auth_flow/result/SignInSubmitPasswordResult.js"; +export { SignInSubmitCredentialResultState } from "./sign_in/auth_flow/result/SignInSubmitCredentialResult.js"; + +// Sign-in Errors +export { + SignInError, + SignInSubmitPasswordError, + SignInSubmitCodeError, + SignInResendCodeError, +} from "./sign_in/auth_flow/error_type/SignInError.js"; + +// Sign-up User Account Attributes +export { UserAccountAttributes } from "./UserAccountAttributes.js"; + +// Sign-up State +export { SignUpState } from "./sign_up/auth_flow/state/SignUpState.js"; +export { SignUpAttributesRequiredState } from "./sign_up/auth_flow/state/SignUpAttributesRequiredState.js"; +export { SignUpCodeRequiredState } from "./sign_up/auth_flow/state/SignUpCodeRequiredState.js"; +export { SignUpPasswordRequiredState } from "./sign_up/auth_flow/state/SignUpPasswordRequiredState.js"; +export { SignUpCompletedState } from "./sign_up/auth_flow/state/SignUpCompletedState.js"; +export { SignUpFailedState } from "./sign_up/auth_flow/state/SignUpFailedState.js"; + +// Sign-up Results +export { + SignUpResult, + SignUpResultState, +} from "./sign_up/auth_flow/result/SignUpResult.js"; +export { + SignUpSubmitAttributesResult, + SignUpSubmitAttributesResultState, +} from "./sign_up/auth_flow/result/SignUpSubmitAttributesResult.js"; +export { + SignUpSubmitCodeResult, + SignUpSubmitCodeResultState, +} from "./sign_up/auth_flow/result/SignUpSubmitCodeResult.js"; +export { + SignUpResendCodeResult, + SignUpResendCodeResultState, +} from "./sign_up/auth_flow/result/SignUpResendCodeResult.js"; +export { + SignUpSubmitPasswordResult, + SignUpSubmitPasswordResultState, +} from "./sign_up/auth_flow/result/SignUpSubmitPasswordResult.js"; + +// Sign-up Errors +export { + SignUpError, + SignUpSubmitPasswordError, + SignUpSubmitCodeError, + SignUpSubmitAttributesError, + SignUpResendCodeError, +} from "./sign_up/auth_flow/error_type/SignUpError.js"; + +// Reset-password State +export { ResetPasswordState } from "./reset_password/auth_flow/state/ResetPasswordState.js"; +export { ResetPasswordCodeRequiredState } from "./reset_password/auth_flow/state/ResetPasswordCodeRequiredState.js"; +export { ResetPasswordPasswordRequiredState } from "./reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.js"; +export { ResetPasswordCompletedState } from "./reset_password/auth_flow/state/ResetPasswordCompletedState.js"; +export { ResetPasswordFailedState } from "./reset_password/auth_flow/state/ResetPasswordFailedState.js"; + +// Reset-password Results +export { + ResetPasswordStartResult, + ResetPasswordStartResultState, +} from "./reset_password/auth_flow/result/ResetPasswordStartResult.js"; +export { + ResetPasswordSubmitCodeResult, + ResetPasswordSubmitCodeResultState, +} from "./reset_password/auth_flow/result/ResetPasswordSubmitCodeResult.js"; +export { + ResetPasswordResendCodeResult, + ResetPasswordResendCodeResultState, +} from "./reset_password/auth_flow/result/ResetPasswordResendCodeResult.js"; +export { + ResetPasswordSubmitPasswordResult, + ResetPasswordSubmitPasswordResultState, +} from "./reset_password/auth_flow/result/ResetPasswordSubmitPasswordResult.js"; + +// Reset-password Errors +export { + ResetPasswordError, + ResetPasswordSubmitPasswordError, + ResetPasswordSubmitCodeError, + ResetPasswordResendCodeError, +} from "./reset_password/auth_flow/error_type/ResetPasswordError.js"; + +// Get Access Token Results +export { + GetAccessTokenResult, + GetAccessTokenResultState, +} from "./get_account/auth_flow/result/GetAccessTokenResult.js"; + +// Get Account Results +export { + GetAccountResult, + GetAccountResultState, +} from "./get_account/auth_flow/result/GetAccountResult.js"; + +// Sign Out Results +export { + SignOutResult, + SignOutResultState, +} from "./get_account/auth_flow/result/SignOutResult.js"; + +// Token Management Errors +export { + GetAccountError, + SignOutError, + GetCurrentAccountAccessTokenError, +} from "./get_account/auth_flow/error_type/GetAccountError.js"; + +// Errors +export { CustomAuthApiError } from "./core/error/CustomAuthApiError.js"; +export { CustomAuthError } from "./core/error/CustomAuthError.js"; +export { HttpError } from "./core/error/HttpError.js"; +export { InvalidArgumentError } from "./core/error/InvalidArgumentError.js"; +export { InvalidConfigurationError } from "./core/error/InvalidConfigurationError.js"; +export { MethodNotImplementedError } from "./core/error/MethodNotImplementedError.js"; +export { MsalCustomAuthError } from "./core/error/MsalCustomAuthError.js"; +export { NoCachedAccountFoundError } from "./core/error/NoCachedAccountFoundError.js"; +export { ParsedUrlError } from "./core/error/ParsedUrlError.js"; +export { UnexpectedError } from "./core/error/UnexpectedError.js"; +export { UnsupportedEnvironmentError } from "./core/error/UnsupportedEnvironmentError.js"; +export { UserAccountAttributeError } from "./core/error/UserAccountAttributeError.js"; +export { UserAlreadySignedInError } from "./core/error/UserAlreadySignedInError.js"; + +// Components from msal_browser +export { LogLevel } from "@azure/msal-common/browser"; diff --git a/lib/msal-browser/src/custom_auth/operating_context/CustomAuthOperatingContext.ts b/lib/msal-browser/src/custom_auth/operating_context/CustomAuthOperatingContext.ts new file mode 100644 index 0000000000..8fcde6498b --- /dev/null +++ b/lib/msal-browser/src/custom_auth/operating_context/CustomAuthOperatingContext.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { BaseOperatingContext } from "../../operatingcontext/BaseOperatingContext.js"; +import { + CustomAuthBrowserConfiguration, + CustomAuthConfiguration, + CustomAuthOptions, +} from "../configuration/CustomAuthConfiguration.js"; + +export class CustomAuthOperatingContext extends BaseOperatingContext { + private readonly customAuthOptions: CustomAuthOptions; + private static readonly MODULE_NAME: string = ""; + private static readonly ID: string = "CustomAuthOperatingContext"; + + constructor(configuration: CustomAuthConfiguration) { + super(configuration); + + this.customAuthOptions = configuration.customAuth; + } + + getModuleName(): string { + return CustomAuthOperatingContext.MODULE_NAME; + } + + getId(): string { + return CustomAuthOperatingContext.ID; + } + + getCustomAuthConfig(): CustomAuthBrowserConfiguration { + return { + ...this.getConfig(), + customAuth: this.customAuthOptions, + }; + } + + async initialize(): Promise { + this.available = typeof window !== "undefined"; + return this.available; + } +} diff --git a/lib/msal-browser/src/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.ts b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.ts new file mode 100644 index 0000000000..9b9e9ed2c1 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.ts @@ -0,0 +1,96 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthActionErrorBase } from "../../../core/auth_flow/AuthFlowErrorBase.js"; +import { CustomAuthApiError } from "../../../core/error/CustomAuthApiError.js"; +import * as CustomAuthApiErrorCode from "../../../core/network_client/custom_auth_api/types/ApiErrorCodes.js"; + +export class ResetPasswordError extends AuthActionErrorBase { + /** + * Checks if the error is due to the user not being found. + * @returns true if the error is due to the user not being found, false otherwise. + */ + isUserNotFound(): boolean { + return this.isUserNotFoundError(); + } + + /** + * Checks if the error is due to the username being invalid. + * @returns true if the error is due to the username being invalid, false otherwise. + */ + isInvalidUsername(): boolean { + return this.isUserInvalidError(); + } + + /** + * Checks if the error is due to the provided challenge type is not supported. + * @returns {boolean} True if the error is due to the provided challenge type is not supported, false otherwise. + */ + isUnsupportedChallengeType(): boolean { + return this.isUnsupportedChallengeTypeError(); + } + + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if client app doesn't support the challenge type configured in Entra, "loginPopup" function is required to continue the operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } +} + +export class ResetPasswordSubmitPasswordError extends AuthActionErrorBase { + /** + * Checks if the new password is invalid or incorrect. + * @returns {boolean} True if the new password is invalid, false otherwise. + */ + isInvalidPassword(): boolean { + return ( + this.isInvalidNewPasswordError() || this.isPasswordIncorrectError() + ); + } + + /** + * Checks if the password reset failed due to reset timeout or password change failed. + * @returns {boolean} True if the password reset failed, false otherwise. + */ + isPasswordResetFailed(): boolean { + return ( + this.errorData instanceof CustomAuthApiError && + (this.errorData.error === + CustomAuthApiErrorCode.PASSWORD_RESET_TIMEOUT || + this.errorData.error === + CustomAuthApiErrorCode.PASSWORD_CHANGE_FAILED) + ); + } +} + +export class ResetPasswordSubmitCodeError extends AuthActionErrorBase { + /** + * Checks if the provided code is invalid. + * @returns {boolean} True if the provided code is invalid, false otherwise. + */ + isInvalidCode(): boolean { + return this.isInvalidCodeError(); + } + + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if client app doesn't support the challenge type configured in Entra, "loginPopup" function is required to continue the operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } +} + +export class ResetPasswordResendCodeError extends AuthActionErrorBase { + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if client app doesn't support the challenge type configured in Entra, "loginPopup" function is required to continue the operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } +} diff --git a/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordResendCodeResult.ts b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordResendCodeResult.ts new file mode 100644 index 0000000000..b880b479eb --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordResendCodeResult.ts @@ -0,0 +1,76 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { ResetPasswordResendCodeError } from "../error_type/ResetPasswordError.js"; +import type { ResetPasswordCodeRequiredState } from "../state/ResetPasswordCodeRequiredState.js"; +import { ResetPasswordFailedState } from "../state/ResetPasswordFailedState.js"; + +/* + * Result of resending code in a reset password operation. + */ +export class ResetPasswordResendCodeResult extends AuthFlowResultBase< + ResetPasswordResendCodeResultState, + ResetPasswordResendCodeError, + void +> { + /** + * Creates a new instance of ResetPasswordResendCodeResult. + * @param state The state of the result. + */ + constructor(state: ResetPasswordResendCodeResultState) { + super(state); + } + + /** + * Creates a new instance of ResetPasswordResendCodeResult with an error. + * @param error The error that occurred. + * @returns {ResetPasswordResendCodeResult} A new instance of ResetPasswordResendCodeResult with the error set. + */ + static createWithError(error: unknown): ResetPasswordResendCodeResult { + const result = new ResetPasswordResendCodeResult( + new ResetPasswordFailedState() + ); + result.error = new ResetPasswordResendCodeError( + ResetPasswordResendCodeResult.createErrorData(error) + ); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is ResetPasswordResendCodeResult & { + state: ResetPasswordFailedState; + } { + return this.state instanceof ResetPasswordFailedState; + } + + /** + * Checks if the result is in a code required state. + */ + isCodeRequired(): this is ResetPasswordResendCodeResult & { + state: ResetPasswordCodeRequiredState; + } { + /* + * The instanceof operator couldn't be used here to check the state type since the circular dependency issue. + * So we are using the constructor name to check the state type. + */ + return ( + this.state.constructor?.name === "ResetPasswordCodeRequiredState" + ); + } +} + +/** + * The possible states for the ResetPasswordResendCodeResult. + * This includes: + * - ResetPasswordCodeRequiredState: The reset password process requires a code. + * - ResetPasswordFailedState: The reset password process has failed. + */ +export type ResetPasswordResendCodeResultState = + | ResetPasswordCodeRequiredState + | ResetPasswordFailedState; diff --git a/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordStartResult.ts b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordStartResult.ts new file mode 100644 index 0000000000..dc5a11d568 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordStartResult.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { ResetPasswordError } from "../error_type/ResetPasswordError.js"; +import { ResetPasswordCodeRequiredState } from "../state/ResetPasswordCodeRequiredState.js"; +import { ResetPasswordFailedState } from "../state/ResetPasswordFailedState.js"; + +/* + * Result of a reset password operation. + */ +export class ResetPasswordStartResult extends AuthFlowResultBase< + ResetPasswordStartResultState, + ResetPasswordError, + void +> { + /** + * Creates a new instance of ResetPasswordStartResult. + * @param state The state of the result. + */ + constructor(state: ResetPasswordStartResultState) { + super(state); + } + + /** + * Creates a new instance of ResetPasswordStartResult with an error. + * @param error The error that occurred. + * @returns {ResetPasswordStartResult} A new instance of ResetPasswordStartResult with the error set. + */ + static createWithError(error: unknown): ResetPasswordStartResult { + const result = new ResetPasswordStartResult( + new ResetPasswordFailedState() + ); + result.error = new ResetPasswordError( + ResetPasswordStartResult.createErrorData(error) + ); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is ResetPasswordStartResult & { + state: ResetPasswordFailedState; + } { + return this.state instanceof ResetPasswordFailedState; + } + + /** + * Checks if the result is in a code required state. + */ + isCodeRequired(): this is ResetPasswordStartResult & { + state: ResetPasswordCodeRequiredState; + } { + return this.state instanceof ResetPasswordCodeRequiredState; + } +} + +/** + * The possible states for the ResetPasswordStartResult. + * This includes: + * - ResetPasswordCodeRequiredState: The reset password process requires a code. + * - ResetPasswordFailedState: The reset password process has failed. + */ +export type ResetPasswordStartResultState = + | ResetPasswordCodeRequiredState + | ResetPasswordFailedState; diff --git a/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitCodeResult.ts b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitCodeResult.ts new file mode 100644 index 0000000000..6e31209203 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitCodeResult.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { ResetPasswordSubmitCodeError } from "../error_type/ResetPasswordError.js"; +import { ResetPasswordFailedState } from "../state/ResetPasswordFailedState.js"; +import { ResetPasswordPasswordRequiredState } from "../state/ResetPasswordPasswordRequiredState.js"; + +/* + * Result of a reset password operation that requires a code. + */ +export class ResetPasswordSubmitCodeResult extends AuthFlowResultBase< + ResetPasswordSubmitCodeResultState, + ResetPasswordSubmitCodeError, + void +> { + /** + * Creates a new instance of ResetPasswordSubmitCodeResult. + * @param state The state of the result. + */ + constructor(state: ResetPasswordSubmitCodeResultState) { + super(state); + } + + /** + * Creates a new instance of ResetPasswordSubmitCodeResult with an error. + * @param error The error that occurred. + * @returns {ResetPasswordSubmitCodeResult} A new instance of ResetPasswordSubmitCodeResult with the error set. + */ + static createWithError(error: unknown): ResetPasswordSubmitCodeResult { + const result = new ResetPasswordSubmitCodeResult( + new ResetPasswordFailedState() + ); + result.error = new ResetPasswordSubmitCodeError( + ResetPasswordSubmitCodeResult.createErrorData(error) + ); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is ResetPasswordSubmitCodeResult & { + state: ResetPasswordFailedState; + } { + return this.state instanceof ResetPasswordFailedState; + } + + /** + * Checks if the result is in a password required state. + */ + isPasswordRequired(): this is ResetPasswordSubmitCodeResult & { + state: ResetPasswordPasswordRequiredState; + } { + return this.state instanceof ResetPasswordPasswordRequiredState; + } +} + +/** + * The possible states for the ResetPasswordSubmitCodeResult. + * This includes: + * - ResetPasswordPasswordRequiredState: The reset password process requires a password. + * - ResetPasswordFailedState: The reset password process has failed. + */ +export type ResetPasswordSubmitCodeResultState = + | ResetPasswordPasswordRequiredState + | ResetPasswordFailedState; diff --git a/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitPasswordResult.ts b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitPasswordResult.ts new file mode 100644 index 0000000000..66c8649002 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitPasswordResult.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { ResetPasswordSubmitPasswordError } from "../error_type/ResetPasswordError.js"; +import { ResetPasswordCompletedState } from "../state/ResetPasswordCompletedState.js"; +import { ResetPasswordFailedState } from "../state/ResetPasswordFailedState.js"; + +/* + * Result of a reset password operation that requires a password. + */ +export class ResetPasswordSubmitPasswordResult extends AuthFlowResultBase< + ResetPasswordSubmitPasswordResultState, + ResetPasswordSubmitPasswordError, + void +> { + /** + * Creates a new instance of ResetPasswordSubmitPasswordResult. + * @param state The state of the result. + */ + constructor(state: ResetPasswordSubmitPasswordResultState) { + super(state); + } + + static createWithError(error: unknown): ResetPasswordSubmitPasswordResult { + const result = new ResetPasswordSubmitPasswordResult( + new ResetPasswordFailedState() + ); + result.error = new ResetPasswordSubmitPasswordError( + ResetPasswordSubmitPasswordResult.createErrorData(error) + ); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is ResetPasswordSubmitPasswordResult & { + state: ResetPasswordFailedState; + } { + return this.state instanceof ResetPasswordFailedState; + } + + /** + * Checks if the result is in a completed state. + */ + isCompleted(): this is ResetPasswordSubmitPasswordResult & { + state: ResetPasswordCompletedState; + } { + return this.state instanceof ResetPasswordCompletedState; + } +} + +/** + * The possible states for the ResetPasswordSubmitPasswordResult. + * This includes: + * - ResetPasswordCompletedState: The reset password process has completed successfully. + * - ResetPasswordFailedState: The reset password process has failed. + */ +export type ResetPasswordSubmitPasswordResultState = + | ResetPasswordCompletedState + | ResetPasswordFailedState; diff --git a/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.ts b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.ts new file mode 100644 index 0000000000..fb2d9ff27c --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.ts @@ -0,0 +1,130 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ResetPasswordResendCodeResult } from "../result/ResetPasswordResendCodeResult.js"; +import { ResetPasswordSubmitCodeResult } from "../result/ResetPasswordSubmitCodeResult.js"; +import { ResetPasswordCodeRequiredStateParameters } from "./ResetPasswordStateParameters.js"; +import { ResetPasswordState } from "./ResetPasswordState.js"; +import { ResetPasswordPasswordRequiredState } from "./ResetPasswordPasswordRequiredState.js"; + +/* + * Reset password code required state. + */ +export class ResetPasswordCodeRequiredState extends ResetPasswordState { + /** + * Submits a one-time passcode that the customer user received in their email in order to continue password reset flow. + * @param {string} code - The code to submit. + * @returns {Promise} The result of the operation. + */ + async submitCode(code: string): Promise { + try { + this.ensureCodeIsValid(code, this.stateParameters.codeLength); + + this.stateParameters.logger.verbose( + "Submitting code for password reset.", + this.stateParameters.correlationId + ); + + const result = + await this.stateParameters.resetPasswordClient.submitCode({ + clientId: this.stateParameters.config.auth.clientId, + correlationId: this.stateParameters.correlationId, + challengeType: + this.stateParameters.config.customAuth.challengeTypes ?? + [], + continuationToken: + this.stateParameters.continuationToken ?? "", + code: code, + username: this.stateParameters.username, + }); + + this.stateParameters.logger.verbose( + "Code is submitted for password reset.", + this.stateParameters.correlationId + ); + + return new ResetPasswordSubmitCodeResult( + new ResetPasswordPasswordRequiredState({ + correlationId: result.correlationId, + continuationToken: result.continuationToken, + logger: this.stateParameters.logger, + config: this.stateParameters.config, + resetPasswordClient: + this.stateParameters.resetPasswordClient, + signInClient: this.stateParameters.signInClient, + cacheClient: this.stateParameters.cacheClient, + username: this.stateParameters.username, + }) + ); + } catch (error) { + this.stateParameters.logger.errorPii( + `Failed to submit code for password reset. Error: ${error}.`, + this.stateParameters.correlationId + ); + + return ResetPasswordSubmitCodeResult.createWithError(error); + } + } + + /** + * Resends another one-time passcode if the previous one hasn't been verified + * @returns {Promise} The result of the operation. + */ + async resendCode(): Promise { + try { + this.stateParameters.logger.verbose( + "Resending code for password reset.", + this.stateParameters.correlationId + ); + + const result = + await this.stateParameters.resetPasswordClient.resendCode({ + clientId: this.stateParameters.config.auth.clientId, + challengeType: + this.stateParameters.config.customAuth.challengeTypes ?? + [], + username: this.stateParameters.username, + correlationId: this.stateParameters.correlationId, + continuationToken: + this.stateParameters.continuationToken ?? "", + }); + + this.stateParameters.logger.verbose( + "Code is resent for password reset.", + this.stateParameters.correlationId + ); + + return new ResetPasswordResendCodeResult( + new ResetPasswordCodeRequiredState({ + correlationId: result.correlationId, + continuationToken: result.continuationToken, + logger: this.stateParameters.logger, + config: this.stateParameters.config, + resetPasswordClient: + this.stateParameters.resetPasswordClient, + signInClient: this.stateParameters.signInClient, + cacheClient: this.stateParameters.cacheClient, + username: this.stateParameters.username, + codeLength: result.codeLength, + }) + ); + } catch (error) { + this.stateParameters.logger.errorPii( + `Failed to resend code for password reset. Error: ${error}.`, + this.stateParameters.correlationId + ); + + return ResetPasswordResendCodeResult.createWithError(error); + } + } + + /** + * Gets the sent code length. + * @returns {number} The length of the code. + */ + getCodeLength(): number { + return this.stateParameters.codeLength; + } +} diff --git a/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordCompletedState.ts b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordCompletedState.ts new file mode 100644 index 0000000000..a1533df316 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordCompletedState.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { SignInContinuationState } from "../../../sign_in/auth_flow/state/SignInContinuationState.js"; + +/** + * Represents the state that indicates the successful completion of a password reset operation. + */ +export class ResetPasswordCompletedState extends SignInContinuationState {} diff --git a/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordFailedState.ts b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordFailedState.ts new file mode 100644 index 0000000000..a920970fbe --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordFailedState.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js"; + +/** + * State of a reset password operation that has failed. + */ +export class ResetPasswordFailedState extends AuthFlowStateBase {} diff --git a/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.ts b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.ts new file mode 100644 index 0000000000..ffb230ec93 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.ts @@ -0,0 +1,73 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ResetPasswordSubmitPasswordResult } from "../result/ResetPasswordSubmitPasswordResult.js"; +import { ResetPasswordState } from "./ResetPasswordState.js"; +import { ResetPasswordPasswordRequiredStateParameters } from "./ResetPasswordStateParameters.js"; +import { ResetPasswordCompletedState } from "./ResetPasswordCompletedState.js"; +import { SignInScenario } from "../../../sign_in/auth_flow/SignInScenario.js"; + +/* + * Reset password password required state. + */ +export class ResetPasswordPasswordRequiredState extends ResetPasswordState { + /** + * Submits a new password for reset password flow. + * @param {string} password - The password to submit. + * @returns {Promise} The result of the operation. + */ + async submitNewPassword( + password: string + ): Promise { + try { + this.ensurePasswordIsNotEmpty(password); + + this.stateParameters.logger.verbose( + "Submitting new password for password reset.", + this.stateParameters.correlationId + ); + + const result = + await this.stateParameters.resetPasswordClient.submitNewPassword( + { + clientId: this.stateParameters.config.auth.clientId, + correlationId: this.stateParameters.correlationId, + challengeType: + this.stateParameters.config.customAuth + .challengeTypes ?? [], + continuationToken: + this.stateParameters.continuationToken ?? "", + newPassword: password, + username: this.stateParameters.username, + } + ); + + this.stateParameters.logger.verbose( + "New password is submitted for sign-up.", + this.stateParameters.correlationId + ); + + return new ResetPasswordSubmitPasswordResult( + new ResetPasswordCompletedState({ + correlationId: result.correlationId, + continuationToken: result.continuationToken, + logger: this.stateParameters.logger, + config: this.stateParameters.config, + username: this.stateParameters.username, + signInClient: this.stateParameters.signInClient, + cacheClient: this.stateParameters.cacheClient, + signInScenario: SignInScenario.SignInAfterPasswordReset, + }) + ); + } catch (error) { + this.stateParameters.logger.errorPii( + `Failed to submit password for password reset. Error: ${error}.`, + this.stateParameters.correlationId + ); + + return ResetPasswordSubmitPasswordResult.createWithError(error); + } + } +} diff --git a/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordState.ts b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordState.ts new file mode 100644 index 0000000000..e9c81480ac --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordState.ts @@ -0,0 +1,29 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowActionRequiredStateBase } from "../../../core/auth_flow/AuthFlowState.js"; +import { ensureArgumentIsNotEmptyString } from "../../../core/utils/ArgumentValidator.js"; +import { ResetPasswordStateParameters } from "./ResetPasswordStateParameters.js"; + +/* + * Base state handler for reset password operation. + */ +export abstract class ResetPasswordState< + TParameters extends ResetPasswordStateParameters +> extends AuthFlowActionRequiredStateBase { + /* + * Creates a new state for reset password operation. + * @param stateParameters - The state parameters for reset-password. + */ + constructor(stateParameters: TParameters) { + super(stateParameters); + + ensureArgumentIsNotEmptyString( + "username", + this.stateParameters.username, + this.stateParameters.correlationId + ); + } +} diff --git a/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordStateParameters.ts b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordStateParameters.ts new file mode 100644 index 0000000000..f8f16a01b5 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/auth_flow/state/ResetPasswordStateParameters.ts @@ -0,0 +1,25 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ResetPasswordClient } from "../../interaction_client/ResetPasswordClient.js"; +import { SignInClient } from "../../../sign_in/interaction_client/SignInClient.js"; +import { CustomAuthSilentCacheClient } from "../../../get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { AuthFlowActionRequiredStateParameters } from "../../../core/auth_flow/AuthFlowState.js"; + +export interface ResetPasswordStateParameters + extends AuthFlowActionRequiredStateParameters { + username: string; + resetPasswordClient: ResetPasswordClient; + signInClient: SignInClient; + cacheClient: CustomAuthSilentCacheClient; +} + +export type ResetPasswordPasswordRequiredStateParameters = + ResetPasswordStateParameters; + +export interface ResetPasswordCodeRequiredStateParameters + extends ResetPasswordStateParameters { + codeLength: number; +} diff --git a/lib/msal-browser/src/custom_auth/reset_password/interaction_client/ResetPasswordClient.ts b/lib/msal-browser/src/custom_auth/reset_password/interaction_client/ResetPasswordClient.ts new file mode 100644 index 0000000000..bab66b22db --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/interaction_client/ResetPasswordClient.ts @@ -0,0 +1,311 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ServerTelemetryManager } from "@azure/msal-common/browser"; +import { CustomAuthApiError } from "../../core/error/CustomAuthApiError.js"; +import { CustomAuthInteractionClientBase } from "../../core/interaction_client/CustomAuthInteractionClientBase.js"; +import * as CustomAuthApiErrorCode from "../../core/network_client/custom_auth_api/types/ApiErrorCodes.js"; +import { + ResetPasswordChallengeRequest, + ResetPasswordContinueRequest, + ResetPasswordPollCompletionRequest, + ResetPasswordStartRequest, + ResetPasswordSubmitRequest, +} from "../../core/network_client/custom_auth_api/types/ApiRequestTypes.js"; +import * as PublicApiId from "../../core/telemetry/PublicApiId.js"; +import { + ChallengeType, + DefaultCustomAuthApiCodeLength, + PasswordResetPollingTimeoutInMs, + ResetPasswordPollStatus, +} from "../../CustomAuthConstants.js"; +import { + ResetPasswordResendCodeParams, + ResetPasswordStartParams, + ResetPasswordSubmitCodeParams, + ResetPasswordSubmitNewPasswordParams, +} from "./parameter/ResetPasswordParams.js"; +import { + ResetPasswordCodeRequiredResult, + ResetPasswordCompletedResult, + ResetPasswordPasswordRequiredResult, +} from "./result/ResetPasswordActionResult.js"; +import { ensureArgumentIsNotEmptyString } from "../../core/utils/ArgumentValidator.js"; + +export class ResetPasswordClient extends CustomAuthInteractionClientBase { + /** + * Starts the password reset flow. + * @param parameters The parameters for starting the password reset flow. + * @returns The result of password reset start operation. + */ + async start( + parameters: ResetPasswordStartParams + ): Promise { + const correlationId = parameters.correlationId; + const apiId = PublicApiId.PASSWORD_RESET_START; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + + const startRequest: ResetPasswordStartRequest = { + challenge_type: this.getChallengeTypes(parameters.challengeType), + username: parameters.username, + correlationId: correlationId, + telemetryManager: telemetryManager, + }; + + this.logger.verbose( + "Calling start endpoint for password reset flow.", + correlationId + ); + + const startResponse = + await this.customAuthApiClient.resetPasswordApi.start(startRequest); + + this.logger.verbose( + "Start endpoint for password reset returned successfully.", + correlationId + ); + + const challengeRequest: ResetPasswordChallengeRequest = { + continuation_token: startResponse.continuation_token ?? "", + challenge_type: this.getChallengeTypes(parameters.challengeType), + correlationId: correlationId, + telemetryManager: telemetryManager, + }; + + return this.performChallengeRequest(challengeRequest); + } + + /** + * Submits the code for password reset. + * @param parameters The parameters for submitting the code for password reset. + * @returns The result of submitting the code for password reset. + */ + async submitCode( + parameters: ResetPasswordSubmitCodeParams + ): Promise { + const correlationId = parameters.correlationId; + ensureArgumentIsNotEmptyString( + "parameters.code", + parameters.code, + correlationId + ); + + const apiId = PublicApiId.PASSWORD_RESET_SUBMIT_CODE; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + + const continueRequest: ResetPasswordContinueRequest = { + continuation_token: parameters.continuationToken, + oob: parameters.code, + correlationId: correlationId, + telemetryManager: telemetryManager, + }; + + this.logger.verbose( + "Calling continue endpoint with code for password reset.", + correlationId + ); + + const response = + await this.customAuthApiClient.resetPasswordApi.continueWithCode( + continueRequest + ); + + this.logger.verbose( + "Continue endpoint called successfully with code for password reset.", + response.correlation_id + ); + + return { + correlationId: response.correlation_id, + continuationToken: response.continuation_token ?? "", + }; + } + + /** + * Resends the another one-time passcode if the previous one hasn't been verified + * @param parameters The parameters for resending the code for password reset. + * @returns The result of resending the code for password reset. + */ + async resendCode( + parameters: ResetPasswordResendCodeParams + ): Promise { + const apiId = PublicApiId.PASSWORD_RESET_RESEND_CODE; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + + const challengeRequest: ResetPasswordChallengeRequest = { + continuation_token: parameters.continuationToken, + challenge_type: this.getChallengeTypes(parameters.challengeType), + correlationId: parameters.correlationId, + telemetryManager: telemetryManager, + }; + + return this.performChallengeRequest(challengeRequest); + } + + /** + * Submits the new password for password reset. + * @param parameters The parameters for submitting the new password for password reset. + * @returns The result of submitting the new password for password reset. + */ + async submitNewPassword( + parameters: ResetPasswordSubmitNewPasswordParams + ): Promise { + const correlationId = parameters.correlationId; + + ensureArgumentIsNotEmptyString( + "parameters.newPassword", + parameters.newPassword, + correlationId + ); + + const apiId = PublicApiId.PASSWORD_RESET_SUBMIT_PASSWORD; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + + const submitRequest: ResetPasswordSubmitRequest = { + continuation_token: parameters.continuationToken, + new_password: parameters.newPassword, + correlationId: correlationId, + telemetryManager: telemetryManager, + }; + + this.logger.verbose( + "Calling submit endpoint with new password for password reset.", + correlationId + ); + + const submitResponse = + await this.customAuthApiClient.resetPasswordApi.submitNewPassword( + submitRequest + ); + + this.logger.verbose( + "Submit endpoint called successfully with new password for password reset.", + correlationId + ); + + return this.performPollCompletionRequest( + submitResponse.continuation_token ?? "", + submitResponse.poll_interval, + correlationId, + telemetryManager + ); + } + + private async performChallengeRequest( + request: ResetPasswordChallengeRequest + ): Promise { + const correlationId = request.correlationId; + this.logger.verbose( + "Calling challenge endpoint for password reset flow.", + correlationId + ); + + const response = + await this.customAuthApiClient.resetPasswordApi.requestChallenge( + request + ); + + this.logger.verbose( + "Challenge endpoint for password reset returned successfully.", + correlationId + ); + + if (response.challenge_type === ChallengeType.OOB) { + // Code is required + this.logger.verbose( + "Code is required for password reset flow.", + correlationId + ); + + return { + correlationId: response.correlation_id, + continuationToken: response.continuation_token ?? "", + challengeChannel: response.challenge_channel ?? "", + challengeTargetLabel: response.challenge_target_label ?? "", + codeLength: + response.code_length ?? DefaultCustomAuthApiCodeLength, + bindingMethod: response.binding_method ?? "", + }; + } + + this.logger.error( + `Unsupported challenge type '${response.challenge_type}' returned from challenge endpoint for password reset.`, + correlationId + ); + + throw new CustomAuthApiError( + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + `Unsupported challenge type '${response.challenge_type}'.`, + correlationId + ); + } + + private async performPollCompletionRequest( + continuationToken: string, + pollInterval: number, + correlationId: string, + telemetryManager: ServerTelemetryManager + ): Promise { + const startTime = performance.now(); + + while ( + performance.now() - startTime < + PasswordResetPollingTimeoutInMs + ) { + const pollRequest: ResetPasswordPollCompletionRequest = { + continuation_token: continuationToken, + correlationId: correlationId, + telemetryManager: telemetryManager, + }; + + this.logger.verbose( + "Calling the poll completion endpoint for password reset flow.", + correlationId + ); + + const pollResponse = + await this.customAuthApiClient.resetPasswordApi.pollCompletion( + pollRequest + ); + + this.logger.verbose( + "Poll completion endpoint for password reset returned successfully.", + correlationId + ); + + if (pollResponse.status === ResetPasswordPollStatus.SUCCEEDED) { + return { + correlationId: pollResponse.correlation_id, + continuationToken: pollResponse.continuation_token ?? "", + }; + } else if (pollResponse.status === ResetPasswordPollStatus.FAILED) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.PASSWORD_CHANGE_FAILED, + "Password is failed to be reset.", + pollResponse.correlation_id + ); + } + + this.logger.verbose( + `Poll completion endpoint for password reset is not started or in progress, waiting ${pollInterval} seconds for next check.`, + correlationId + ); + + await this.delay(pollInterval * 1000); + } + + this.logger.error("Password reset flow has timed out.", correlationId); + + throw new CustomAuthApiError( + CustomAuthApiErrorCode.PASSWORD_RESET_TIMEOUT, + "Password reset flow has timed out.", + correlationId + ); + } + + private async delay(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); + } +} diff --git a/lib/msal-browser/src/custom_auth/reset_password/interaction_client/parameter/ResetPasswordParams.ts b/lib/msal-browser/src/custom_auth/reset_password/interaction_client/parameter/ResetPasswordParams.ts new file mode 100644 index 0000000000..fdf38e8bf1 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/interaction_client/parameter/ResetPasswordParams.ts @@ -0,0 +1,28 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export interface ResetPasswordParamsBase { + clientId: string; + challengeType: Array; + username: string; + correlationId: string; +} + +export type ResetPasswordStartParams = ResetPasswordParamsBase; + +export interface ResetPasswordResendCodeParams extends ResetPasswordParamsBase { + continuationToken: string; +} + +export interface ResetPasswordSubmitCodeParams extends ResetPasswordParamsBase { + continuationToken: string; + code: string; +} + +export interface ResetPasswordSubmitNewPasswordParams + extends ResetPasswordParamsBase { + continuationToken: string; + newPassword: string; +} diff --git a/lib/msal-browser/src/custom_auth/reset_password/interaction_client/result/ResetPasswordActionResult.ts b/lib/msal-browser/src/custom_auth/reset_password/interaction_client/result/ResetPasswordActionResult.ts new file mode 100644 index 0000000000..c6b9b844bb --- /dev/null +++ b/lib/msal-browser/src/custom_auth/reset_password/interaction_client/result/ResetPasswordActionResult.ts @@ -0,0 +1,21 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +interface ResetPasswordActionResult { + correlationId: string; + continuationToken: string; +} + +export interface ResetPasswordCodeRequiredResult + extends ResetPasswordActionResult { + challengeChannel: string; + challengeTargetLabel: string; + codeLength: number; + bindingMethod: string; +} + +export type ResetPasswordPasswordRequiredResult = ResetPasswordActionResult; + +export type ResetPasswordCompletedResult = ResetPasswordActionResult; diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/SignInScenario.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/SignInScenario.ts new file mode 100644 index 0000000000..6c2c591b2c --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/SignInScenario.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export const SignInScenario = { + SignInAfterSignUp: "SignInAfterSignUp", + SignInAfterPasswordReset: "SignInAfterPasswordReset", +} as const; + +export type SignInScenarioType = + (typeof SignInScenario)[keyof typeof SignInScenario]; diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/error_type/SignInError.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/error_type/SignInError.ts new file mode 100644 index 0000000000..2adc8c5320 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/error_type/SignInError.ts @@ -0,0 +1,79 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthActionErrorBase } from "../../../core/auth_flow/AuthFlowErrorBase.js"; +import * as CustomAuthApiErrorCode from "../../../core/network_client/custom_auth_api/types/ApiErrorCodes.js"; + +export class SignInError extends AuthActionErrorBase { + /** + * Checks if the error is due to the user not being found. + * @returns true if the error is due to the user not being found, false otherwise. + */ + isUserNotFound(): boolean { + return this.errorData.error === CustomAuthApiErrorCode.USER_NOT_FOUND; + } + + /** + * Checks if the error is due to the username being invalid. + * @returns true if the error is due to the username being invalid, false otherwise. + */ + isInvalidUsername(): boolean { + return this.isUserInvalidError(); + } + + /** + * Checks if the error is due to the provided password being incorrect. + * @returns true if the error is due to the provided password being incorrect, false otherwise. + */ + isPasswordIncorrect(): boolean { + return this.isPasswordIncorrectError(); + } + + /** + * Checks if the error is due to the provided challenge type is not supported. + * @returns {boolean} True if the error is due to the provided challenge type is not supported, false otherwise. + */ + isUnsupportedChallengeType(): boolean { + return this.isUnsupportedChallengeTypeError(); + } + + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if "loginPopup" function is required to continue sthe operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } +} + +export class SignInSubmitPasswordError extends AuthActionErrorBase { + /** + * Checks if the password submitted during sign-in is incorrect. + * @returns {boolean} True if the error is due to the password being invalid, false otherwise. + */ + isInvalidPassword(): boolean { + return this.isPasswordIncorrectError(); + } +} + +export class SignInSubmitCodeError extends AuthActionErrorBase { + /** + * Checks if the code submitted during sign-in is invalid. + * @returns {boolean} True if the error is due to the code being invalid, false otherwise. + */ + isInvalidCode(): boolean { + return this.isInvalidCodeError(); + } +} + +export class SignInResendCodeError extends AuthActionErrorBase { + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if "loginPopup" function is required to continue sthe operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInResendCodeResult.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInResendCodeResult.ts new file mode 100644 index 0000000000..f9c598745b --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInResendCodeResult.ts @@ -0,0 +1,67 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { SignInResendCodeError } from "../error_type/SignInError.js"; +import type { SignInCodeRequiredState } from "../state/SignInCodeRequiredState.js"; +import { SignInFailedState } from "../state/SignInFailedState.js"; + +export class SignInResendCodeResult extends AuthFlowResultBase< + SignInResendCodeResultState, + SignInResendCodeError, + void +> { + /** + * Creates a new instance of SignInResendCodeResult. + * @param state The state of the result. + */ + constructor(state: SignInResendCodeResultState) { + super(state); + } + + /** + * Creates a new instance of SignInResendCodeResult with an error. + * @param error The error that occurred. + * @returns {SignInResendCodeResult} A new instance of SignInResendCodeResult with the error set. + */ + static createWithError(error: unknown): SignInResendCodeResult { + const result = new SignInResendCodeResult(new SignInFailedState()); + result.error = new SignInResendCodeError( + SignInResendCodeResult.createErrorData(error) + ); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is SignInResendCodeResult & { state: SignInFailedState } { + return this.state instanceof SignInFailedState; + } + + /** + * Checks if the result is in a code required state. + */ + isCodeRequired(): this is SignInResendCodeResult & { + state: SignInCodeRequiredState; + } { + /* + * The instanceof operator couldn't be used here to check the state type since the circular dependency issue. + * So we are using the constructor name to check the state type. + */ + return this.state.constructor?.name === "SignInCodeRequiredState"; + } +} + +/** + * The possible states for the SignInResendCodeResult. + * This includes: + * - SignInCodeRequiredState: The sign-in process requires a code. + * - SignInFailedState: The sign-in process has failed. + */ +export type SignInResendCodeResultState = + | SignInCodeRequiredState + | SignInFailedState; diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInResult.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInResult.ts new file mode 100644 index 0000000000..59d101d496 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInResult.ts @@ -0,0 +1,87 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthAccountData } from "../../../get_account/auth_flow/CustomAuthAccountData.js"; +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { SignInError } from "../error_type/SignInError.js"; +import { SignInCodeRequiredState } from "../state/SignInCodeRequiredState.js"; +import { SignInPasswordRequiredState } from "../state/SignInPasswordRequiredState.js"; +import { SignInFailedState } from "../state/SignInFailedState.js"; +import { SignInCompletedState } from "../state/SignInCompletedState.js"; + +/* + * Result of a sign-in operation. + */ +export class SignInResult extends AuthFlowResultBase< + SignInResultState, + SignInError, + CustomAuthAccountData +> { + /** + * Creates a new instance of SignInResultState. + * @param state The state of the result. + */ + constructor(state: SignInResultState, resultData?: CustomAuthAccountData) { + super(state, resultData); + } + + /** + * Creates a new instance of SignInResult with an error. + * @param error The error that occurred. + * @returns {SignInResult} A new instance of SignInResult with the error set. + */ + static createWithError(error: unknown): SignInResult { + const result = new SignInResult(new SignInFailedState()); + result.error = new SignInError(SignInResult.createErrorData(error)); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is SignInResult & { state: SignInFailedState } { + return this.state instanceof SignInFailedState; + } + + /** + * Checks if the result is in a code required state. + */ + isCodeRequired(): this is SignInResult & { + state: SignInCodeRequiredState; + } { + return this.state instanceof SignInCodeRequiredState; + } + + /** + * Checks if the result is in a password required state. + */ + isPasswordRequired(): this is SignInResult & { + state: SignInPasswordRequiredState; + } { + return this.state instanceof SignInPasswordRequiredState; + } + + /** + * Checks if the result is in a completed state. + */ + isCompleted(): this is SignInResult & { state: SignInCompletedState } { + return this.state instanceof SignInCompletedState; + } +} + +/** + * The possible states for the SignInResult. + * This includes: + * - SignInCodeRequiredState: The sign-in process requires a code. + * - SignInPasswordRequiredState: The sign-in process requires a password. + * - SignInFailedState: The sign-in process has failed. + * - SignInCompletedState: The sign-in process is completed. + */ +export type SignInResultState = + | SignInCodeRequiredState + | SignInPasswordRequiredState + | SignInFailedState + | SignInCompletedState; diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitCodeResult.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitCodeResult.ts new file mode 100644 index 0000000000..356178ffd4 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitCodeResult.ts @@ -0,0 +1,44 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { SignInSubmitCodeError } from "../error_type/SignInError.js"; +import { SignInCompletedState } from "../state/SignInCompletedState.js"; +import { SignInFailedState } from "../state/SignInFailedState.js"; +import { SignInSubmitCredentialResult } from "./SignInSubmitCredentialResult.js"; + +/* + * Result of a sign-in submit code operation. + */ +export class SignInSubmitCodeResult extends SignInSubmitCredentialResult { + /** + * Creates a new instance of SignInSubmitCodeResult with error data. + * @param error The error that occurred. + * @returns {SignInSubmitCodeResult} A new instance of SignInSubmitCodeResult with the error set. + */ + static createWithError(error: unknown): SignInSubmitCodeResult { + const result = new SignInSubmitCodeResult(new SignInFailedState()); + result.error = new SignInSubmitCodeError( + SignInSubmitCodeResult.createErrorData(error) + ); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is SignInSubmitCodeResult & { state: SignInFailedState } { + return this.state instanceof SignInFailedState; + } + + /** + * Checks if the result is in a completed state. + */ + isCompleted(): this is SignInSubmitCodeResult & { + state: SignInCompletedState; + } { + return this.state instanceof SignInCompletedState; + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitCredentialResult.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitCredentialResult.ts new file mode 100644 index 0000000000..53df78770d --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitCredentialResult.ts @@ -0,0 +1,43 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthAccountData } from "../../../get_account/auth_flow/CustomAuthAccountData.js"; +import { AuthFlowErrorBase } from "../../../core/auth_flow/AuthFlowErrorBase.js"; +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { SignInFailedState } from "../state/SignInFailedState.js"; +import { SignInCompletedState } from "../state/SignInCompletedState.js"; + +/* + * Result of a sign-in submit credential operation. + */ +export abstract class SignInSubmitCredentialResult< + TError extends AuthFlowErrorBase +> extends AuthFlowResultBase< + SignInSubmitCredentialResultState, + TError, + CustomAuthAccountData +> { + /** + * Creates a new instance of SignInSubmitCredentialResult. + * @param state The state of the result. + * @param resultData The result data. + */ + constructor( + state: SignInSubmitCredentialResultState, + resultData?: CustomAuthAccountData + ) { + super(state, resultData); + } +} + +/** + * The possible states of the SignInSubmitCredentialResult. + * This includes: + * - SignInCompletedState: The sign-in process has completed successfully. + * - SignInFailedState: The sign-in process has failed. + */ +export type SignInSubmitCredentialResultState = + | SignInCompletedState + | SignInFailedState; diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitPasswordResult.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitPasswordResult.ts new file mode 100644 index 0000000000..b653697aa0 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/result/SignInSubmitPasswordResult.ts @@ -0,0 +1,41 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { SignInSubmitPasswordError } from "../error_type/SignInError.js"; +import { SignInCompletedState } from "../state/SignInCompletedState.js"; +import { SignInFailedState } from "../state/SignInFailedState.js"; +import { SignInSubmitCredentialResult } from "./SignInSubmitCredentialResult.js"; + +/* + * Result of a sign-in submit password operation. + */ +export class SignInSubmitPasswordResult extends SignInSubmitCredentialResult { + static createWithError(error: unknown): SignInSubmitPasswordResult { + const result = new SignInSubmitPasswordResult(new SignInFailedState()); + result.error = new SignInSubmitPasswordError( + SignInSubmitPasswordResult.createErrorData(error) + ); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is SignInSubmitPasswordResult & { + state: SignInFailedState; + } { + return this.state instanceof SignInFailedState; + } + + /** + * Checks if the result is in a completed state. + */ + isCompleted(): this is SignInSubmitPasswordResult & { + state: SignInCompletedState; + } { + return this.state instanceof SignInCompletedState; + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInCodeRequiredState.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInCodeRequiredState.ts new file mode 100644 index 0000000000..8094b4f70b --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInCodeRequiredState.ts @@ -0,0 +1,141 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthAccountData } from "../../../get_account/auth_flow/CustomAuthAccountData.js"; +import { + SignInResendCodeParams, + SignInSubmitCodeParams, +} from "../../interaction_client/parameter/SignInParams.js"; +import { SignInResendCodeResult } from "../result/SignInResendCodeResult.js"; +import { SignInSubmitCodeResult } from "../result/SignInSubmitCodeResult.js"; +import { SignInCodeRequiredStateParameters } from "./SignInStateParameters.js"; +import { SignInState } from "./SignInState.js"; +import { SignInCompletedState } from "./SignInCompletedState.js"; + +/* + * Sign-in code required state. + */ +export class SignInCodeRequiredState extends SignInState { + /** + * Once user configures email one-time passcode as a authentication method in Microsoft Entra, a one-time passcode will be sent to the user’s email. + * Submit this one-time passcode to continue sign-in flow. + * @param {string} code - The code to submit. + * @returns {Promise} The result of the operation. + */ + async submitCode(code: string): Promise { + try { + this.ensureCodeIsValid(code, this.stateParameters.codeLength); + + const submitCodeParams: SignInSubmitCodeParams = { + clientId: this.stateParameters.config.auth.clientId, + correlationId: this.stateParameters.correlationId, + challengeType: + this.stateParameters.config.customAuth.challengeTypes ?? [], + scopes: this.stateParameters.scopes ?? [], + continuationToken: this.stateParameters.continuationToken ?? "", + code: code, + username: this.stateParameters.username, + }; + + this.stateParameters.logger.verbose( + "Submitting code for sign-in.", + this.stateParameters.correlationId + ); + + const completedResult = + await this.stateParameters.signInClient.submitCode( + submitCodeParams + ); + + this.stateParameters.logger.verbose( + "Code submitted for sign-in.", + this.stateParameters.correlationId + ); + + const accountInfo = new CustomAuthAccountData( + completedResult.authenticationResult.account, + this.stateParameters.config, + this.stateParameters.cacheClient, + this.stateParameters.logger, + this.stateParameters.correlationId + ); + + return new SignInSubmitCodeResult( + new SignInCompletedState(), + accountInfo + ); + } catch (error) { + this.stateParameters.logger.errorPii( + `Failed to submit code for sign-in. Error: ${error}.`, + this.stateParameters.correlationId + ); + + return SignInSubmitCodeResult.createWithError(error); + } + } + + /** + * Resends the another one-time passcode for sign-in flow if the previous one hasn't been verified. + * @returns {Promise} The result of the operation. + */ + async resendCode(): Promise { + try { + const submitCodeParams: SignInResendCodeParams = { + clientId: this.stateParameters.config.auth.clientId, + correlationId: this.stateParameters.correlationId, + challengeType: + this.stateParameters.config.customAuth.challengeTypes ?? [], + continuationToken: this.stateParameters.continuationToken ?? "", + username: this.stateParameters.username, + }; + + this.stateParameters.logger.verbose( + "Resending code for sign-in.", + this.stateParameters.correlationId + ); + + const result = await this.stateParameters.signInClient.resendCode( + submitCodeParams + ); + + this.stateParameters.logger.verbose( + "Code resent for sign-in.", + this.stateParameters.correlationId + ); + + return new SignInResendCodeResult( + new SignInCodeRequiredState({ + correlationId: result.correlationId, + continuationToken: result.continuationToken, + logger: this.stateParameters.logger, + config: this.stateParameters.config, + signInClient: this.stateParameters.signInClient, + cacheClient: this.stateParameters.cacheClient, + username: this.stateParameters.username, + codeLength: result.codeLength, + scopes: this.stateParameters.scopes, + }) + ); + } catch (error) { + return SignInResendCodeResult.createWithError(error); + } + } + + /** + * Gets the sent code length. + * @returns {number} The length of the code. + */ + getCodeLength(): number { + return this.stateParameters.codeLength; + } + + /** + * Gets the scopes to request. + * @returns {string[] | undefined} The scopes to request. + */ + getScopes(): string[] | undefined { + return this.stateParameters.scopes; + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInCompletedState.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInCompletedState.ts new file mode 100644 index 0000000000..313a6e4030 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInCompletedState.ts @@ -0,0 +1,12 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js"; + +/** + * Represents the completed state of the sign-in operation. + * This state indicates that the sign-in process has finished successfully. + */ +export class SignInCompletedState extends AuthFlowStateBase {} diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInContinuationState.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInContinuationState.ts new file mode 100644 index 0000000000..f7c069fe4e --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInContinuationState.ts @@ -0,0 +1,71 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthAccountData } from "../../../get_account/auth_flow/CustomAuthAccountData.js"; +import { SignInContinuationTokenParams } from "../../interaction_client/parameter/SignInParams.js"; +import { SignInResult } from "../result/SignInResult.js"; +import { SignInWithContinuationTokenInputs } from "../../../CustomAuthActionInputs.js"; +import { SignInContinuationStateParameters } from "./SignInStateParameters.js"; +import { SignInState } from "./SignInState.js"; +import { SignInCompletedState } from "./SignInCompletedState.js"; + +/* + * Sign-in continuation state. + */ +export class SignInContinuationState extends SignInState { + /** + * Initiates the sign-in flow with continuation token. + * @param {SignInWithContinuationTokenInputs} signInWithContinuationTokenInputs - The result of the operation. + * @returns {Promise} The result of the operation. + */ + async signIn( + signInWithContinuationTokenInputs?: SignInWithContinuationTokenInputs + ): Promise { + try { + const continuationTokenParams: SignInContinuationTokenParams = { + clientId: this.stateParameters.config.auth.clientId, + correlationId: this.stateParameters.correlationId, + challengeType: + this.stateParameters.config.customAuth.challengeTypes ?? [], + scopes: signInWithContinuationTokenInputs?.scopes ?? [], + continuationToken: this.stateParameters.continuationToken ?? "", + username: this.stateParameters.username, + signInScenario: this.stateParameters.signInScenario, + }; + + this.stateParameters.logger.verbose( + "Signing in with continuation token.", + this.stateParameters.correlationId + ); + + const completedResult = + await this.stateParameters.signInClient.signInWithContinuationToken( + continuationTokenParams + ); + + this.stateParameters.logger.verbose( + "Signed in with continuation token.", + this.stateParameters.correlationId + ); + + const accountInfo = new CustomAuthAccountData( + completedResult.authenticationResult.account, + this.stateParameters.config, + this.stateParameters.cacheClient, + this.stateParameters.logger, + this.stateParameters.correlationId + ); + + return new SignInResult(new SignInCompletedState(), accountInfo); + } catch (error) { + this.stateParameters.logger.errorPii( + `Failed to sign in with continuation token. Error: ${error}.`, + this.stateParameters.correlationId + ); + + return SignInResult.createWithError(error); + } + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInFailedState.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInFailedState.ts new file mode 100644 index 0000000000..e80641e575 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInFailedState.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js"; + +/** + * Represents the state of a sign-in operation that has been failed. + */ +export class SignInFailedState extends AuthFlowStateBase {} diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInPasswordRequiredState.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInPasswordRequiredState.ts new file mode 100644 index 0000000000..025e13174e --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInPasswordRequiredState.ts @@ -0,0 +1,83 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthAccountData } from "../../../get_account/auth_flow/CustomAuthAccountData.js"; +import { SignInSubmitPasswordParams } from "../../interaction_client/parameter/SignInParams.js"; +import { SignInSubmitPasswordResult } from "../result/SignInSubmitPasswordResult.js"; +import { SignInCompletedState } from "./SignInCompletedState.js"; +import { SignInState } from "./SignInState.js"; +import { SignInPasswordRequiredStateParameters } from "./SignInStateParameters.js"; + +/* + * Sign-in password required state. + */ +export class SignInPasswordRequiredState extends SignInState { + /** + * Once user configures email with password as a authentication method in Microsoft Entra, user submits a password to continue sign-in flow. + * @param {string} password - The password to submit. + * @returns {Promise} The result of the operation. + */ + async submitPassword( + password: string + ): Promise { + try { + this.ensurePasswordIsNotEmpty(password); + + const submitPasswordParams: SignInSubmitPasswordParams = { + clientId: this.stateParameters.config.auth.clientId, + correlationId: this.stateParameters.correlationId, + challengeType: + this.stateParameters.config.customAuth.challengeTypes ?? [], + scopes: this.stateParameters.scopes ?? [], + continuationToken: this.stateParameters.continuationToken ?? "", + password: password, + username: this.stateParameters.username, + }; + + this.stateParameters.logger.verbose( + "Submitting password for sign-in.", + this.stateParameters.correlationId + ); + + const completedResult = + await this.stateParameters.signInClient.submitPassword( + submitPasswordParams + ); + + this.stateParameters.logger.verbose( + "Password submitted for sign-in.", + this.stateParameters.correlationId + ); + + const accountInfo = new CustomAuthAccountData( + completedResult.authenticationResult.account, + this.stateParameters.config, + this.stateParameters.cacheClient, + this.stateParameters.logger, + this.stateParameters.correlationId + ); + + return new SignInSubmitPasswordResult( + new SignInCompletedState(), + accountInfo + ); + } catch (error) { + this.stateParameters.logger.errorPii( + `Failed to sign in after submitting password. Error: ${error}.`, + this.stateParameters.correlationId + ); + + return SignInSubmitPasswordResult.createWithError(error); + } + } + + /** + * Gets the scopes to request. + * @returns {string[] | undefined} The scopes to request. + */ + getScopes(): string[] | undefined { + return this.stateParameters.scopes; + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInState.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInState.ts new file mode 100644 index 0000000000..4e7f01cb53 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInState.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowActionRequiredStateBase } from "../../../core/auth_flow/AuthFlowState.js"; +import { ensureArgumentIsNotEmptyString } from "../../../core/utils/ArgumentValidator.js"; +import { SignInStateParameters } from "./SignInStateParameters.js"; + +/* + * Base state handler for sign-in flow. + */ +export abstract class SignInState< + TParameters extends SignInStateParameters +> extends AuthFlowActionRequiredStateBase { + /* + * Creates a new SignInState. + * @param stateParameters - The state parameters for sign-in. + */ + constructor(stateParameters: TParameters) { + super(stateParameters); + + ensureArgumentIsNotEmptyString( + "username", + stateParameters.username, + stateParameters.correlationId + ); + ensureArgumentIsNotEmptyString( + "continuationToken", + stateParameters.continuationToken, + stateParameters.correlationId + ); + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInStateParameters.ts b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInStateParameters.ts new file mode 100644 index 0000000000..61f802a9b2 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/auth_flow/state/SignInStateParameters.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowActionRequiredStateParameters } from "../../../core/auth_flow/AuthFlowState.js"; +import { CustomAuthSilentCacheClient } from "../../../get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { SignInClient } from "../../interaction_client/SignInClient.js"; +import { SignInScenarioType } from "../SignInScenario.js"; + +export interface SignInStateParameters + extends AuthFlowActionRequiredStateParameters { + username: string; + signInClient: SignInClient; + cacheClient: CustomAuthSilentCacheClient; +} + +export interface SignInPasswordRequiredStateParameters + extends SignInStateParameters { + scopes?: string[]; +} + +export interface SignInCodeRequiredStateParameters + extends SignInStateParameters { + codeLength: number; + scopes?: string[]; +} + +export interface SignInContinuationStateParameters + extends SignInStateParameters { + signInScenario: SignInScenarioType; +} diff --git a/lib/msal-browser/src/custom_auth/sign_in/interaction_client/SignInClient.ts b/lib/msal-browser/src/custom_auth/sign_in/interaction_client/SignInClient.ts new file mode 100644 index 0000000000..d5351850fb --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/interaction_client/SignInClient.ts @@ -0,0 +1,396 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + ChallengeType, + DefaultCustomAuthApiCodeLength, +} from "../../CustomAuthConstants.js"; +import { CustomAuthApiError } from "../../core/error/CustomAuthApiError.js"; +import * as CustomAuthApiErrorCode from "../../core/network_client/custom_auth_api/types/ApiErrorCodes.js"; + +import { CustomAuthInteractionClientBase } from "../../core/interaction_client/CustomAuthInteractionClientBase.js"; +import { + SignInStartParams, + SignInResendCodeParams, + SignInSubmitCodeParams, + SignInSubmitPasswordParams, + SignInContinuationTokenParams, +} from "./parameter/SignInParams.js"; +import { + createSignInCodeSendResult, + createSignInCompleteResult, + createSignInPasswordRequiredResult, + SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE, + SignInCodeSendResult, + SignInCompletedResult, + SignInPasswordRequiredResult, +} from "./result/SignInActionResult.js"; +import * as PublicApiId from "../../core/telemetry/PublicApiId.js"; +import { + SignInChallengeRequest, + SignInContinuationTokenRequest, + SignInInitiateRequest, + SignInOobTokenRequest, + SignInPasswordTokenRequest, +} from "../../core/network_client/custom_auth_api/types/ApiRequestTypes.js"; +import { SignInTokenResponse } from "../../core/network_client/custom_auth_api/types/ApiResponseTypes.js"; +import { + SignInScenario, + SignInScenarioType, +} from "../auth_flow/SignInScenario.js"; +import { UnexpectedError } from "../../core/error/UnexpectedError.js"; +import { ICustomAuthApiClient } from "../../core/network_client/custom_auth_api/ICustomAuthApiClient.js"; +import { CustomAuthAuthority } from "../../core/CustomAuthAuthority.js"; +import { + ICrypto, + IPerformanceClient, + Logger, + ResponseHandler, +} from "@azure/msal-common/browser"; +import { BrowserConfiguration } from "../../../config/Configuration.js"; +import { BrowserCacheManager } from "../../../cache/BrowserCacheManager.js"; +import { EventHandler } from "../../../event/EventHandler.js"; +import { INavigationClient } from "../../../navigation/INavigationClient.js"; +import { AuthenticationResult } from "../../../response/AuthenticationResult.js"; +import { ensureArgumentIsNotEmptyString } from "../../core/utils/ArgumentValidator.js"; + +export class SignInClient extends CustomAuthInteractionClientBase { + private readonly tokenResponseHandler: ResponseHandler; + + constructor( + config: BrowserConfiguration, + storageImpl: BrowserCacheManager, + browserCrypto: ICrypto, + logger: Logger, + eventHandler: EventHandler, + navigationClient: INavigationClient, + performanceClient: IPerformanceClient, + customAuthApiClient: ICustomAuthApiClient, + customAuthAuthority: CustomAuthAuthority + ) { + super( + config, + storageImpl, + browserCrypto, + logger, + eventHandler, + navigationClient, + performanceClient, + customAuthApiClient, + customAuthAuthority + ); + + this.tokenResponseHandler = new ResponseHandler( + this.config.auth.clientId, + this.browserStorage, + this.browserCrypto, + this.logger, + null, + null + ); + } + + /** + * Starts the signin flow. + * @param parameters The parameters required to start the sign-in flow. + * @returns The result of the sign-in start operation. + */ + async start( + parameters: SignInStartParams + ): Promise { + const apiId = !parameters.password + ? PublicApiId.SIGN_IN_WITH_CODE_START + : PublicApiId.SIGN_IN_WITH_PASSWORD_START; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + + this.logger.verbose( + "Calling initiate endpoint for sign in.", + parameters.correlationId + ); + + const initReq: SignInInitiateRequest = { + challenge_type: this.getChallengeTypes(parameters.challengeType), + username: parameters.username, + correlationId: parameters.correlationId, + telemetryManager: telemetryManager, + }; + + const initiateResponse = + await this.customAuthApiClient.signInApi.initiate(initReq); + + this.logger.verbose( + "Initiate endpoint called for sign in.", + parameters.correlationId + ); + + const challengeReq: SignInChallengeRequest = { + challenge_type: this.getChallengeTypes(parameters.challengeType), + continuation_token: initiateResponse.continuation_token ?? "", + correlationId: initiateResponse.correlation_id, + telemetryManager: telemetryManager, + }; + + return this.performChallengeRequest(challengeReq); + } + + /** + * Resends the code for sign-in flow. + * @param parameters The parameters required to resend the code. + * @returns The result of the sign-in resend code action. + */ + async resendCode( + parameters: SignInResendCodeParams + ): Promise { + const apiId = PublicApiId.SIGN_IN_RESEND_CODE; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + + const challengeReq: SignInChallengeRequest = { + challenge_type: this.getChallengeTypes(parameters.challengeType), + continuation_token: parameters.continuationToken ?? "", + correlationId: parameters.correlationId, + telemetryManager: telemetryManager, + }; + + const result = await this.performChallengeRequest(challengeReq); + + if (result.type === SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE) { + this.logger.error( + "Resend code operation failed due to the challenge type 'password' is not supported.", + parameters.correlationId + ); + + throw new CustomAuthApiError( + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + "Unsupported challenge type 'password'.", + result.correlationId + ); + } + + return result; + } + + /** + * Submits the code for sign-in flow. + * @param parameters The parameters required to submit the code. + * @returns The result of the sign-in submit code action. + */ + async submitCode( + parameters: SignInSubmitCodeParams + ): Promise { + ensureArgumentIsNotEmptyString( + "parameters.code", + parameters.code, + parameters.correlationId + ); + + const apiId = PublicApiId.SIGN_IN_SUBMIT_CODE; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + const scopes = this.getScopes(parameters.scopes); + + const request: SignInOobTokenRequest = { + continuation_token: parameters.continuationToken, + oob: parameters.code, + scope: scopes.join(" "), + correlationId: parameters.correlationId, + telemetryManager: telemetryManager, + }; + + return this.performTokenRequest( + () => + this.customAuthApiClient.signInApi.requestTokensWithOob( + request + ), + scopes + ); + } + + /** + * Submits the password for sign-in flow. + * @param parameters The parameters required to submit the password. + * @returns The result of the sign-in submit password action. + */ + async submitPassword( + parameters: SignInSubmitPasswordParams + ): Promise { + ensureArgumentIsNotEmptyString( + "parameters.password", + parameters.password, + parameters.correlationId + ); + + const apiId = PublicApiId.SIGN_IN_SUBMIT_PASSWORD; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + const scopes = this.getScopes(parameters.scopes); + + const request: SignInPasswordTokenRequest = { + continuation_token: parameters.continuationToken, + password: parameters.password, + scope: scopes.join(" "), + correlationId: parameters.correlationId, + telemetryManager: telemetryManager, + }; + + return this.performTokenRequest( + () => + this.customAuthApiClient.signInApi.requestTokensWithPassword( + request + ), + scopes + ); + } + + /** + * Signs in with continuation token. + * @param parameters The parameters required to sign in with continuation token. + * @returns The result of the sign-in complete action. + */ + async signInWithContinuationToken( + parameters: SignInContinuationTokenParams + ): Promise { + const apiId = this.getPublicApiIdBySignInScenario( + parameters.signInScenario, + parameters.correlationId + ); + const telemetryManager = this.initializeServerTelemetryManager(apiId); + const scopes = this.getScopes(parameters.scopes); + + // Create token request. + const request: SignInContinuationTokenRequest = { + continuation_token: parameters.continuationToken, + username: parameters.username, + correlationId: parameters.correlationId, + telemetryManager: telemetryManager, + scope: scopes.join(" "), + }; + + // Call token endpoint. + return this.performTokenRequest( + () => + this.customAuthApiClient.signInApi.requestTokenWithContinuationToken( + request + ), + scopes + ); + } + + private async performTokenRequest( + tokenEndpointCaller: () => Promise, + requestScopes: string[] + ): Promise { + this.logger.verbose( + "Calling token endpoint for sign in.", + this.correlationId + ); + + const requestTimestamp = Math.round(new Date().getTime() / 1000.0); + const tokenResponse = await tokenEndpointCaller(); + + this.logger.verbose( + "Token endpoint called for sign in.", + this.correlationId + ); + + // Save tokens and create authentication result. + const result = + await this.tokenResponseHandler.handleServerTokenResponse( + tokenResponse, + this.customAuthAuthority, + requestTimestamp, + { + authority: this.customAuthAuthority.canonicalAuthority, + correlationId: tokenResponse.correlation_id ?? "", + scopes: requestScopes, + storeInCache: { + idToken: true, + accessToken: true, + refreshToken: true, + }, + } + ); + + return createSignInCompleteResult({ + correlationId: tokenResponse.correlation_id ?? "", + authenticationResult: result as AuthenticationResult, + }); + } + + private async performChallengeRequest( + request: SignInChallengeRequest + ): Promise { + this.logger.verbose( + "Calling challenge endpoint for sign in.", + request.correlationId + ); + + const challengeResponse = + await this.customAuthApiClient.signInApi.requestChallenge(request); + + this.logger.verbose( + "Challenge endpoint called for sign in.", + request.correlationId + ); + + if (challengeResponse.challenge_type === ChallengeType.OOB) { + // Code is required + this.logger.verbose( + "Challenge type is oob for sign in.", + request.correlationId + ); + + return createSignInCodeSendResult({ + correlationId: challengeResponse.correlation_id, + continuationToken: challengeResponse.continuation_token ?? "", + challengeChannel: challengeResponse.challenge_channel ?? "", + challengeTargetLabel: + challengeResponse.challenge_target_label ?? "", + codeLength: + challengeResponse.code_length ?? + DefaultCustomAuthApiCodeLength, + bindingMethod: challengeResponse.binding_method ?? "", + }); + } + + if (challengeResponse.challenge_type === ChallengeType.PASSWORD) { + // Password is required + this.logger.verbose( + "Challenge type is password for sign in.", + request.correlationId + ); + + return createSignInPasswordRequiredResult({ + correlationId: challengeResponse.correlation_id, + continuationToken: challengeResponse.continuation_token ?? "", + }); + } + + this.logger.error( + `Unsupported challenge type '${challengeResponse.challenge_type}' for sign in.`, + request.correlationId + ); + + throw new CustomAuthApiError( + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + `Unsupported challenge type '${challengeResponse.challenge_type}'.`, + challengeResponse.correlation_id + ); + } + + private getPublicApiIdBySignInScenario( + scenario: SignInScenarioType, + correlationId: string + ): number { + switch (scenario) { + case SignInScenario.SignInAfterSignUp: + return PublicApiId.SIGN_IN_AFTER_SIGN_UP; + case SignInScenario.SignInAfterPasswordReset: + return PublicApiId.SIGN_IN_AFTER_PASSWORD_RESET; + default: + throw new UnexpectedError( + `Unsupported sign-in scenario '${scenario}'.`, + correlationId + ); + } + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_in/interaction_client/parameter/SignInParams.ts b/lib/msal-browser/src/custom_auth/sign_in/interaction_client/parameter/SignInParams.ts new file mode 100644 index 0000000000..9d83fee076 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/interaction_client/parameter/SignInParams.ts @@ -0,0 +1,39 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { SignInScenarioType } from "../../auth_flow/SignInScenario.js"; + +export interface SignInParamsBase { + clientId: string; + correlationId: string; + challengeType: Array; + username: string; +} + +export interface SignInResendCodeParams extends SignInParamsBase { + continuationToken: string; +} + +export interface SignInStartParams extends SignInParamsBase { + password?: string; +} + +export interface SignInSubmitCodeParams extends SignInParamsBase { + continuationToken: string; + code: string; + scopes: Array; +} + +export interface SignInSubmitPasswordParams extends SignInParamsBase { + continuationToken: string; + password: string; + scopes: Array; +} + +export interface SignInContinuationTokenParams extends SignInParamsBase { + continuationToken: string; + signInScenario: SignInScenarioType; + scopes: Array; +} diff --git a/lib/msal-browser/src/custom_auth/sign_in/interaction_client/result/SignInActionResult.ts b/lib/msal-browser/src/custom_auth/sign_in/interaction_client/result/SignInActionResult.ts new file mode 100644 index 0000000000..0446f6e85c --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_in/interaction_client/result/SignInActionResult.ts @@ -0,0 +1,65 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthenticationResult } from "../../../../response/AuthenticationResult.js"; + +interface SignInActionResult { + type: string; + correlationId: string; +} + +interface SignInContinuationTokenResult extends SignInActionResult { + continuationToken: string; +} + +export interface SignInCompletedResult extends SignInActionResult { + type: typeof SIGN_IN_COMPLETED_RESULT_TYPE; + authenticationResult: AuthenticationResult; +} + +export interface SignInPasswordRequiredResult + extends SignInContinuationTokenResult { + type: typeof SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE; +} + +export interface SignInCodeSendResult extends SignInContinuationTokenResult { + type: typeof SIGN_IN_CODE_SEND_RESULT_TYPE; + challengeChannel: string; + challengeTargetLabel: string; + codeLength: number; + bindingMethod: string; +} + +export const SIGN_IN_CODE_SEND_RESULT_TYPE = "SignInCodeSendResult"; +export const SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE = + "SignInPasswordRequiredResult"; +export const SIGN_IN_COMPLETED_RESULT_TYPE = "SignInCompletedResult"; + +export function createSignInCompleteResult( + input: Omit +): SignInCompletedResult { + return { + type: SIGN_IN_COMPLETED_RESULT_TYPE, + ...input, + }; +} + +export function createSignInPasswordRequiredResult( + input: Omit +): SignInPasswordRequiredResult { + return { + type: SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE, + ...input, + }; +} + +export function createSignInCodeSendResult( + input: Omit +): SignInCodeSendResult { + return { + type: SIGN_IN_CODE_SEND_RESULT_TYPE, + ...input, + }; +} diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/error_type/SignUpError.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/error_type/SignUpError.ts new file mode 100644 index 0000000000..01edb064fa --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/error_type/SignUpError.ts @@ -0,0 +1,138 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthActionErrorBase } from "../../../core/auth_flow/AuthFlowErrorBase.js"; + +export class SignUpError extends AuthActionErrorBase { + /** + * Checks if the error is due to the user already exists. + * @returns {boolean} True if the error is due to the user already exists, false otherwise. + */ + isUserAlreadyExists(): boolean { + return this.isUserAlreadyExistsError(); + } + + /** + * Checks if the error is due to the username is invalid. + * @returns {boolean} True if the error is due to the user is invalid, false otherwise. + */ + isInvalidUsername(): boolean { + return this.isUserInvalidError(); + } + + /** + * Checks if the error is due to the password being invalid or incorrect. + * @returns {boolean} True if the error is due to the password being invalid, false otherwise. + */ + isInvalidPassword(): boolean { + return this.isInvalidNewPasswordError(); + } + + /** + * Checks if the error is due to the required attributes are missing. + * @returns {boolean} True if the error is due to the required attributes are missing, false otherwise. + */ + isMissingRequiredAttributes(): boolean { + return this.isAttributeRequiredError(); + } + + /** + * Checks if the error is due to the attributes validation failed. + * @returns {boolean} True if the error is due to the attributes validation failed, false otherwise. + */ + isAttributesValidationFailed(): boolean { + return this.isAttributeValidationFailedError(); + } + + /** + * Checks if the error is due to the provided challenge type is not supported. + * @returns {boolean} True if the error is due to the provided challenge type is not supported, false otherwise. + */ + isUnsupportedChallengeType(): boolean { + return this.isUnsupportedChallengeTypeError(); + } + + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if "loginPopup" function is required to continue sthe operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } +} + +export class SignUpSubmitPasswordError extends AuthActionErrorBase { + /** + * Checks if the error is due to the password being invalid or incorrect. + * @returns {boolean} True if the error is due to the password being invalid, false otherwise. + */ + isInvalidPassword(): boolean { + return ( + this.isPasswordIncorrectError() || this.isInvalidNewPasswordError() + ); + } + + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if "loginPopup" function is required to continue sthe operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } +} + +export class SignUpSubmitCodeError extends AuthActionErrorBase { + /** + * Checks if the provided code is invalid. + * @returns {boolean} True if the provided code is invalid, false otherwise. + */ + isInvalidCode(): boolean { + return this.isInvalidCodeError(); + } + + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if "loginPopup" function is required to continue sthe operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } +} + +export class SignUpSubmitAttributesError extends AuthActionErrorBase { + /** + * Checks if the error is due to the required attributes are missing. + * @returns {boolean} True if the error is due to the required attributes are missing, false otherwise. + */ + isMissingRequiredAttributes(): boolean { + return this.isAttributeRequiredError(); + } + + /** + * Checks if the error is due to the attributes validation failed. + * @returns {boolean} True if the error is due to the attributes validation failed, false otherwise. + */ + isAttributesValidationFailed(): boolean { + return this.isAttributeValidationFailedError(); + } + + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if "loginPopup" function is required to continue sthe operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } +} + +export class SignUpResendCodeError extends AuthActionErrorBase { + /** + * Check if client app supports the challenge type configured in Entra. + * @returns {boolean} True if "loginPopup" function is required to continue sthe operation. + */ + isRedirectRequired(): boolean { + return this.isRedirectError(); + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpResendCodeResult.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpResendCodeResult.ts new file mode 100644 index 0000000000..4864403dba --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpResendCodeResult.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { SignUpResendCodeError } from "../error_type/SignUpError.js"; +import type { SignUpCodeRequiredState } from "../state/SignUpCodeRequiredState.js"; +import { SignUpFailedState } from "../state/SignUpFailedState.js"; + +/* + * Result of resending code in a sign-up operation. + */ +export class SignUpResendCodeResult extends AuthFlowResultBase< + SignUpResendCodeResultState, + SignUpResendCodeError, + void +> { + /** + * Creates a new instance of SignUpResendCodeResult. + * @param state The state of the result. + */ + constructor(state: SignUpResendCodeResultState) { + super(state); + } + + /** + * Creates a new instance of SignUpResendCodeResult with an error. + * @param error The error that occurred. + * @returns {SignUpResendCodeResult} A new instance of SignUpResendCodeResult with the error set. + */ + static createWithError(error: unknown): SignUpResendCodeResult { + const result = new SignUpResendCodeResult(new SignUpFailedState()); + result.error = new SignUpResendCodeError( + SignUpResendCodeResult.createErrorData(error) + ); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is SignUpResendCodeResult & { state: SignUpFailedState } { + return this.state instanceof SignUpFailedState; + } + + /** + * Checks if the result is in a code required state. + */ + isCodeRequired(): this is SignUpResendCodeResult & { + state: SignUpCodeRequiredState; + } { + /* + * The instanceof operator couldn't be used here to check the state type since the circular dependency issue. + * So we are using the constructor name to check the state type. + */ + return this.state.constructor?.name === "SignUpCodeRequiredState"; + } +} + +/** + * The possible states for the SignUpResendCodeResult. + * This includes: + * - SignUpCodeRequiredState: The sign-up process requires a code. + * - SignUpFailedState: The sign-up process has failed. + */ +export type SignUpResendCodeResultState = + | SignUpCodeRequiredState + | SignUpFailedState; diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpResult.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpResult.ts new file mode 100644 index 0000000000..98ba06cb0b --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpResult.ts @@ -0,0 +1,88 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { SignUpError } from "../error_type/SignUpError.js"; +import { SignUpAttributesRequiredState } from "../state/SignUpAttributesRequiredState.js"; +import { SignUpCodeRequiredState } from "../state/SignUpCodeRequiredState.js"; +import { SignUpFailedState } from "../state/SignUpFailedState.js"; +import { SignUpPasswordRequiredState } from "../state/SignUpPasswordRequiredState.js"; + +/* + * Result of a sign-up operation. + */ +export class SignUpResult extends AuthFlowResultBase< + SignUpResultState, + SignUpError, + void +> { + /** + * Creates a new instance of SignUpResult. + * @param state The state of the result. + */ + constructor(state: SignUpResultState) { + super(state); + } + + /** + * Creates a new instance of SignUpResult with an error. + * @param error The error that occurred. + * @returns {SignUpResult} A new instance of SignUpResult with the error set. + */ + static createWithError(error: unknown): SignUpResult { + const result = new SignUpResult(new SignUpFailedState()); + result.error = new SignUpError(SignUpResult.createErrorData(error)); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is SignUpResult & { state: SignUpFailedState } { + return this.state instanceof SignUpFailedState; + } + + /** + * Checks if the result is in a code required state. + */ + isCodeRequired(): this is SignUpResult & { + state: SignUpCodeRequiredState; + } { + return this.state instanceof SignUpCodeRequiredState; + } + + /** + * Checks if the result is in a password required state. + */ + isPasswordRequired(): this is SignUpResult & { + state: SignUpPasswordRequiredState; + } { + return this.state instanceof SignUpPasswordRequiredState; + } + + /** + * Checks if the result is in an attributes required state. + */ + isAttributesRequired(): this is SignUpResult & { + state: SignUpAttributesRequiredState; + } { + return this.state instanceof SignUpAttributesRequiredState; + } +} + +/** + * The possible states for the SignUpResult. + * This includes: + * - SignUpCodeRequiredState: The sign-up process requires a code. + * - SignUpPasswordRequiredState: The sign-up process requires a password. + * - SignUpAttributesRequiredState: The sign-up process requires additional attributes. + * - SignUpFailedState: The sign-up process has failed. + */ +export type SignUpResultState = + | SignUpCodeRequiredState + | SignUpPasswordRequiredState + | SignUpAttributesRequiredState + | SignUpFailedState; diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitAttributesResult.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitAttributesResult.ts new file mode 100644 index 0000000000..b40322ac3a --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitAttributesResult.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { SignUpSubmitAttributesError } from "../error_type/SignUpError.js"; +import { SignUpCompletedState } from "../state/SignUpCompletedState.js"; +import { SignUpFailedState } from "../state/SignUpFailedState.js"; + +/* + * Result of a sign-up operation that requires attributes. + */ +export class SignUpSubmitAttributesResult extends AuthFlowResultBase< + SignUpSubmitAttributesResultState, + SignUpSubmitAttributesError, + void +> { + /** + * Creates a new instance of SignUpSubmitAttributesResult. + * @param state The state of the result. + */ + constructor(state: SignUpSubmitAttributesResultState) { + super(state); + } + + /** + * Creates a new instance of SignUpSubmitAttributesResult with an error. + * @param error The error that occurred. + * @returns {SignUpSubmitAttributesResult} A new instance of SignUpSubmitAttributesResult with the error set. + */ + static createWithError(error: unknown): SignUpSubmitAttributesResult { + const result = new SignUpSubmitAttributesResult( + new SignUpFailedState() + ); + result.error = new SignUpSubmitAttributesError( + SignUpSubmitAttributesResult.createErrorData(error) + ); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is SignUpSubmitAttributesResult & { + state: SignUpFailedState; + } { + return this.state instanceof SignUpFailedState; + } + + /** + * Checks if the result is in a completed state. + */ + isCompleted(): this is SignUpSubmitAttributesResult & { + state: SignUpCompletedState; + } { + return this.state instanceof SignUpCompletedState; + } +} + +/** + * The possible states for the SignUpSubmitAttributesResult. + * This includes: + * - SignUpCompletedState: The sign-up process has completed successfully. + * - SignUpFailedState: The sign-up process has failed. + */ +export type SignUpSubmitAttributesResultState = + | SignUpCompletedState + | SignUpFailedState; diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitCodeResult.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitCodeResult.ts new file mode 100644 index 0000000000..048cb84cbd --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitCodeResult.ts @@ -0,0 +1,90 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { SignUpSubmitCodeError } from "../error_type/SignUpError.js"; +import { SignUpAttributesRequiredState } from "../state/SignUpAttributesRequiredState.js"; +import { SignUpPasswordRequiredState } from "../state/SignUpPasswordRequiredState.js"; +import { SignUpCompletedState } from "../state/SignUpCompletedState.js"; +import { SignUpFailedState } from "../state/SignUpFailedState.js"; + +/* + * Result of a sign-up operation that requires a code. + */ +export class SignUpSubmitCodeResult extends AuthFlowResultBase< + SignUpSubmitCodeResultState, + SignUpSubmitCodeError, + void +> { + /** + * Creates a new instance of SignUpSubmitCodeResult. + * @param state The state of the result. + */ + constructor(state: SignUpSubmitCodeResultState) { + super(state); + } + + /** + * Creates a new instance of SignUpSubmitCodeResult with an error. + * @param error The error that occurred. + * @returns {SignUpSubmitCodeResult} A new instance of SignUpSubmitCodeResult with the error set. + */ + static createWithError(error: unknown): SignUpSubmitCodeResult { + const result = new SignUpSubmitCodeResult(new SignUpFailedState()); + result.error = new SignUpSubmitCodeError( + SignUpSubmitCodeResult.createErrorData(error) + ); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is SignUpSubmitCodeResult & { state: SignUpFailedState } { + return this.state instanceof SignUpFailedState; + } + + /** + * Checks if the result is in a password required state. + */ + isPasswordRequired(): this is SignUpSubmitCodeResult & { + state: SignUpPasswordRequiredState; + } { + return this.state instanceof SignUpPasswordRequiredState; + } + + /** + * Checks if the result is in an attributes required state. + */ + isAttributesRequired(): this is SignUpSubmitCodeResult & { + state: SignUpAttributesRequiredState; + } { + return this.state instanceof SignUpAttributesRequiredState; + } + + /** + * Checks if the result is in a completed state. + */ + isCompleted(): this is SignUpSubmitCodeResult & { + state: SignUpCompletedState; + } { + return this.state instanceof SignUpCompletedState; + } +} + +/** + * The possible states for the SignUpSubmitCodeResult. + * This includes: + * - SignUpPasswordRequiredState: The sign-up process requires a password. + * - SignUpAttributesRequiredState: The sign-up process requires additional attributes. + * - SignUpCompletedState: The sign-up process has completed successfully. + * - SignUpFailedState: The sign-up process has failed. + */ +export type SignUpSubmitCodeResultState = + | SignUpPasswordRequiredState + | SignUpAttributesRequiredState + | SignUpCompletedState + | SignUpFailedState; diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitPasswordResult.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitPasswordResult.ts new file mode 100644 index 0000000000..eed94e482f --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/result/SignUpSubmitPasswordResult.ts @@ -0,0 +1,80 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowResultBase } from "../../../core/auth_flow/AuthFlowResultBase.js"; +import { SignUpSubmitPasswordError } from "../error_type/SignUpError.js"; +import { SignUpAttributesRequiredState } from "../state/SignUpAttributesRequiredState.js"; +import { SignUpCompletedState } from "../state/SignUpCompletedState.js"; +import { SignUpFailedState } from "../state/SignUpFailedState.js"; + +/* + * Result of a sign-up operation that requires a password. + */ +export class SignUpSubmitPasswordResult extends AuthFlowResultBase< + SignUpSubmitPasswordResultState, + SignUpSubmitPasswordError, + void +> { + /** + * Creates a new instance of SignUpSubmitPasswordResult. + * @param state The state of the result. + */ + constructor(state: SignUpSubmitPasswordResultState) { + super(state); + } + + /** + * Creates a new instance of SignUpSubmitPasswordResult with an error. + * @param error The error that occurred. + * @returns {SignUpSubmitPasswordResult} A new instance of SignUpSubmitPasswordResult with the error set. + */ + static createWithError(error: unknown): SignUpSubmitPasswordResult { + const result = new SignUpSubmitPasswordResult(new SignUpFailedState()); + result.error = new SignUpSubmitPasswordError( + SignUpSubmitPasswordResult.createErrorData(error) + ); + + return result; + } + + /** + * Checks if the result is in a failed state. + */ + isFailed(): this is SignUpSubmitPasswordResult & { + state: SignUpFailedState; + } { + return this.state instanceof SignUpFailedState; + } + + /** + * Checks if the result is in an attributes required state. + */ + isAttributesRequired(): this is SignUpSubmitPasswordResult & { + state: SignUpAttributesRequiredState; + } { + return this.state instanceof SignUpAttributesRequiredState; + } + + /** + * Checks if the result is in a completed state. + */ + isCompleted(): this is SignUpSubmitPasswordResult & { + state: SignUpCompletedState; + } { + return this.state instanceof SignUpCompletedState; + } +} + +/** + * The possible states for the SignUpSubmitPasswordResult. + * This includes: + * - SignUpAttributesRequiredState: The sign-up process requires additional attributes. + * - SignUpCompletedState: The sign-up process has completed successfully. + * - SignUpFailedState: The sign-up process has failed. + */ +export type SignUpSubmitPasswordResultState = + | SignUpAttributesRequiredState + | SignUpCompletedState + | SignUpFailedState; diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpAttributesRequiredState.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpAttributesRequiredState.ts new file mode 100644 index 0000000000..a084cf6c0e --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpAttributesRequiredState.ts @@ -0,0 +1,115 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { InvalidArgumentError } from "../../../core/error/InvalidArgumentError.js"; +import { UnexpectedError } from "../../../core/error/UnexpectedError.js"; +import { UserAccountAttributes } from "../../../UserAccountAttributes.js"; +import { SIGN_UP_COMPLETED_RESULT_TYPE } from "../../interaction_client/result/SignUpActionResult.js"; +import { SignUpSubmitAttributesResult } from "../result/SignUpSubmitAttributesResult.js"; +import { SignUpState } from "./SignUpState.js"; +import { SignUpAttributesRequiredStateParameters } from "./SignUpStateParameters.js"; +import { UserAttribute } from "../../../core/network_client/custom_auth_api/types/ApiErrorResponseTypes.js"; +import { SignUpCompletedState } from "./SignUpCompletedState.js"; +import { SignInScenario } from "../../../sign_in/auth_flow/SignInScenario.js"; + +/* + * Sign-up attributes required state. + */ +export class SignUpAttributesRequiredState extends SignUpState { + /** + * Submits attributes to continue sign-up flow. + * This methods is used to submit required attributes. + * These attributes, built in or custom, were configured in the Microsoft Entra admin center by the tenant administrator. + * @param {UserAccountAttributes} attributes - The attributes to submit. + * @returns {Promise} The result of the operation. + */ + async submitAttributes( + attributes: UserAccountAttributes + ): Promise { + if (!attributes || Object.keys(attributes).length === 0) { + this.stateParameters.logger.error( + "Attributes are required for sign-up.", + this.stateParameters.correlationId + ); + + return Promise.resolve( + SignUpSubmitAttributesResult.createWithError( + new InvalidArgumentError( + "attributes", + this.stateParameters.correlationId + ) + ) + ); + } + + try { + this.stateParameters.logger.verbose( + "Submitting attributes for sign-up.", + this.stateParameters.correlationId + ); + + const result = + await this.stateParameters.signUpClient.submitAttributes({ + clientId: this.stateParameters.config.auth.clientId, + correlationId: this.stateParameters.correlationId, + challengeType: + this.stateParameters.config.customAuth.challengeTypes ?? + [], + continuationToken: + this.stateParameters.continuationToken ?? "", + attributes: attributes, + username: this.stateParameters.username, + }); + + this.stateParameters.logger.verbose( + "Attributes submitted for sign-up.", + this.stateParameters.correlationId + ); + + if (result.type === SIGN_UP_COMPLETED_RESULT_TYPE) { + // Sign-up completed + this.stateParameters.logger.verbose( + "Sign-up completed.", + this.stateParameters.correlationId + ); + + return new SignUpSubmitAttributesResult( + new SignUpCompletedState({ + correlationId: result.correlationId, + continuationToken: result.continuationToken, + logger: this.stateParameters.logger, + config: this.stateParameters.config, + signInClient: this.stateParameters.signInClient, + cacheClient: this.stateParameters.cacheClient, + username: this.stateParameters.username, + signInScenario: SignInScenario.SignInAfterSignUp, + }) + ); + } + + return SignUpSubmitAttributesResult.createWithError( + new UnexpectedError( + "Unknown sign-up result type.", + this.stateParameters.correlationId + ) + ); + } catch (error) { + this.stateParameters.logger.errorPii( + `Failed to submit attributes for sign up. Error: ${error}.`, + this.stateParameters.correlationId + ); + + return SignUpSubmitAttributesResult.createWithError(error); + } + } + + /** + * Gets the required attributes for sign-up. + * @returns {UserAttribute[]} The required attributes for sign-up. + */ + getRequiredAttributes(): UserAttribute[] { + return this.stateParameters.requiredAttributes; + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpCodeRequiredState.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpCodeRequiredState.ts new file mode 100644 index 0000000000..a20e04a798 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpCodeRequiredState.ts @@ -0,0 +1,196 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { UnexpectedError } from "../../../core/error/UnexpectedError.js"; +import { + SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE, + SIGN_UP_COMPLETED_RESULT_TYPE, + SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE, +} from "../../interaction_client/result/SignUpActionResult.js"; +import { SignUpResendCodeResult } from "../result/SignUpResendCodeResult.js"; +import { SignUpSubmitCodeResult } from "../result/SignUpSubmitCodeResult.js"; +import { SignUpState } from "./SignUpState.js"; +import { SignUpCodeRequiredStateParameters } from "./SignUpStateParameters.js"; +import { SignUpPasswordRequiredState } from "./SignUpPasswordRequiredState.js"; +import { SignUpAttributesRequiredState } from "./SignUpAttributesRequiredState.js"; +import { SignUpCompletedState } from "./SignUpCompletedState.js"; +import { SignInScenario } from "../../../sign_in/auth_flow/SignInScenario.js"; + +/* + * Sign-up code required state. + */ +export class SignUpCodeRequiredState extends SignUpState { + /** + * Submit one-time passcode to continue sign-up flow. + * @param {string} code - The code to submit. + * @returns {Promise} The result of the operation. + */ + async submitCode(code: string): Promise { + try { + this.ensureCodeIsValid(code, this.stateParameters.codeLength); + + this.stateParameters.logger.verbose( + "Submitting code for sign-up.", + this.stateParameters.correlationId + ); + + const result = await this.stateParameters.signUpClient.submitCode({ + clientId: this.stateParameters.config.auth.clientId, + correlationId: this.stateParameters.correlationId, + challengeType: + this.stateParameters.config.customAuth.challengeTypes ?? [], + continuationToken: this.stateParameters.continuationToken ?? "", + code: code, + username: this.stateParameters.username, + }); + + this.stateParameters.logger.verbose( + "Code submitted for sign-up.", + this.stateParameters.correlationId + ); + + if (result.type === SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE) { + // Password required + this.stateParameters.logger.verbose( + "Password required for sign-up.", + this.stateParameters.correlationId + ); + + return new SignUpSubmitCodeResult( + new SignUpPasswordRequiredState({ + correlationId: result.correlationId, + continuationToken: result.continuationToken, + logger: this.stateParameters.logger, + config: this.stateParameters.config, + signInClient: this.stateParameters.signInClient, + signUpClient: this.stateParameters.signUpClient, + cacheClient: this.stateParameters.cacheClient, + username: this.stateParameters.username, + }) + ); + } else if ( + result.type === SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE + ) { + // Attributes required + this.stateParameters.logger.verbose( + "Attributes required for sign-up.", + this.stateParameters.correlationId + ); + + return new SignUpSubmitCodeResult( + new SignUpAttributesRequiredState({ + correlationId: result.correlationId, + continuationToken: result.continuationToken, + logger: this.stateParameters.logger, + config: this.stateParameters.config, + signInClient: this.stateParameters.signInClient, + signUpClient: this.stateParameters.signUpClient, + cacheClient: this.stateParameters.cacheClient, + username: this.stateParameters.username, + requiredAttributes: result.requiredAttributes, + }) + ); + } else if (result.type === SIGN_UP_COMPLETED_RESULT_TYPE) { + // Sign-up completed + this.stateParameters.logger.verbose( + "Sign-up completed.", + this.stateParameters.correlationId + ); + + return new SignUpSubmitCodeResult( + new SignUpCompletedState({ + correlationId: result.correlationId, + continuationToken: result.continuationToken, + logger: this.stateParameters.logger, + config: this.stateParameters.config, + signInClient: this.stateParameters.signInClient, + cacheClient: this.stateParameters.cacheClient, + username: this.stateParameters.username, + signInScenario: SignInScenario.SignInAfterSignUp, + }) + ); + } + + return SignUpSubmitCodeResult.createWithError( + new UnexpectedError( + "Unknown sign-up result type.", + this.stateParameters.correlationId + ) + ); + } catch (error) { + this.stateParameters.logger.errorPii( + `Failed to submit code for sign up. Error: ${error}.`, + this.stateParameters.correlationId + ); + + return SignUpSubmitCodeResult.createWithError(error); + } + } + + /** + * Resends the another one-time passcode for sign-up flow if the previous one hasn't been verified. + * @returns {Promise} The result of the operation. + */ + async resendCode(): Promise { + try { + this.stateParameters.logger.verbose( + "Resending code for sign-up.", + this.stateParameters.correlationId + ); + + const result = await this.stateParameters.signUpClient.resendCode({ + clientId: this.stateParameters.config.auth.clientId, + challengeType: + this.stateParameters.config.customAuth.challengeTypes ?? [], + username: this.stateParameters.username, + correlationId: this.stateParameters.correlationId, + continuationToken: this.stateParameters.continuationToken ?? "", + }); + + this.stateParameters.logger.verbose( + "Code resent for sign-up.", + this.stateParameters.correlationId + ); + + return new SignUpResendCodeResult( + new SignUpCodeRequiredState({ + correlationId: result.correlationId, + continuationToken: result.continuationToken, + logger: this.stateParameters.logger, + config: this.stateParameters.config, + signInClient: this.stateParameters.signInClient, + signUpClient: this.stateParameters.signUpClient, + cacheClient: this.stateParameters.cacheClient, + username: this.stateParameters.username, + codeLength: result.codeLength, + codeResendInterval: result.interval, + }) + ); + } catch (error) { + this.stateParameters.logger.errorPii( + `Failed to resend code for sign up. Error: ${error}.`, + this.stateParameters.correlationId + ); + + return SignUpResendCodeResult.createWithError(error); + } + } + + /** + * Gets the sent code length. + * @returns {number} The length of the code. + */ + getCodeLength(): number { + return this.stateParameters.codeLength; + } + + /** + * Gets the interval in seconds for the code to be resent. + * @returns {number} The interval in seconds for the code to be resent. + */ + getCodeResendInterval(): number { + return this.stateParameters.codeResendInterval; + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpCompletedState.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpCompletedState.ts new file mode 100644 index 0000000000..4526ae5724 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpCompletedState.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { SignInContinuationState } from "../../../sign_in/auth_flow/state/SignInContinuationState.js"; + +/** + * Represents the state of a sign-up operation that has been completed scuccessfully. + */ +export class SignUpCompletedState extends SignInContinuationState {} diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpFailedState.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpFailedState.ts new file mode 100644 index 0000000000..c3b631308a --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpFailedState.ts @@ -0,0 +1,11 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowStateBase } from "../../../core/auth_flow/AuthFlowState.js"; + +/** + * Represents the state of a sign-up operation that has failed. + */ +export class SignUpFailedState extends AuthFlowStateBase {} diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpPasswordRequiredState.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpPasswordRequiredState.ts new file mode 100644 index 0000000000..290d3c86e0 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpPasswordRequiredState.ts @@ -0,0 +1,112 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { UnexpectedError } from "../../../core/error/UnexpectedError.js"; +import { SignInScenario } from "../../../sign_in/auth_flow/SignInScenario.js"; +import { + SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE, + SIGN_UP_COMPLETED_RESULT_TYPE, +} from "../../interaction_client/result/SignUpActionResult.js"; +import { SignUpSubmitPasswordResult } from "../result/SignUpSubmitPasswordResult.js"; +import { SignUpAttributesRequiredState } from "./SignUpAttributesRequiredState.js"; +import { SignUpCompletedState } from "./SignUpCompletedState.js"; +import { SignUpState } from "./SignUpState.js"; +import { SignUpPasswordRequiredStateParameters } from "./SignUpStateParameters.js"; + +/* + * Sign-up password required state. + */ +export class SignUpPasswordRequiredState extends SignUpState { + /** + * Submits a password for sign-up. + * @param {string} password - The password to submit. + * @returns {Promise} The result of the operation. + */ + async submitPassword( + password: string + ): Promise { + try { + this.ensurePasswordIsNotEmpty(password); + + this.stateParameters.logger.verbose( + "Submitting password for sign-up.", + this.stateParameters.correlationId + ); + + const result = + await this.stateParameters.signUpClient.submitPassword({ + clientId: this.stateParameters.config.auth.clientId, + correlationId: this.stateParameters.correlationId, + challengeType: + this.stateParameters.config.customAuth.challengeTypes ?? + [], + continuationToken: + this.stateParameters.continuationToken ?? "", + password: password, + username: this.stateParameters.username, + }); + + this.stateParameters.logger.verbose( + "Password submitted for sign-up.", + this.stateParameters.correlationId + ); + + if (result.type === SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE) { + // Attributes required + this.stateParameters.logger.verbose( + "Attributes required for sign-up.", + this.stateParameters.correlationId + ); + + return new SignUpSubmitPasswordResult( + new SignUpAttributesRequiredState({ + correlationId: result.correlationId, + continuationToken: result.continuationToken, + logger: this.stateParameters.logger, + config: this.stateParameters.config, + signInClient: this.stateParameters.signInClient, + signUpClient: this.stateParameters.signUpClient, + cacheClient: this.stateParameters.cacheClient, + username: this.stateParameters.username, + requiredAttributes: result.requiredAttributes, + }) + ); + } else if (result.type === SIGN_UP_COMPLETED_RESULT_TYPE) { + // Sign-up completed + this.stateParameters.logger.verbose( + "Sign-up completed.", + this.stateParameters.correlationId + ); + + return new SignUpSubmitPasswordResult( + new SignUpCompletedState({ + correlationId: result.correlationId, + continuationToken: result.continuationToken, + logger: this.stateParameters.logger, + config: this.stateParameters.config, + signInClient: this.stateParameters.signInClient, + cacheClient: this.stateParameters.cacheClient, + username: this.stateParameters.username, + signInScenario: SignInScenario.SignInAfterSignUp, + }) + ); + } + + return SignUpSubmitPasswordResult.createWithError( + new UnexpectedError( + "Unknown sign-up result type.", + this.stateParameters.correlationId + ) + ); + } catch (error) { + this.stateParameters.logger.errorPii( + `Failed to submit password for sign up. Error: ${error}.`, + this.stateParameters.correlationId + ); + + return SignUpSubmitPasswordResult.createWithError(error); + } + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpState.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpState.ts new file mode 100644 index 0000000000..c130fbb585 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpState.ts @@ -0,0 +1,34 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { AuthFlowActionRequiredStateBase } from "../../../core/auth_flow/AuthFlowState.js"; +import { ensureArgumentIsNotEmptyString } from "../../../core/utils/ArgumentValidator.js"; +import { SignUpStateParameters } from "./SignUpStateParameters.js"; + +/* + * Base state handler for sign-up flow. + */ +export abstract class SignUpState< + TParameters extends SignUpStateParameters +> extends AuthFlowActionRequiredStateBase { + /* + * Creates a new SignUpState. + * @param stateParameters - The state parameters for sign-up. + */ + constructor(stateParameters: TParameters) { + super(stateParameters); + + ensureArgumentIsNotEmptyString( + "username", + stateParameters.username, + stateParameters.correlationId + ); + ensureArgumentIsNotEmptyString( + "continuationToken", + stateParameters.continuationToken, + stateParameters.correlationId + ); + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpStateParameters.ts b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpStateParameters.ts new file mode 100644 index 0000000000..c1df011282 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/auth_flow/state/SignUpStateParameters.ts @@ -0,0 +1,31 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { SignUpClient } from "../../interaction_client/SignUpClient.js"; +import { SignInClient } from "../../../sign_in/interaction_client/SignInClient.js"; +import { CustomAuthSilentCacheClient } from "../../../get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { AuthFlowActionRequiredStateParameters } from "../../../core/auth_flow/AuthFlowState.js"; +import { UserAttribute } from "../../../core/network_client/custom_auth_api/types/ApiErrorResponseTypes.js"; + +export interface SignUpStateParameters + extends AuthFlowActionRequiredStateParameters { + username: string; + signUpClient: SignUpClient; + signInClient: SignInClient; + cacheClient: CustomAuthSilentCacheClient; +} + +export type SignUpPasswordRequiredStateParameters = SignUpStateParameters; + +export interface SignUpCodeRequiredStateParameters + extends SignUpStateParameters { + codeLength: number; + codeResendInterval: number; +} + +export interface SignUpAttributesRequiredStateParameters + extends SignUpStateParameters { + requiredAttributes: Array; +} diff --git a/lib/msal-browser/src/custom_auth/sign_up/interaction_client/SignUpClient.ts b/lib/msal-browser/src/custom_auth/sign_up/interaction_client/SignUpClient.ts new file mode 100644 index 0000000000..4ab31d039c --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/interaction_client/SignUpClient.ts @@ -0,0 +1,496 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthApiError } from "../../core/error/CustomAuthApiError.js"; +import * as CustomAuthApiErrorCode from "../../core/network_client/custom_auth_api/types/ApiErrorCodes.js"; +import { UnexpectedError } from "../../core/error/UnexpectedError.js"; +import { CustomAuthInteractionClientBase } from "../../core/interaction_client/CustomAuthInteractionClientBase.js"; +import * as PublicApiId from "../../core/telemetry/PublicApiId.js"; +import { + ChallengeType, + DefaultCustomAuthApiCodeLength, + DefaultCustomAuthApiCodeResendIntervalInSec, +} from "../../CustomAuthConstants.js"; +import { + SignUpParamsBase, + SignUpResendCodeParams, + SignUpStartParams, + SignUpSubmitCodeParams, + SignUpSubmitPasswordParams, + SignUpSubmitUserAttributesParams, +} from "./parameter/SignUpParams.js"; +import { + createSignUpAttributesRequiredResult, + createSignUpCodeRequiredResult, + createSignUpCompletedResult, + createSignUpPasswordRequiredResult, + SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE, + SIGN_UP_CODE_REQUIRED_RESULT_TYPE, + SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE, + SignUpAttributesRequiredResult, + SignUpCodeRequiredResult, + SignUpCompletedResult, + SignUpPasswordRequiredResult, +} from "./result/SignUpActionResult.js"; +import { + SignUpChallengeRequest, + SignUpContinueWithAttributesRequest, + SignUpContinueWithOobRequest, + SignUpContinueWithPasswordRequest, + SignUpStartRequest, +} from "../../core/network_client/custom_auth_api/types/ApiRequestTypes.js"; +import { SignUpContinueResponse } from "../../core/network_client/custom_auth_api/types/ApiResponseTypes.js"; +import { ServerTelemetryManager } from "@azure/msal-common/browser"; + +export class SignUpClient extends CustomAuthInteractionClientBase { + /** + * Starts the sign up flow. + * @param parameters The parameters for the sign up start action. + * @returns The result of the sign up start action. + */ + async start( + parameters: SignUpStartParams + ): Promise { + const apiId = !parameters.password + ? PublicApiId.SIGN_UP_START + : PublicApiId.SIGN_UP_WITH_PASSWORD_START; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + + const startRequest: SignUpStartRequest = { + username: parameters.username, + password: parameters.password, + attributes: parameters.attributes, + challenge_type: this.getChallengeTypes(parameters.challengeType), + telemetryManager, + correlationId: parameters.correlationId, + }; + + this.logger.verbose( + "Calling start endpoint for sign up.", + parameters.correlationId + ); + + const startResponse = await this.customAuthApiClient.signUpApi.start( + startRequest + ); + + this.logger.verbose( + "Start endpoint called for sign up.", + parameters.correlationId + ); + + const challengeRequest: SignUpChallengeRequest = { + continuation_token: startResponse.continuation_token ?? "", + challenge_type: this.getChallengeTypes(parameters.challengeType), + telemetryManager, + correlationId: startResponse.correlation_id, + }; + + return this.performChallengeRequest(challengeRequest); + } + + /** + * Submits the code for the sign up flow. + * @param parameters The parameters for the sign up submit code action. + * @returns The result of the sign up submit code action. + */ + async submitCode( + parameters: SignUpSubmitCodeParams + ): Promise< + | SignUpCompletedResult + | SignUpPasswordRequiredResult + | SignUpAttributesRequiredResult + > { + const apiId = PublicApiId.SIGN_UP_SUBMIT_CODE; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + + const requestSubmitCode: SignUpContinueWithOobRequest = { + continuation_token: parameters.continuationToken, + oob: parameters.code, + telemetryManager, + correlationId: parameters.correlationId, + }; + + const result = await this.performContinueRequest( + "SignUpClient.submitCode", + parameters, + telemetryManager, + () => + this.customAuthApiClient.signUpApi.continueWithCode( + requestSubmitCode + ), + parameters.correlationId + ); + + if (result.type === SIGN_UP_CODE_REQUIRED_RESULT_TYPE) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + "The challenge type 'oob' is invalid after submtting code for sign up.", + parameters.correlationId + ); + } + + return result; + } + + /** + * Submits the password for the sign up flow. + * @param parameter The parameters for the sign up submit password action. + * @returns The result of the sign up submit password action. + */ + async submitPassword( + parameter: SignUpSubmitPasswordParams + ): Promise< + | SignUpCompletedResult + | SignUpCodeRequiredResult + | SignUpAttributesRequiredResult + > { + const apiId = PublicApiId.SIGN_UP_SUBMIT_PASSWORD; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + + const requestSubmitPwd: SignUpContinueWithPasswordRequest = { + continuation_token: parameter.continuationToken, + password: parameter.password, + telemetryManager, + correlationId: parameter.correlationId, + }; + + const result = await this.performContinueRequest( + "SignUpClient.submitPassword", + parameter, + telemetryManager, + () => + this.customAuthApiClient.signUpApi.continueWithPassword( + requestSubmitPwd + ), + parameter.correlationId + ); + + if (result.type === SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + "The challenge type 'password' is invalid after submtting password for sign up.", + parameter.correlationId + ); + } + + return result; + } + + /** + * Submits the attributes for the sign up flow. + * @param parameter The parameters for the sign up submit attributes action. + * @returns The result of the sign up submit attributes action. + */ + async submitAttributes( + parameter: SignUpSubmitUserAttributesParams + ): Promise< + | SignUpCompletedResult + | SignUpPasswordRequiredResult + | SignUpCodeRequiredResult + > { + const apiId = PublicApiId.SIGN_UP_SUBMIT_ATTRIBUTES; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + const reqWithAttr: SignUpContinueWithAttributesRequest = { + continuation_token: parameter.continuationToken, + attributes: parameter.attributes, + telemetryManager, + correlationId: parameter.correlationId, + }; + + const result = await this.performContinueRequest( + "SignUpClient.submitAttributes", + parameter, + telemetryManager, + () => + this.customAuthApiClient.signUpApi.continueWithAttributes( + reqWithAttr + ), + parameter.correlationId + ); + + if (result.type === SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.ATTRIBUTES_REQUIRED, + "User attributes required", + parameter.correlationId, + [], + "", + result.requiredAttributes, + result.continuationToken + ); + } + + return result; + } + + /** + * Resends the code for the sign up flow. + * @param parameters The parameters for the sign up resend code action. + * @returns The result of the sign up resend code action. + */ + async resendCode( + parameters: SignUpResendCodeParams + ): Promise { + const apiId = PublicApiId.SIGN_UP_RESEND_CODE; + const telemetryManager = this.initializeServerTelemetryManager(apiId); + + const challengeRequest: SignUpChallengeRequest = { + continuation_token: parameters.continuationToken ?? "", + challenge_type: this.getChallengeTypes(parameters.challengeType), + telemetryManager, + correlationId: parameters.correlationId, + }; + + const result = await this.performChallengeRequest(challengeRequest); + + if (result.type === SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + "The challenge type 'password' is invalid after resending code for sign up.", + parameters.correlationId + ); + } + + return result; + } + + private async performChallengeRequest( + request: SignUpChallengeRequest + ): Promise { + this.logger.verbose( + "Calling challenge endpoint for sign up.", + request.correlationId + ); + + const challengeResponse = + await this.customAuthApiClient.signUpApi.requestChallenge(request); + + this.logger.verbose( + "Challenge endpoint called for sign up.", + request.correlationId + ); + + if (challengeResponse.challenge_type === ChallengeType.OOB) { + // Code is required + this.logger.verbose( + "Challenge type is oob for sign up.", + request.correlationId + ); + + return createSignUpCodeRequiredResult({ + correlationId: challengeResponse.correlation_id, + continuationToken: challengeResponse.continuation_token ?? "", + challengeChannel: challengeResponse.challenge_channel ?? "", + challengeTargetLabel: + challengeResponse.challenge_target_label ?? "", + codeLength: + challengeResponse.code_length ?? + DefaultCustomAuthApiCodeLength, + interval: + challengeResponse.interval ?? + DefaultCustomAuthApiCodeResendIntervalInSec, + bindingMethod: challengeResponse.binding_method ?? "", + }); + } + + if (challengeResponse.challenge_type === ChallengeType.PASSWORD) { + // Password is required + this.logger.verbose( + "Challenge type is password for sign up.", + request.correlationId + ); + + return createSignUpPasswordRequiredResult({ + correlationId: challengeResponse.correlation_id, + continuationToken: challengeResponse.continuation_token ?? "", + }); + } + + this.logger.error( + `Unsupported challenge type '${challengeResponse.challenge_type}' for sign up.`, + request.correlationId + ); + + throw new CustomAuthApiError( + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + `Unsupported challenge type '${challengeResponse.challenge_type}'.`, + request.correlationId + ); + } + + private async performContinueRequest( + callerName: string, + requestParams: SignUpParamsBase, + telemetryManager: ServerTelemetryManager, + responseGetter: () => Promise, + requestCorrelationId: string + ): Promise< + | SignUpCompletedResult + | SignUpPasswordRequiredResult + | SignUpCodeRequiredResult + | SignUpAttributesRequiredResult + > { + this.logger.verbose( + `${callerName} is calling continue endpoint for sign up.`, + requestCorrelationId + ); + + try { + const response = await responseGetter(); + + this.logger.verbose( + `Continue endpoint called by ${callerName} for sign up.`, + requestCorrelationId + ); + + return createSignUpCompletedResult({ + correlationId: requestCorrelationId, + continuationToken: response.continuation_token ?? "", + }); + } catch (error) { + if (error instanceof CustomAuthApiError) { + return this.handleContinueResponseError( + error, + error.correlationId ?? requestCorrelationId, + requestParams, + telemetryManager + ); + } else { + this.logger.errorPii( + `${callerName} is failed to call continue endpoint for sign up. Error: ${error}`, + requestCorrelationId + ); + + throw new UnexpectedError(error, requestCorrelationId); + } + } + } + + private async handleContinueResponseError( + responseError: CustomAuthApiError, + correlationId: string, + requestParams: SignUpParamsBase, + telemetryManager: ServerTelemetryManager + ): Promise< + | SignUpPasswordRequiredResult + | SignUpCodeRequiredResult + | SignUpAttributesRequiredResult + > { + if ( + responseError.error === + CustomAuthApiErrorCode.CREDENTIAL_REQUIRED && + !!responseError.errorCodes && + responseError.errorCodes.includes(55103) + ) { + // Credential is required + this.logger.verbose( + "The credential is required in the sign up flow.", + correlationId + ); + + const continuationToken = + this.readContinuationTokenFromResponeError(responseError); + + // Call the challenge endpoint to ensure the password challenge type is supported. + const challengeRequest: SignUpChallengeRequest = { + continuation_token: continuationToken, + challenge_type: this.getChallengeTypes( + requestParams.challengeType + ), + telemetryManager, + correlationId, + }; + + const challengeResult = await this.performChallengeRequest( + challengeRequest + ); + + if ( + challengeResult.type === SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE + ) { + return createSignUpPasswordRequiredResult({ + correlationId: correlationId, + continuationToken: challengeResult.continuationToken, + }); + } + + if (challengeResult.type === SIGN_UP_CODE_REQUIRED_RESULT_TYPE) { + return createSignUpCodeRequiredResult({ + correlationId: challengeResult.correlationId, + continuationToken: challengeResult.continuationToken, + challengeChannel: challengeResult.challengeChannel, + challengeTargetLabel: challengeResult.challengeTargetLabel, + codeLength: challengeResult.codeLength, + interval: challengeResult.interval, + bindingMethod: challengeResult.bindingMethod, + }); + } + + throw new CustomAuthApiError( + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + "The challenge type is not supported.", + correlationId + ); + } + + if (this.isAttributesRequiredError(responseError, correlationId)) { + // Attributes are required + this.logger.verbose( + "Attributes are required in the sign up flow.", + correlationId + ); + + const continuationToken = + this.readContinuationTokenFromResponeError(responseError); + + return createSignUpAttributesRequiredResult({ + correlationId: correlationId, + continuationToken: continuationToken, + requiredAttributes: responseError.attributes ?? [], + }); + } + + throw responseError; + } + + private isAttributesRequiredError( + responseError: CustomAuthApiError, + correlationId: string + ): boolean { + if ( + responseError.error === CustomAuthApiErrorCode.ATTRIBUTES_REQUIRED + ) { + if ( + !responseError.attributes || + responseError.attributes.length === 0 + ) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_RESPONSE_BODY, + "Attributes are required but required_attributes field is missing in the response body.", + correlationId + ); + } + + return true; + } + + return false; + } + + private readContinuationTokenFromResponeError( + responseError: CustomAuthApiError + ): string { + if (!responseError.continuationToken) { + throw new CustomAuthApiError( + CustomAuthApiErrorCode.CONTINUATION_TOKEN_MISSING, + "Continuation token is missing in the response body", + responseError.correlationId + ); + } + + return responseError.continuationToken; + } +} diff --git a/lib/msal-browser/src/custom_auth/sign_up/interaction_client/parameter/SignUpParams.ts b/lib/msal-browser/src/custom_auth/sign_up/interaction_client/parameter/SignUpParams.ts new file mode 100644 index 0000000000..e34643f0a9 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/interaction_client/parameter/SignUpParams.ts @@ -0,0 +1,36 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export interface SignUpParamsBase { + clientId: string; + challengeType: Array; + username: string; + correlationId: string; +} + +export interface SignUpStartParams extends SignUpParamsBase { + password?: string; + attributes?: Record; +} + +export interface SignUpResendCodeParams extends SignUpParamsBase { + continuationToken: string; +} + +export interface SignUpContinueParams extends SignUpParamsBase { + continuationToken: string; +} + +export interface SignUpSubmitCodeParams extends SignUpContinueParams { + code: string; +} + +export interface SignUpSubmitPasswordParams extends SignUpContinueParams { + password: string; +} + +export interface SignUpSubmitUserAttributesParams extends SignUpContinueParams { + attributes: Record; +} diff --git a/lib/msal-browser/src/custom_auth/sign_up/interaction_client/result/SignUpActionResult.ts b/lib/msal-browser/src/custom_auth/sign_up/interaction_client/result/SignUpActionResult.ts new file mode 100644 index 0000000000..536537f192 --- /dev/null +++ b/lib/msal-browser/src/custom_auth/sign_up/interaction_client/result/SignUpActionResult.ts @@ -0,0 +1,77 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { UserAttribute } from "../../../core/network_client/custom_auth_api/types/ApiErrorResponseTypes.js"; + +interface SignUpActionResult { + type: string; + correlationId: string; + continuationToken: string; +} + +export interface SignUpCompletedResult extends SignUpActionResult { + type: typeof SIGN_UP_COMPLETED_RESULT_TYPE; +} + +export interface SignUpPasswordRequiredResult extends SignUpActionResult { + type: typeof SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE; +} + +export interface SignUpCodeRequiredResult extends SignUpActionResult { + type: typeof SIGN_UP_CODE_REQUIRED_RESULT_TYPE; + challengeChannel: string; + challengeTargetLabel: string; + codeLength: number; + interval: number; + bindingMethod: string; +} + +export interface SignUpAttributesRequiredResult extends SignUpActionResult { + type: typeof SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE; + requiredAttributes: Array; +} + +export const SIGN_UP_COMPLETED_RESULT_TYPE = "SignUpCompletedResult"; +export const SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE = + "SignUpPasswordRequiredResult"; +export const SIGN_UP_CODE_REQUIRED_RESULT_TYPE = "SignUpCodeRequiredResult"; +export const SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE = + "SignUpAttributesRequiredResult"; + +export function createSignUpCompletedResult( + input: Omit +): SignUpCompletedResult { + return { + type: SIGN_UP_COMPLETED_RESULT_TYPE, + ...input, + }; +} + +export function createSignUpPasswordRequiredResult( + input: Omit +): SignUpPasswordRequiredResult { + return { + type: SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE, + ...input, + }; +} + +export function createSignUpCodeRequiredResult( + input: Omit +): SignUpCodeRequiredResult { + return { + type: SIGN_UP_CODE_REQUIRED_RESULT_TYPE, + ...input, + }; +} + +export function createSignUpAttributesRequiredResult( + input: Omit +): SignUpAttributesRequiredResult { + return { + type: SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE, + ...input, + }; +} diff --git a/lib/msal-browser/src/error/BrowserAuthError.ts b/lib/msal-browser/src/error/BrowserAuthError.ts index 1cbc966270..3c5849dc9b 100644 --- a/lib/msal-browser/src/error/BrowserAuthError.ts +++ b/lib/msal-browser/src/error/BrowserAuthError.ts @@ -15,6 +15,10 @@ const ErrorLink = "For more visit: aka.ms/msaljs/browser-errors"; export const BrowserAuthErrorMessages = { [BrowserAuthErrorCodes.pkceNotCreated]: "The PKCE code challenge and verifier could not be generated.", + [BrowserAuthErrorCodes.earJwkEmpty]: + "No EAR encryption key provided. This is unexpected.", + [BrowserAuthErrorCodes.earJweEmpty]: + "Server response does not contain ear_jwe property. This is unexpected.", [BrowserAuthErrorCodes.cryptoNonExistent]: "The crypto object or function is not available.", [BrowserAuthErrorCodes.emptyNavigateUri]: @@ -52,8 +56,6 @@ export const BrowserAuthErrorMessages = { "No token request found in cache.", [BrowserAuthErrorCodes.unableToParseTokenRequestCacheError]: "The cached token request could not be parsed.", - [BrowserAuthErrorCodes.noCachedAuthorityError]: - "No cached authority found.", [BrowserAuthErrorCodes.authRequestNotSetError]: "Auth Request not set. Please ensure initiateAuthRequest was called from the InteractionHandler", [BrowserAuthErrorCodes.invalidCacheType]: "Invalid cache type", @@ -96,6 +98,9 @@ export const BrowserAuthErrorMessages = { "Failed to build request headers object.", [BrowserAuthErrorCodes.failedToParseHeaders]: "Failed to parse response headers", + [BrowserAuthErrorCodes.failedToDecryptEarResponse]: + "Failed to decrypt ear response", + [BrowserAuthErrorCodes.timedOut]: "The request timed out.", }; /** @@ -221,12 +226,6 @@ export const BrowserAuthErrorMessage = { BrowserAuthErrorCodes.unableToParseTokenRequestCacheError ], }, - noCachedAuthorityError: { - code: BrowserAuthErrorCodes.noCachedAuthorityError, - desc: BrowserAuthErrorMessages[ - BrowserAuthErrorCodes.noCachedAuthorityError - ], - }, authRequestNotSet: { code: BrowserAuthErrorCodes.authRequestNotSetError, desc: BrowserAuthErrorMessages[ diff --git a/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts b/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts index 8ba75a665a..31ee9fc9e5 100644 --- a/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts +++ b/lib/msal-browser/src/error/BrowserAuthErrorCodes.ts @@ -4,6 +4,8 @@ */ export const pkceNotCreated = "pkce_not_created"; +export const earJwkEmpty = "ear_jwk_empty"; +export const earJweEmpty = "ear_jwe_empty"; export const cryptoNonExistent = "crypto_nonexistent"; export const emptyNavigateUri = "empty_navigate_uri"; export const hashEmptyError = "hash_empty_error"; @@ -28,7 +30,6 @@ export const silentPromptValueError = "silent_prompt_value_error"; export const noTokenRequestCacheError = "no_token_request_cache_error"; export const unableToParseTokenRequestCacheError = "unable_to_parse_token_request_cache_error"; -export const noCachedAuthorityError = "no_cached_authority_error"; export const authRequestNotSetError = "auth_request_not_set_error"; export const invalidCacheType = "invalid_cache_type"; export const nonBrowserEnvironment = "non_browser_environment"; @@ -58,3 +59,5 @@ export const invalidBase64String = "invalid_base64_string"; export const invalidPopTokenRequest = "invalid_pop_token_request"; export const failedToBuildHeaders = "failed_to_build_headers"; export const failedToParseHeaders = "failed_to_parse_headers"; +export const failedToDecryptEarResponse = "failed_to_decrypt_ear_response"; +export const timedOut = "timed_out"; diff --git a/lib/msal-browser/src/error/NativeAuthError.ts b/lib/msal-browser/src/error/NativeAuthError.ts index c3056ae7e3..637bcac2e4 100644 --- a/lib/msal-browser/src/error/NativeAuthError.ts +++ b/lib/msal-browser/src/error/NativeAuthError.ts @@ -102,6 +102,10 @@ export function createNativeAuthError( return createBrowserAuthError( BrowserAuthErrorCodes.noNetworkConnectivity ); + case NativeStatusCodes.UX_NOT_ALLOWED: + return createInteractionRequiredAuthError( + InteractionRequiredAuthErrorCodes.uxNotAllowed + ); } } diff --git a/lib/msal-browser/src/event/EventMessage.ts b/lib/msal-browser/src/event/EventMessage.ts index d029284827..2ba2e29cb3 100644 --- a/lib/msal-browser/src/event/EventMessage.ts +++ b/lib/msal-browser/src/event/EventMessage.ts @@ -28,6 +28,16 @@ export type PopupEvent = { popupWindow: Window; }; +/** + * Payload for the BrokerConnectionEstablished event + */ +export type BrokerConnectionEvent = { + /** + * The origin of the broker that is connected to the client + */ + pairwiseBrokerOrigin: string; +}; + export type EventPayload = | AccountInfo | PopupRequest @@ -37,6 +47,7 @@ export type EventPayload = | EndSessionRequest | AuthenticationResult | PopupEvent + | BrokerConnectionEvent | null; export type EventError = AuthError | Error | null; diff --git a/lib/msal-browser/src/event/EventType.ts b/lib/msal-browser/src/event/EventType.ts index 88610d360d..53473d34cb 100644 --- a/lib/msal-browser/src/event/EventType.ts +++ b/lib/msal-browser/src/event/EventType.ts @@ -30,5 +30,6 @@ export const EventType = { LOGOUT_FAILURE: "msal:logoutFailure", LOGOUT_END: "msal:logoutEnd", RESTORE_FROM_BFCACHE: "msal:restoreFromBFCache", + BROKER_CONNECTION_ESTABLISHED: "msal:brokerConnectionEstablished", } as const; export type EventType = (typeof EventType)[keyof typeof EventType]; diff --git a/lib/msal-browser/src/index.ts b/lib/msal-browser/src/index.ts index db8b912914..55e327f5d3 100644 --- a/lib/msal-browser/src/index.ts +++ b/lib/msal-browser/src/index.ts @@ -86,6 +86,7 @@ export { EventCallbackFunction, EventMessageUtils, PopupEvent, + BrokerConnectionEvent, } from "./event/EventMessage.js"; export { EventType } from "./event/EventType.js"; export { EventHandler } from "./event/EventHandler.js"; @@ -160,3 +161,5 @@ export { } from "@azure/msal-common/browser"; export { version } from "./packageMetadata.js"; + +export { isPlatformBrokerAvailable } from "./broker/nativeBroker/PlatformAuthProvider.js"; diff --git a/lib/msal-browser/src/interaction_client/BaseInteractionClient.ts b/lib/msal-browser/src/interaction_client/BaseInteractionClient.ts index b8ef74c55e..a401b8945c 100644 --- a/lib/msal-browser/src/interaction_client/BaseInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/BaseInteractionClient.ts @@ -34,10 +34,10 @@ import { version } from "../packageMetadata.js"; import { BrowserConstants } from "../utils/BrowserConstants.js"; import * as BrowserUtils from "../utils/BrowserUtils.js"; import { INavigationClient } from "../navigation/INavigationClient.js"; -import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; import { ClearCacheRequest } from "../request/ClearCacheRequest.js"; import { createNewGuid } from "../crypto/BrowserCrypto.js"; +import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js"; export abstract class BaseInteractionClient { protected config: BrowserConfiguration; @@ -47,7 +47,7 @@ export abstract class BaseInteractionClient { protected logger: Logger; protected eventHandler: EventHandler; protected navigationClient: INavigationClient; - protected nativeMessageHandler: NativeMessageHandler | undefined; + protected platformAuthProvider: IPlatformAuthHandler | undefined; protected correlationId: string; protected performanceClient: IPerformanceClient; @@ -59,7 +59,7 @@ export abstract class BaseInteractionClient { eventHandler: EventHandler, navigationClient: INavigationClient, performanceClient: IPerformanceClient, - nativeMessageHandler?: NativeMessageHandler, + platformAuthProvider?: IPlatformAuthHandler, correlationId?: string ) { this.config = config; @@ -68,7 +68,7 @@ export abstract class BaseInteractionClient { this.networkClient = this.config.system.networkClient; this.eventHandler = eventHandler; this.navigationClient = navigationClient; - this.nativeMessageHandler = nativeMessageHandler; + this.platformAuthProvider = platformAuthProvider; this.correlationId = correlationId || createNewGuid(); this.logger = logger.clone( BrowserConstants.MSAL_SKU, @@ -87,23 +87,25 @@ export abstract class BaseInteractionClient { ): Promise; protected async clearCacheOnLogout( + correlationId: string, account?: AccountInfo | null ): Promise { if (account) { if ( AccountEntity.accountInfoIsEqual( account, - this.browserStorage.getActiveAccount(), + this.browserStorage.getActiveAccount(correlationId), false ) ) { this.logger.verbose("Setting active account to null"); - this.browserStorage.setActiveAccount(null); + this.browserStorage.setActiveAccount(null, correlationId); } // Clear given account. try { - await this.browserStorage.removeAccount( - AccountEntity.generateAccountCacheKey(account) + this.browserStorage.removeAccount( + AccountEntity.generateAccountCacheKey(account), + correlationId ); this.logger.verbose( "Cleared cache items belonging to the account provided in the logout request." @@ -120,7 +122,7 @@ export abstract class BaseInteractionClient { this.correlationId ); // Clear all accounts and tokens - await this.browserStorage.clear(); + this.browserStorage.clear(correlationId); // Clear any stray keys from IndexedDB await this.browserCrypto.clearKeystore(); } catch (e) { diff --git a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts similarity index 82% rename from lib/msal-browser/src/interaction_client/NativeInteractionClient.ts rename to lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts index 59ae769391..6546f6a6ff 100644 --- a/lib/msal-browser/src/interaction_client/NativeInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/PlatformAuthInteractionClient.ts @@ -30,8 +30,6 @@ import { createClientAuthError, ClientAuthErrorCodes, invokeAsync, - createAuthError, - AuthErrorCodes, updateAccountTenantProfileData, CacheHelpers, buildAccountToCache, @@ -45,20 +43,18 @@ import { EventHandler } from "../event/EventHandler.js"; import { PopupRequest } from "../request/PopupRequest.js"; import { SilentRequest } from "../request/SilentRequest.js"; import { SsoSilentRequest } from "../request/SsoSilentRequest.js"; -import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js"; import { - NativeExtensionMethod, ApiId, TemporaryCacheKeys, - NativeConstants, + PlatformAuthConstants, BrowserConstants, CacheLookupPolicy, } from "../utils/BrowserConstants.js"; +import { PlatformAuthRequest } from "../broker/nativeBroker/PlatformAuthRequest.js"; import { - NativeExtensionRequestBody, - NativeTokenRequest, -} from "../broker/nativeBroker/NativeRequest.js"; -import { MATS, NativeResponse } from "../broker/nativeBroker/NativeResponse.js"; + MATS, + PlatformAuthResponse, +} from "../broker/nativeBroker/PlatformAuthResponse.js"; import { NativeAuthError, NativeAuthErrorCodes, @@ -76,11 +72,12 @@ import { SilentCacheClient } from "./SilentCacheClient.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; import { base64Decode } from "../encode/Base64Decode.js"; import { version } from "../packageMetadata.js"; +import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js"; -export class NativeInteractionClient extends BaseInteractionClient { +export class PlatformAuthInteractionClient extends BaseInteractionClient { protected apiId: ApiId; protected accountId: string; - protected nativeMessageHandler: NativeMessageHandler; + protected platformAuthProvider: IPlatformAuthHandler; protected silentCacheClient: SilentCacheClient; protected nativeStorageManager: BrowserCacheManager; protected skus: string; @@ -94,7 +91,7 @@ export class NativeInteractionClient extends BaseInteractionClient { navigationClient: INavigationClient, apiId: ApiId, performanceClient: IPerformanceClient, - provider: NativeMessageHandler, + provider: IPlatformAuthHandler, accountId: string, nativeStorageImpl: BrowserCacheManager, correlationId?: string @@ -112,7 +109,7 @@ export class NativeInteractionClient extends BaseInteractionClient { ); this.apiId = apiId; this.accountId = accountId; - this.nativeMessageHandler = provider; + this.platformAuthProvider = provider; this.nativeStorageManager = nativeStorageImpl; this.silentCacheClient = new SilentCacheClient( config, @@ -126,27 +123,22 @@ export class NativeInteractionClient extends BaseInteractionClient { correlationId ); - const extensionName = - this.nativeMessageHandler.getExtensionId() === - NativeConstants.PREFERRED_EXTENSION_ID - ? "chrome" - : this.nativeMessageHandler.getExtensionId()?.length - ? "unknown" - : undefined; + const extensionName = this.platformAuthProvider.getExtensionName(); + this.skus = ServerTelemetryManager.makeExtraSkuString({ libraryName: BrowserConstants.MSAL_SKU, libraryVersion: version, extensionName: extensionName, - extensionVersion: this.nativeMessageHandler.getExtensionVersion(), + extensionVersion: this.platformAuthProvider.getExtensionVersion(), }); } /** * Adds SKUs to request extra query parameters - * @param request {NativeTokenRequest} + * @param request {PlatformAuthRequest} * @private */ - private addRequestSKUs(request: NativeTokenRequest) { + private addRequestSKUs(request: PlatformAuthRequest): void { request.extraParameters = { ...request.extraParameters, [AADServerParamKeys.X_CLIENT_EXTRA_SKU]: this.skus, @@ -177,6 +169,7 @@ export class NativeInteractionClient extends BaseInteractionClient { const serverTelemetryManager = this.initializeServerTelemetryManager( this.apiId ); + try { // initialize native request const nativeRequest = await this.initializeNativeRequest(request); @@ -206,18 +199,8 @@ export class NativeInteractionClient extends BaseInteractionClient { ); } - const { ...nativeTokenRequest } = nativeRequest; - - // fall back to native calls - const messageBody: NativeExtensionRequestBody = { - method: NativeExtensionMethod.GetToken, - request: nativeTokenRequest, - }; - - const response: object = - await this.nativeMessageHandler.sendMessage(messageBody); - const validatedResponse: NativeResponse = - this.validateNativeResponse(response); + const validatedResponse: PlatformAuthResponse = + await this.platformAuthProvider.sendMessage(nativeRequest); return await this.handleNativeResponse( validatedResponse, @@ -257,7 +240,7 @@ export class NativeInteractionClient extends BaseInteractionClient { * @returns CommonSilentFlowRequest */ private createSilentCacheRequest( - request: NativeTokenRequest, + request: PlatformAuthRequest, cachedAccount: AccountInfo ): CommonSilentFlowRequest { return { @@ -277,7 +260,7 @@ export class NativeInteractionClient extends BaseInteractionClient { */ protected async acquireTokensFromCache( nativeAccountId: string, - request: NativeTokenRequest + request: PlatformAuthRequest ): Promise { if (!nativeAccountId) { this.logger.warning( @@ -286,9 +269,12 @@ export class NativeInteractionClient extends BaseInteractionClient { throw createClientAuthError(ClientAuthErrorCodes.noAccountFound); } // fetch the account from browser cache - const account = this.browserStorage.getBaseAccountInfo({ - nativeAccountId, - }); + const account = this.browserStorage.getBaseAccountInfo( + { + nativeAccountId, + }, + this.correlationId + ); if (!account) { throw createClientAuthError(ClientAuthErrorCodes.noAccountFound); @@ -339,15 +325,8 @@ export class NativeInteractionClient extends BaseInteractionClient { remainingParameters ); - const messageBody: NativeExtensionRequestBody = { - method: NativeExtensionMethod.GetToken, - request: nativeRequest, - }; - try { - const response: object = - await this.nativeMessageHandler.sendMessage(messageBody); - this.validateNativeResponse(response); + await this.platformAuthProvider.sendMessage(nativeRequest); } catch (e) { // Only throw fatal errors here to allow application to fallback to regular redirect. Otherwise proceed and the error will be thrown in handleRedirectPromise if (e instanceof NativeAuthError) { @@ -427,33 +406,25 @@ export class NativeInteractionClient extends BaseInteractionClient { ) ); - const messageBody: NativeExtensionRequestBody = { - method: NativeExtensionMethod.GetToken, - request: request, - }; - const reqTimestamp = TimeUtils.nowSeconds(); try { this.logger.verbose( "NativeInteractionClient - handleRedirectPromise sending message to native broker." ); - const response: object = - await this.nativeMessageHandler.sendMessage(messageBody); - this.validateNativeResponse(response); - const result = this.handleNativeResponse( - response as NativeResponse, + const response: PlatformAuthResponse = + await this.platformAuthProvider.sendMessage(request); + const authResult = await this.handleNativeResponse( + response, request, reqTimestamp ); - this.browserStorage.setInteractionInProgress(false); - const res = await result; + const serverTelemetryManager = this.initializeServerTelemetryManager(this.apiId); serverTelemetryManager.clearNativeBrokerErrorCode(); - return res; + return authResult; } catch (e) { - this.browserStorage.setInteractionInProgress(false); throw e; } } @@ -474,8 +445,8 @@ export class NativeInteractionClient extends BaseInteractionClient { * @param reqTimestamp */ protected async handleNativeResponse( - response: NativeResponse, - request: NativeTokenRequest, + response: PlatformAuthResponse, + request: PlatformAuthRequest, reqTimestamp: number ): Promise { this.logger.trace( @@ -494,11 +465,22 @@ export class NativeInteractionClient extends BaseInteractionClient { ); const cachedhomeAccountId = - this.browserStorage.getAccountInfoFilteredBy({ - nativeAccountId: request.accountId, - })?.homeAccountId; - + this.browserStorage.getAccountInfoFilteredBy( + { + nativeAccountId: request.accountId, + }, + this.correlationId + )?.homeAccountId; + + // add exception for double brokering, please note this is temporary and will be fortified in future if ( + request.extraParameters?.child_client_id && + response.account.id !== request.accountId + ) { + this.logger.info( + "handleNativeServerResponse: Double broker flow detected, ignoring accountId mismatch" + ); + } else if ( homeAccountIdentifier !== cachedhomeAccountId && response.account.id !== request.accountId ) { @@ -516,6 +498,7 @@ export class NativeInteractionClient extends BaseInteractionClient { authority, homeAccountIdentifier, base64Decode, + this.correlationId, idTokenClaims, response.client_info, undefined, // environment @@ -525,6 +508,9 @@ export class NativeInteractionClient extends BaseInteractionClient { this.logger ); + // Ensure expires_in is in number format + response.expires_in = Number(response.expires_in); + // generate authenticationResult const result = await this.generateAuthenticationResult( response, @@ -536,7 +522,7 @@ export class NativeInteractionClient extends BaseInteractionClient { ); // cache accounts and tokens in the appropriate storage - await this.cacheAccount(baseAccount); + await this.cacheAccount(baseAccount, this.correlationId); await this.cacheNativeTokens( response, request, @@ -557,7 +543,7 @@ export class NativeInteractionClient extends BaseInteractionClient { * @returns */ protected createHomeAccountIdentifier( - response: NativeResponse, + response: PlatformAuthResponse, idTokenClaims: TokenClaims ): string { // Save account in browser storage @@ -578,13 +564,10 @@ export class NativeInteractionClient extends BaseInteractionClient { * @param request * @returns */ - generateScopes( - response: NativeResponse, - request: NativeTokenRequest - ): ScopeSet { - return response.scope - ? ScopeSet.fromString(response.scope) - : ScopeSet.fromString(request.scope); + generateScopes(requestScopes: string, responseScopes?: string): ScopeSet { + return responseScopes + ? ScopeSet.fromString(responseScopes) + : ScopeSet.fromString(requestScopes); } /** @@ -593,8 +576,8 @@ export class NativeInteractionClient extends BaseInteractionClient { * @param response */ async generatePopAccessToken( - response: NativeResponse, - request: NativeTokenRequest + response: PlatformAuthResponse, + request: PlatformAuthRequest ): Promise { if ( request.tokenType === AuthenticationScheme.POP && @@ -652,20 +635,23 @@ export class NativeInteractionClient extends BaseInteractionClient { * @returns */ protected async generateAuthenticationResult( - response: NativeResponse, - request: NativeTokenRequest, + response: PlatformAuthResponse, + request: PlatformAuthRequest, idTokenClaims: TokenClaims, accountEntity: AccountEntity, authority: string, reqTimestamp: number ): Promise { // Add Native Broker fields to Telemetry - const mats = this.addTelemetryFromNativeResponse(response); + const mats = this.addTelemetryFromNativeResponse( + response.properties.MATS + ); // If scopes not returned in server response, use request scopes - const responseScopes = response.scope - ? ScopeSet.fromString(response.scope) - : ScopeSet.fromString(request.scope); + const responseScopes = this.generateScopes( + request.scope, + response.scope + ); const accountProperties = response.account.properties || {}; const uid = @@ -730,16 +716,15 @@ export class NativeInteractionClient extends BaseInteractionClient { * cache the account entity in browser storage * @param accountEntity */ - async cacheAccount(accountEntity: AccountEntity): Promise { + async cacheAccount( + accountEntity: AccountEntity, + correlationId: string + ): Promise { // Store the account info and hence `nativeAccountId` in browser cache await this.browserStorage.setAccount(accountEntity, this.correlationId); // Remove any existing cached tokens for this account in browser storage - this.browserStorage.removeAccountContext(accountEntity).catch((e) => { - this.logger.error( - `Error occurred while removing account context from browser storage. ${e}` - ); - }); + this.browserStorage.removeAccountContext(accountEntity, correlationId); } /** @@ -753,8 +738,8 @@ export class NativeInteractionClient extends BaseInteractionClient { * @param reqTimestamp */ cacheNativeTokens( - response: NativeResponse, - request: NativeTokenRequest, + response: PlatformAuthResponse, + request: PlatformAuthRequest, homeAccountIdentifier: string, idTokenClaims: TokenClaims, responseAccessToken: string, @@ -778,7 +763,10 @@ export class NativeInteractionClient extends BaseInteractionClient { ? parseInt(response.expires_in, 10) : response.expires_in) || 0; const tokenExpirationSeconds = reqTimestamp + expiresIn; - const responseScopes = this.generateScopes(response, request); + const responseScopes = this.generateScopes( + response.scope, + request.scope + ); const cachedAccessToken: AccessTokenEntity | null = CacheHelpers.createAccessTokenEntity( @@ -809,10 +797,21 @@ export class NativeInteractionClient extends BaseInteractionClient { ); } + getExpiresInValue( + tokenType: string, + expiresIn: string | number | undefined + ): number { + return tokenType === AuthenticationScheme.POP + ? Constants.SHR_NONCE_VALIDITY + : (typeof expiresIn === "string" + ? parseInt(expiresIn, 10) + : expiresIn) || 0; + } + protected addTelemetryFromNativeResponse( - response: NativeResponse + matsResponse?: string ): MATS | null { - const mats = this.getMATSFromResponse(response); + const mats = this.getMATSFromResponse(matsResponse); if (!mats) { return null; @@ -820,9 +819,9 @@ export class NativeInteractionClient extends BaseInteractionClient { this.performanceClient.addFields( { - extensionId: this.nativeMessageHandler.getExtensionId(), + extensionId: this.platformAuthProvider.getExtensionId(), extensionVersion: - this.nativeMessageHandler.getExtensionVersion(), + this.platformAuthProvider.getExtensionVersion(), matsBrokerVersion: mats.broker_version, matsAccountJoinOnStart: mats.account_join_on_start, matsAccountJoinOnEnd: mats.account_join_on_end, @@ -843,37 +842,15 @@ export class NativeInteractionClient extends BaseInteractionClient { return mats; } - /** - * Validates native platform response before processing - * @param response - */ - private validateNativeResponse(response: object): NativeResponse { - if ( - response.hasOwnProperty("access_token") && - response.hasOwnProperty("id_token") && - response.hasOwnProperty("client_info") && - response.hasOwnProperty("account") && - response.hasOwnProperty("scope") && - response.hasOwnProperty("expires_in") - ) { - return response as NativeResponse; - } else { - throw createAuthError( - AuthErrorCodes.unexpectedError, - "Response missing expected properties." - ); - } - } - /** * Gets MATS telemetry from native response * @param response * @returns */ - private getMATSFromResponse(response: NativeResponse): MATS | null { - if (response.properties.MATS) { + private getMATSFromResponse(matsResponse: string | undefined): MATS | null { + if (matsResponse) { try { - return JSON.parse(response.properties.MATS); + return JSON.parse(matsResponse); } catch (e) { this.logger.error( "NativeInteractionClient - Error parsing MATS telemetry, returning null instead" @@ -906,79 +883,26 @@ export class NativeInteractionClient extends BaseInteractionClient { */ protected async initializeNativeRequest( request: PopupRequest | SsoSilentRequest - ): Promise { + ): Promise { this.logger.trace( "NativeInteractionClient - initializeNativeRequest called" ); - const requestAuthority = - request.authority || this.config.auth.authority; - - if (request.account) { - // validate authority - await this.getDiscoveredAuthority({ - requestAuthority, - requestAzureCloudOptions: request.azureCloudOptions, - account: request.account, - }); - } - - const canonicalAuthority = new UrlString(requestAuthority); - canonicalAuthority.validateAsUri(); + const canonicalAuthority = await this.getCanonicalAuthority(request); // scopes are expected to be received by the native broker as "scope" and will be added to the request below. Other properties that should be dropped from the request to the native broker can be included in the object destructuring here. const { scopes, ...remainingProperties } = request; const scopeSet = new ScopeSet(scopes || []); scopeSet.appendScopes(OIDC_DEFAULT_SCOPES); - const getPrompt = () => { - // If request is silent, prompt is always none - switch (this.apiId) { - case ApiId.ssoSilent: - case ApiId.acquireTokenSilent_silentFlow: - this.logger.trace( - "initializeNativeRequest: silent request sets prompt to none" - ); - return PromptValue.NONE; - default: - break; - } - - // Prompt not provided, request may proceed and native broker decides if it needs to prompt - if (!request.prompt) { - this.logger.trace( - "initializeNativeRequest: prompt was not provided" - ); - return undefined; - } - - // If request is interactive, check if prompt provided is allowed to go directly to native broker - switch (request.prompt) { - case PromptValue.NONE: - case PromptValue.CONSENT: - case PromptValue.LOGIN: - this.logger.trace( - "initializeNativeRequest: prompt is compatible with native flow" - ); - return request.prompt; - default: - this.logger.trace( - `initializeNativeRequest: prompt = ${request.prompt} is not compatible with native flow` - ); - throw createBrowserAuthError( - BrowserAuthErrorCodes.nativePromptNotSupported - ); - } - }; - - const validatedRequest: NativeTokenRequest = { + const validatedRequest: PlatformAuthRequest = { ...remainingProperties, accountId: this.accountId, clientId: this.config.auth.clientId, authority: canonicalAuthority.urlString, scope: scopeSet.printScopes(), redirectUri: this.getRedirectUri(request.redirectUri), - prompt: getPrompt(), + prompt: this.getPrompt(request.prompt), correlationId: this.correlationId, tokenType: request.authenticationScheme, windowTitleSubstring: document.title, @@ -1001,7 +925,7 @@ export class NativeInteractionClient extends BaseInteractionClient { validatedRequest.extraParameters = validatedRequest.extraParameters || {}; validatedRequest.extraParameters.telemetry = - NativeConstants.MATS_TELEMETRY; + PlatformAuthConstants.MATS_TELEMETRY; if (request.authenticationScheme === AuthenticationScheme.POP) { // add POP request type @@ -1042,12 +966,72 @@ export class NativeInteractionClient extends BaseInteractionClient { return validatedRequest; } + private async getCanonicalAuthority( + request: PopupRequest | SsoSilentRequest + ): Promise { + const requestAuthority = + request.authority || this.config.auth.authority; + + if (request.account) { + // validate authority + await this.getDiscoveredAuthority({ + requestAuthority, + requestAzureCloudOptions: request.azureCloudOptions, + account: request.account, + }); + } + + const canonicalAuthority = new UrlString(requestAuthority); + canonicalAuthority.validateAsUri(); + return canonicalAuthority; + } + + private getPrompt(prompt?: string): string | undefined { + // If request is silent, prompt is always none + switch (this.apiId) { + case ApiId.ssoSilent: + case ApiId.acquireTokenSilent_silentFlow: + this.logger.trace( + "initializeNativeRequest: silent request sets prompt to none" + ); + return PromptValue.NONE; + default: + break; + } + + // Prompt not provided, request may proceed and native broker decides if it needs to prompt + if (!prompt) { + this.logger.trace( + "initializeNativeRequest: prompt was not provided" + ); + return undefined; + } + + // If request is interactive, check if prompt provided is allowed to go directly to native broker + switch (prompt) { + case PromptValue.NONE: + case PromptValue.CONSENT: + case PromptValue.LOGIN: + this.logger.trace( + "initializeNativeRequest: prompt is compatible with native flow" + ); + return prompt; + default: + this.logger.trace( + `initializeNativeRequest: prompt = ${prompt} is not compatible with native flow` + ); + throw createBrowserAuthError( + BrowserAuthErrorCodes.nativePromptNotSupported + ); + } + } + /** * Handles extra broker request parameters - * @param request {NativeTokenRequest} + * @param request {PlatformAuthRequest} * @private */ - private handleExtraBrokerParams(request: NativeTokenRequest): void { + private handleExtraBrokerParams(request: PlatformAuthRequest): void { const hasExtraBrokerParams = request.extraParameters && request.extraParameters.hasOwnProperty( diff --git a/lib/msal-browser/src/interaction_client/PopupClient.ts b/lib/msal-browser/src/interaction_client/PopupClient.ts index c94c4019e2..287f5c27f9 100644 --- a/lib/msal-browser/src/interaction_client/PopupClient.ts +++ b/lib/msal-browser/src/interaction_client/PopupClient.ts @@ -4,14 +4,11 @@ */ import { - CommonAuthorizationCodeRequest, AuthorizationCodeClient, - ThrottlingUtils, CommonEndSessionRequest, UrlString, AuthError, OIDC_DEFAULT_SCOPES, - ProtocolUtils, PerformanceEvents, IPerformanceClient, Logger, @@ -21,6 +18,7 @@ import { invokeAsync, invoke, PkceCodes, + CommonAuthorizationUrlRequest, } from "@azure/msal-common/browser"; import { StandardInteractionClient } from "./StandardInteractionClient.js"; import { EventType } from "../event/EventType.js"; @@ -33,8 +31,6 @@ import { EndSessionPopupRequest } from "../request/EndSessionPopupRequest.js"; import { NavigationOptions } from "../navigation/NavigationOptions.js"; import * as BrowserUtils from "../utils/BrowserUtils.js"; import { PopupRequest } from "../request/PopupRequest.js"; -import { NativeInteractionClient } from "./NativeInteractionClient.js"; -import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js"; import { createBrowserAuthError, BrowserAuthErrorCodes, @@ -43,11 +39,15 @@ import { INavigationClient } from "../navigation/INavigationClient.js"; import { EventHandler } from "../event/EventHandler.js"; import { BrowserCacheManager } from "../cache/BrowserCacheManager.js"; import { BrowserConfiguration } from "../config/Configuration.js"; -import { InteractionHandler } from "../interaction_handler/InteractionHandler.js"; import { PopupWindowAttributes } from "../request/PopupWindowAttributes.js"; import { EventError } from "../event/EventMessage.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; import * as ResponseHandler from "../response/ResponseHandler.js"; +import * as Authorize from "../protocol/Authorize.js"; +import { generatePkceCodes } from "../crypto/PkceGenerator.js"; +import { isPlatformAuthAllowed } from "../broker/nativeBroker/PlatformAuthProvider.js"; +import { generateEarKey } from "../crypto/BrowserCrypto.js"; +import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js"; export type PopupParams = { popup?: Window | null; @@ -69,7 +69,7 @@ export class PopupClient extends StandardInteractionClient { navigationClient: INavigationClient, performanceClient: IPerformanceClient, nativeStorageImpl: BrowserCacheManager, - nativeMessageHandler?: NativeMessageHandler, + platformAuthHandler?: IPlatformAuthHandler, correlationId?: string ) { super( @@ -80,12 +80,13 @@ export class PopupClient extends StandardInteractionClient { eventHandler, navigationClient, performanceClient, - nativeMessageHandler, + platformAuthHandler, correlationId ); // Properly sets this reference for the unload event. this.unloadWindow = this.unloadWindow.bind(this); this.nativeStorage = nativeStorageImpl; + this.eventHandler = eventHandler; } /** @@ -205,9 +206,6 @@ export class PopupClient extends StandardInteractionClient { pkceCodes?: PkceCodes ): Promise { this.logger.verbose("acquireTokenPopupAsync called"); - const serverTelemetryManager = this.initializeServerTelemetryManager( - ApiId.acquireTokenPopup - ); const validRequest = await invokeAsync( this.initializeAuthorizationRequest.bind(this), @@ -225,60 +223,80 @@ export class PopupClient extends StandardInteractionClient { BrowserUtils.preconnect(validRequest.authority); } - try { - // Create auth code request and generate PKCE params - const authCodeRequest: CommonAuthorizationCodeRequest = - await invokeAsync( - this.initializeAuthorizationCodeRequest.bind(this), - PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest, - this.logger, - this.performanceClient, - this.correlationId - )(validRequest, pkceCodes); + const isPlatformBroker = isPlatformAuthAllowed( + this.config, + this.logger, + this.platformAuthProvider, + request.authenticationScheme + ); + validRequest.platformBroker = isPlatformBroker; + if (this.config.auth.protocolMode === ProtocolMode.EAR) { + return this.executeEarFlow(validRequest, popupParams); + } else { + return this.executeCodeFlow(validRequest, popupParams, pkceCodes); + } + } + + /** + * Executes auth code + PKCE flow + * @param request + * @param popupParams + * @param pkceCodes + * @returns + */ + async executeCodeFlow( + request: CommonAuthorizationUrlRequest, + popupParams: PopupParams, + pkceCodes?: PkceCodes + ): Promise { + const correlationId = request.correlationId; + const serverTelemetryManager = this.initializeServerTelemetryManager( + ApiId.acquireTokenPopup + ); + + const pkce = + pkceCodes || + (await invokeAsync( + generatePkceCodes, + PerformanceEvents.GeneratePkceCodes, + this.logger, + this.performanceClient, + correlationId + )(this.performanceClient, this.logger, correlationId)); + + const popupRequest = { + ...request, + codeChallenge: pkce.challenge, + }; + + try { // Initialize the client const authClient: AuthorizationCodeClient = await invokeAsync( this.createAuthCodeClient.bind(this), PerformanceEvents.StandardInteractionClientCreateAuthCodeClient, this.logger, this.performanceClient, - this.correlationId + correlationId )({ serverTelemetryManager, - requestAuthority: validRequest.authority, - requestAzureCloudOptions: validRequest.azureCloudOptions, - requestExtraQueryParameters: validRequest.extraQueryParameters, - account: validRequest.account, + requestAuthority: popupRequest.authority, + requestAzureCloudOptions: popupRequest.azureCloudOptions, + requestExtraQueryParameters: popupRequest.extraQueryParameters, + account: popupRequest.account, }); - const isPlatformBroker = - NativeMessageHandler.isPlatformBrokerAvailable( - this.config, - this.logger, - this.nativeMessageHandler, - request.authenticationScheme - ); - // Start measurement for server calls with native brokering enabled - let fetchNativeAccountIdMeasurement; - if (isPlatformBroker) { - fetchNativeAccountIdMeasurement = - this.performanceClient.startMeasurement( - PerformanceEvents.FetchAccountIdWithNativeBroker, - request.correlationId - ); - } - // Create acquire token url. - const navigateUrl = await authClient.getAuthCodeUrl({ - ...validRequest, - platformBroker: isPlatformBroker, - }); - - // Create popup interaction handler. - const interactionHandler = new InteractionHandler( - authClient, - this.browserStorage, - authCodeRequest, + const navigateUrl = await invokeAsync( + Authorize.getAuthCodeRequestUrl, + PerformanceEvents.GetAuthCodeUrl, + this.logger, + this.performanceClient, + correlationId + )( + this.config, + authClient.authority, + popupRequest, this.logger, this.performanceClient ); @@ -312,62 +330,27 @@ export class PopupClient extends StandardInteractionClient { this.config.auth.OIDCOptions.serverResponseType, this.logger ); - // Remove throttle if it exists - ThrottlingUtils.removeThrottle( - this.browserStorage, - this.config.auth.clientId, - authCodeRequest - ); - - if (serverParams.accountId) { - this.logger.verbose( - "Account id found in hash, calling WAM for token" - ); - // end measurement for server call with native brokering enabled - if (fetchNativeAccountIdMeasurement) { - fetchNativeAccountIdMeasurement.end({ - success: true, - isNativeBroker: true, - }); - } - if (!this.nativeMessageHandler) { - throw createBrowserAuthError( - BrowserAuthErrorCodes.nativeConnectionNotEstablished - ); - } - const nativeInteractionClient = new NativeInteractionClient( - this.config, - this.browserStorage, - this.browserCrypto, - this.logger, - this.eventHandler, - this.navigationClient, - ApiId.acquireTokenPopup, - this.performanceClient, - this.nativeMessageHandler, - serverParams.accountId, - this.nativeStorage, - validRequest.correlationId - ); - const { userRequestState } = ProtocolUtils.parseRequestState( - this.browserCrypto, - validRequest.state - ); - return await nativeInteractionClient.acquireToken({ - ...validRequest, - state: userRequestState, - prompt: undefined, // Server should handle the prompt, ideally native broker can do this part silently - }); - } - - // Handle response from hash string. - const result = await interactionHandler.handleCodeResponse( + return await invokeAsync( + Authorize.handleResponseCode, + PerformanceEvents.HandleResponseCode, + this.logger, + this.performanceClient, + correlationId + )( + request, serverParams, - validRequest + pkce.verifier, + ApiId.acquireTokenPopup, + this.config, + authClient, + this.browserStorage, + this.nativeStorage, + this.eventHandler, + this.logger, + this.performanceClient, + this.platformAuthProvider ); - - return result; } catch (e) { // Close the synchronous popup if an error is thrown before the window unload event is registered popupParams.popup?.close(); @@ -380,6 +363,95 @@ export class PopupClient extends StandardInteractionClient { } } + /** + * Executes EAR flow + * @param request + */ + async executeEarFlow( + request: CommonAuthorizationUrlRequest, + popupParams: PopupParams + ): Promise { + const correlationId = request.correlationId; + // Get the frame handle for the silent request + const discoveredAuthority = await invokeAsync( + this.getDiscoveredAuthority.bind(this), + PerformanceEvents.StandardInteractionClientGetDiscoveredAuthority, + this.logger, + this.performanceClient, + correlationId + )({ + requestAuthority: request.authority, + requestAzureCloudOptions: request.azureCloudOptions, + requestExtraQueryParameters: request.extraQueryParameters, + account: request.account, + }); + + const earJwk = await invokeAsync( + generateEarKey, + PerformanceEvents.GenerateEarKey, + this.logger, + this.performanceClient, + correlationId + )(); + const popupRequest = { + ...request, + earJwk: earJwk, + }; + const popupWindow = + popupParams.popup || this.openPopup("about:blank", popupParams); + + const form = await Authorize.getEARForm( + popupWindow.document, + this.config, + discoveredAuthority, + popupRequest, + this.logger, + this.performanceClient + ); + form.submit(); + + // Monitor the popup for the hash. Return the string value and close the popup when the hash is received. Default timeout is 60 seconds. + const responseString = await invokeAsync( + this.monitorPopupForHash.bind(this), + PerformanceEvents.SilentHandlerMonitorIframeForHash, + this.logger, + this.performanceClient, + correlationId + )(popupWindow, popupParams.popupWindowParent); + + const serverParams = invoke( + ResponseHandler.deserializeResponse, + PerformanceEvents.DeserializeResponse, + this.logger, + this.performanceClient, + this.correlationId + )( + responseString, + this.config.auth.OIDCOptions.serverResponseType, + this.logger + ); + + return invokeAsync( + Authorize.handleResponseEAR, + PerformanceEvents.HandleResponseEar, + this.logger, + this.performanceClient, + correlationId + )( + popupRequest, + serverParams, + ApiId.acquireTokenPopup, + this.config, + discoveredAuthority, + this.browserStorage, + this.nativeStorage, + this.eventHandler, + this.logger, + this.performanceClient, + this.platformAuthProvider + ); + } + /** * * @param validRequest @@ -408,7 +480,10 @@ export class PopupClient extends StandardInteractionClient { try { // Clear cache on logout - await this.clearCacheOnLogout(validRequest.account); + await this.clearCacheOnLogout( + this.correlationId, + validRequest.account + ); // Initialize the client const authClient = await invokeAsync( @@ -431,8 +506,9 @@ export class PopupClient extends StandardInteractionClient { validRequest.postLogoutRedirectUri && authClient.authority.protocolMode === ProtocolMode.OIDC ) { - void this.browserStorage.removeAccount( - validRequest.account?.homeAccountId + this.browserStorage.removeAccount( + validRequest.account?.homeAccountId, + this.correlationId ); this.eventHandler.emitEvent( @@ -521,7 +597,6 @@ export class PopupClient extends StandardInteractionClient { (e as AuthError).setCorrelationId(this.correlationId); serverTelemetryManager.cacheFailedRequest(e); } - this.browserStorage.setInteractionInProgress(false); this.eventHandler.emitEvent( EventType.LOGOUT_FAILURE, InteractionType.Popup, @@ -678,7 +753,6 @@ export class PopupClient extends StandardInteractionClient { this.logger.error( "error opening popup " + (e as AuthError).message ); - this.browserStorage.setInteractionInProgress(false); throw createBrowserAuthError( BrowserAuthErrorCodes.popupWindowError ); @@ -769,9 +843,6 @@ export class PopupClient extends StandardInteractionClient { * Event callback to unload main window. */ unloadWindow(e: Event): void { - this.browserStorage.cleanRequestByInteractionType( - InteractionType.Popup - ); if (this.currentWindow) { this.currentWindow.close(); } @@ -792,9 +863,6 @@ export class PopupClient extends StandardInteractionClient { "beforeunload", this.unloadWindow ); - - // Interaction is completed - remove interaction status. - this.browserStorage.setInteractionInProgress(false); } /** diff --git a/lib/msal-browser/src/interaction_client/RedirectClient.ts b/lib/msal-browser/src/interaction_client/RedirectClient.ts index 0d18134bd9..5c728ff3d6 100644 --- a/lib/msal-browser/src/interaction_client/RedirectClient.ts +++ b/lib/msal-browser/src/interaction_client/RedirectClient.ts @@ -4,15 +4,12 @@ */ import { - CommonAuthorizationCodeRequest, AuthorizationCodeClient, UrlString, AuthError, ServerTelemetryManager, Constants, - ProtocolUtils, - ServerAuthorizationCodeResponse, - ThrottlingUtils, + AuthorizeResponse, ICrypto, Logger, IPerformanceClient, @@ -22,14 +19,15 @@ import { ServerResponseType, UrlUtils, InProgressPerformanceEvent, + CommonAuthorizationUrlRequest, } from "@azure/msal-common/browser"; import { StandardInteractionClient } from "./StandardInteractionClient.js"; import { ApiId, + INTERACTION_TYPE, InteractionType, TemporaryCacheKeys, } from "../utils/BrowserConstants.js"; -import { RedirectHandler } from "../interaction_handler/RedirectHandler.js"; import * as BrowserUtils from "../utils/BrowserUtils.js"; import { EndSessionRequest } from "../request/EndSessionRequest.js"; import { EventType } from "../event/EventType.js"; @@ -39,8 +37,6 @@ import { BrowserAuthErrorCodes, } from "../error/BrowserAuthError.js"; import { RedirectRequest } from "../request/RedirectRequest.js"; -import { NativeInteractionClient } from "./NativeInteractionClient.js"; -import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js"; import { BrowserConfiguration } from "../config/Configuration.js"; import { BrowserCacheManager } from "../cache/BrowserCacheManager.js"; import { EventHandler } from "../event/EventHandler.js"; @@ -48,6 +44,11 @@ import { INavigationClient } from "../navigation/INavigationClient.js"; import { EventError } from "../event/EventMessage.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; import * as ResponseHandler from "../response/ResponseHandler.js"; +import * as Authorize from "../protocol/Authorize.js"; +import { generatePkceCodes } from "../crypto/PkceGenerator.js"; +import { isPlatformAuthAllowed } from "../broker/nativeBroker/PlatformAuthProvider.js"; +import { generateEarKey } from "../crypto/BrowserCrypto.js"; +import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js"; function getNavigationType(): NavigationTimingType | undefined { if ( @@ -77,7 +78,7 @@ export class RedirectClient extends StandardInteractionClient { navigationClient: INavigationClient, performanceClient: IPerformanceClient, nativeStorageImpl: BrowserCacheManager, - nativeMessageHandler?: NativeMessageHandler, + platformAuthHandler?: IPlatformAuthHandler, correlationId?: string ) { super( @@ -88,7 +89,7 @@ export class RedirectClient extends StandardInteractionClient { eventHandler, navigationClient, performanceClient, - nativeMessageHandler, + platformAuthHandler, correlationId ); this.nativeStorage = nativeStorageImpl; @@ -107,15 +108,11 @@ export class RedirectClient extends StandardInteractionClient { this.correlationId )(request, InteractionType.Redirect); - this.browserStorage.updateCacheEntries( - validRequest.state, - validRequest.nonce, - validRequest.authority, - validRequest.loginHint || "", - validRequest.account || null - ); - const serverTelemetryManager = this.initializeServerTelemetryManager( - ApiId.acquireTokenRedirect + validRequest.platformBroker = isPlatformAuthAllowed( + this.config, + this.logger, + this.platformAuthProvider, + request.authenticationScheme ); const handleBackButton = (event: PageTransitionEvent) => { @@ -124,7 +121,7 @@ export class RedirectClient extends StandardInteractionClient { this.logger.verbose( "Page was restored from back/forward cache. Clearing temporary cache." ); - this.browserStorage.cleanRequestByState(validRequest.state); + this.browserStorage.resetRequestCache(); this.eventHandler.emitEvent( EventType.RESTORE_FROM_BFCACHE, InteractionType.Redirect @@ -132,17 +129,71 @@ export class RedirectClient extends StandardInteractionClient { } }; + const redirectStartPage = this.getRedirectStartPage( + request.redirectStartPage + ); + this.logger.verbosePii(`Redirect start page: ${redirectStartPage}`); + // Cache start page, returns to this page after redirectUri if navigateToLoginRequestUrl is true + this.browserStorage.setTemporaryCache( + TemporaryCacheKeys.ORIGIN_URI, + redirectStartPage, + true + ); + + // Clear temporary cache if the back button is clicked during the redirect flow. + window.addEventListener("pageshow", handleBackButton); + try { - // Create auth code request and generate PKCE params - const authCodeRequest: CommonAuthorizationCodeRequest = - await invokeAsync( - this.initializeAuthorizationCodeRequest.bind(this), - PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest, - this.logger, - this.performanceClient, - this.correlationId - )(validRequest); + if (this.config.auth.protocolMode === ProtocolMode.EAR) { + await this.executeEarFlow(validRequest); + } else { + await this.executeCodeFlow( + validRequest, + request.onRedirectNavigate + ); + } + } catch (e) { + if (e instanceof AuthError) { + e.setCorrelationId(this.correlationId); + } + window.removeEventListener("pageshow", handleBackButton); + throw e; + } + } + + /** + * Executes auth code + PKCE flow + * @param request + * @returns + */ + async executeCodeFlow( + request: CommonAuthorizationUrlRequest, + onRedirectNavigate?: (url: string) => boolean | void + ): Promise { + const correlationId = request.correlationId; + const serverTelemetryManager = this.initializeServerTelemetryManager( + ApiId.acquireTokenRedirect + ); + const pkceCodes = await invokeAsync( + generatePkceCodes, + PerformanceEvents.GeneratePkceCodes, + this.logger, + this.performanceClient, + correlationId + )(this.performanceClient, this.logger, correlationId); + + const redirectRequest = { + ...request, + codeChallenge: pkceCodes.challenge, + }; + + this.browserStorage.cacheAuthorizeRequest( + redirectRequest, + pkceCodes.verifier + ); + + try { // Initialize the client const authClient: AuthorizationCodeClient = await invokeAsync( this.createAuthCodeClient.bind(this), @@ -152,60 +203,97 @@ export class RedirectClient extends StandardInteractionClient { this.correlationId )({ serverTelemetryManager, - requestAuthority: validRequest.authority, - requestAzureCloudOptions: validRequest.azureCloudOptions, - requestExtraQueryParameters: validRequest.extraQueryParameters, - account: validRequest.account, + requestAuthority: redirectRequest.authority, + requestAzureCloudOptions: redirectRequest.azureCloudOptions, + requestExtraQueryParameters: + redirectRequest.extraQueryParameters, + account: redirectRequest.account, }); - // Create redirect interaction handler. - const interactionHandler = new RedirectHandler( - authClient, - this.browserStorage, - authCodeRequest, + // Create acquire token url. + const navigateUrl = await invokeAsync( + Authorize.getAuthCodeRequestUrl, + PerformanceEvents.GetAuthCodeUrl, + this.logger, + this.performanceClient, + request.correlationId + )( + this.config, + authClient.authority, + redirectRequest, this.logger, this.performanceClient ); - - // Create acquire token url. - const navigateUrl = await authClient.getAuthCodeUrl({ - ...validRequest, - platformBroker: NativeMessageHandler.isPlatformBrokerAvailable( - this.config, - this.logger, - this.nativeMessageHandler, - request.authenticationScheme - ), - }); - - const redirectStartPage = this.getRedirectStartPage( - request.redirectStartPage - ); - this.logger.verbosePii(`Redirect start page: ${redirectStartPage}`); - - // Clear temporary cache if the back button is clicked during the redirect flow. - window.addEventListener("pageshow", handleBackButton); - // Show the UI once the url has been created. Response will come back in the hash, which will be handled in the handleRedirectCallback function. - return await interactionHandler.initiateAuthRequest(navigateUrl, { - navigationClient: this.navigationClient, - redirectTimeout: this.config.system.redirectNavigationTimeout, - redirectStartPage: redirectStartPage, - onRedirectNavigate: - request.onRedirectNavigate || - this.config.auth.onRedirectNavigate, - }); + return await this.initiateAuthRequest( + navigateUrl, + onRedirectNavigate + ); } catch (e) { if (e instanceof AuthError) { e.setCorrelationId(this.correlationId); serverTelemetryManager.cacheFailedRequest(e); } - window.removeEventListener("pageshow", handleBackButton); - this.browserStorage.cleanRequestByState(validRequest.state); throw e; } } + /** + * Executes EAR flow + * @param request + */ + async executeEarFlow( + request: CommonAuthorizationUrlRequest + ): Promise { + const correlationId = request.correlationId; + // Get the frame handle for the silent request + const discoveredAuthority = await invokeAsync( + this.getDiscoveredAuthority.bind(this), + PerformanceEvents.StandardInteractionClientGetDiscoveredAuthority, + this.logger, + this.performanceClient, + correlationId + )({ + requestAuthority: request.authority, + requestAzureCloudOptions: request.azureCloudOptions, + requestExtraQueryParameters: request.extraQueryParameters, + account: request.account, + }); + + const earJwk = await invokeAsync( + generateEarKey, + PerformanceEvents.GenerateEarKey, + this.logger, + this.performanceClient, + correlationId + )(); + const redirectRequest = { + ...request, + earJwk: earJwk, + }; + this.browserStorage.cacheAuthorizeRequest(redirectRequest); + + const form = await Authorize.getEARForm( + document, + this.config, + discoveredAuthority, + redirectRequest, + this.logger, + this.performanceClient + ); + form.submit(); + return new Promise((resolve, reject) => { + setTimeout(() => { + reject( + createBrowserAuthError( + BrowserAuthErrorCodes.timedOut, + "failed_to_redirect" + ) + ); + }, this.config.system.redirectNavigationTimeout); + }); + } + /** * Checks if navigateToLoginRequestUrl is set, and: * - if true, performs logic to cache and navigate @@ -215,6 +303,8 @@ export class RedirectClient extends StandardInteractionClient { */ async handleRedirectPromise( hash: string = "", + request: CommonAuthorizationUrlRequest, + pkceVerifier: string, parentMeasurement: InProgressPerformanceEvent ): Promise { const serverTelemetryManager = this.initializeServerTelemetryManager( @@ -222,12 +312,6 @@ export class RedirectClient extends StandardInteractionClient { ); try { - if (!this.browserStorage.isInteractionInProgress(true)) { - this.logger.info( - "handleRedirectPromise called but there is no interaction in progress, returning null." - ); - return null; - } const [serverParams, responseString] = this.getRedirectResponse( hash || "" ); @@ -236,9 +320,7 @@ export class RedirectClient extends StandardInteractionClient { this.logger.info( "handleRedirectPromise did not detect a response as a result of a redirect. Cleaning temporary cache." ); - this.browserStorage.cleanRequestByInteractionType( - InteractionType.Redirect - ); + this.browserStorage.resetRequestCache(); // Do not instrument "no_server_response" if user clicked back button if (getNavigationType() !== "back_forward") { @@ -279,6 +361,8 @@ export class RedirectClient extends StandardInteractionClient { const handleHashResult = await this.handleResponse( serverParams, + request, + pkceVerifier, serverTelemetryManager ); @@ -289,6 +373,8 @@ export class RedirectClient extends StandardInteractionClient { ); return await this.handleResponse( serverParams, + request, + pkceVerifier, serverTelemetryManager ); } else if ( @@ -348,6 +434,8 @@ export class RedirectClient extends StandardInteractionClient { if (!processHashOnRedirect) { return await this.handleResponse( serverParams, + request, + pkceVerifier, serverTelemetryManager ); } @@ -359,9 +447,6 @@ export class RedirectClient extends StandardInteractionClient { (e as AuthError).setCorrelationId(this.correlationId); serverTelemetryManager.cacheFailedRequest(e); } - this.browserStorage.cleanRequestByInteractionType( - InteractionType.Redirect - ); throw e; } } @@ -373,7 +458,7 @@ export class RedirectClient extends StandardInteractionClient { */ protected getRedirectResponse( userProvidedResponse: string - ): [ServerAuthorizationCodeResponse | null, string] { + ): [AuthorizeResponse | null, string] { this.logger.verbose("getRedirectResponseHash called"); // Get current location hash from window or cache. let responseString = userProvidedResponse; @@ -439,7 +524,9 @@ export class RedirectClient extends StandardInteractionClient { * @param state */ protected async handleResponse( - serverParams: ServerAuthorizationCodeResponse, + serverParams: AuthorizeResponse, + request: CommonAuthorizationUrlRequest, + codeVerifier: string, serverTelemetryManager: ServerTelemetryManager ): Promise { const state = serverParams.state; @@ -447,52 +534,37 @@ export class RedirectClient extends StandardInteractionClient { throw createBrowserAuthError(BrowserAuthErrorCodes.noStateInHash); } - const cachedRequest = this.browserStorage.getCachedRequest(state); - this.logger.verbose("handleResponse called, retrieved cached request"); - - if (serverParams.accountId) { - this.logger.verbose( - "Account id found in hash, calling WAM for token" - ); - if (!this.nativeMessageHandler) { - throw createBrowserAuthError( - BrowserAuthErrorCodes.nativeConnectionNotEstablished - ); - } - const nativeInteractionClient = new NativeInteractionClient( + if (serverParams.ear_jwe) { + const discoveredAuthority = await invokeAsync( + this.getDiscoveredAuthority.bind(this), + PerformanceEvents.StandardInteractionClientGetDiscoveredAuthority, + this.logger, + this.performanceClient, + request.correlationId + )({ + requestAuthority: request.authority, + requestAzureCloudOptions: request.azureCloudOptions, + requestExtraQueryParameters: request.extraQueryParameters, + account: request.account, + }); + return invokeAsync( + Authorize.handleResponseEAR, + PerformanceEvents.HandleResponseEar, + this.logger, + this.performanceClient, + request.correlationId + )( + request, + serverParams, + ApiId.acquireTokenRedirect, this.config, + discoveredAuthority, this.browserStorage, - this.browserCrypto, - this.logger, + this.nativeStorage, this.eventHandler, - this.navigationClient, - ApiId.acquireTokenPopup, + this.logger, this.performanceClient, - this.nativeMessageHandler, - serverParams.accountId, - this.nativeStorage, - cachedRequest.correlationId - ); - const { userRequestState } = ProtocolUtils.parseRequestState( - this.browserCrypto, - state - ); - return nativeInteractionClient - .acquireToken({ - ...cachedRequest, - state: userRequestState, - prompt: undefined, // Server should handle the prompt, ideally native broker can do this part silently - }) - .finally(() => { - this.browserStorage.cleanRequestByState(state); - }); - } - - // Hash contains known properties - handle and return in callback - const currentAuthority = this.browserStorage.getCachedAuthority(state); - if (!currentAuthority) { - throw createBrowserAuthError( - BrowserAuthErrorCodes.noCachedAuthorityError + this.platformAuthProvider ); } @@ -502,21 +574,97 @@ export class RedirectClient extends StandardInteractionClient { this.logger, this.performanceClient, this.correlationId - )({ serverTelemetryManager, requestAuthority: currentAuthority }); - - ThrottlingUtils.removeThrottle( - this.browserStorage, - this.config.auth.clientId, - cachedRequest - ); - const interactionHandler = new RedirectHandler( + )({ serverTelemetryManager, requestAuthority: request.authority }); + return invokeAsync( + Authorize.handleResponseCode, + PerformanceEvents.HandleResponseCode, + this.logger, + this.performanceClient, + request.correlationId + )( + request, + serverParams, + codeVerifier, + ApiId.acquireTokenRedirect, + this.config, authClient, this.browserStorage, - cachedRequest, + this.nativeStorage, + this.eventHandler, this.logger, - this.performanceClient + this.performanceClient, + this.platformAuthProvider ); - return interactionHandler.handleCodeResponse(serverParams, state); + } + + /** + * Redirects window to given URL. + * @param urlNavigate + * @param onRedirectNavigateRequest - onRedirectNavigate callback provided on the request + */ + async initiateAuthRequest( + requestUrl: string, + onRedirectNavigateRequest?: (url: string) => boolean | void + ): Promise { + this.logger.verbose("RedirectHandler.initiateAuthRequest called"); + // Navigate if valid URL + if (requestUrl) { + this.logger.infoPii( + `RedirectHandler.initiateAuthRequest: Navigate to: ${requestUrl}` + ); + const navigationOptions: NavigationOptions = { + apiId: ApiId.acquireTokenRedirect, + timeout: this.config.system.redirectNavigationTimeout, + noHistory: false, + }; + + const onRedirectNavigate = + onRedirectNavigateRequest || + this.config.auth.onRedirectNavigate; + + // If onRedirectNavigate is implemented, invoke it and provide requestUrl + if (typeof onRedirectNavigate === "function") { + this.logger.verbose( + "RedirectHandler.initiateAuthRequest: Invoking onRedirectNavigate callback" + ); + const navigate = onRedirectNavigate(requestUrl); + + // Returning false from onRedirectNavigate will stop navigation + if (navigate !== false) { + this.logger.verbose( + "RedirectHandler.initiateAuthRequest: onRedirectNavigate did not return false, navigating" + ); + await this.navigationClient.navigateExternal( + requestUrl, + navigationOptions + ); + return; + } else { + this.logger.verbose( + "RedirectHandler.initiateAuthRequest: onRedirectNavigate returned false, stopping navigation" + ); + return; + } + } else { + // Navigate window to request URL + this.logger.verbose( + "RedirectHandler.initiateAuthRequest: Navigating window to navigate url" + ); + await this.navigationClient.navigateExternal( + requestUrl, + navigationOptions + ); + return; + } + } else { + // Throw error if request URL is empty. + this.logger.info( + "RedirectHandler.initiateAuthRequest: Navigate url is empty" + ); + throw createBrowserAuthError( + BrowserAuthErrorCodes.emptyNavigateUri + ); + } } /** @@ -539,7 +687,10 @@ export class RedirectClient extends StandardInteractionClient { ); // Clear cache on logout - await this.clearCacheOnLogout(validLogoutRequest.account); + await this.clearCacheOnLogout( + this.correlationId, + validLogoutRequest.account + ); const navigationOptions: NavigationOptions = { apiId: ApiId.logout, @@ -566,8 +717,9 @@ export class RedirectClient extends StandardInteractionClient { authClient.authority.endSessionEndpoint; } catch { if (validLogoutRequest.account?.homeAccountId) { - void this.browserStorage.removeAccount( - validLogoutRequest.account?.homeAccountId + this.browserStorage.removeAccount( + validLogoutRequest.account?.homeAccountId, + this.correlationId ); this.eventHandler.emitEvent( @@ -603,7 +755,10 @@ export class RedirectClient extends StandardInteractionClient { ); // Ensure interaction is in progress if (!this.browserStorage.getInteractionInProgress()) { - this.browserStorage.setInteractionInProgress(true); + this.browserStorage.setInteractionInProgress( + true, + INTERACTION_TYPE.SIGNOUT + ); } await this.navigationClient.navigateExternal( logoutUri, @@ -620,7 +775,10 @@ export class RedirectClient extends StandardInteractionClient { } else { // Ensure interaction is in progress if (!this.browserStorage.getInteractionInProgress()) { - this.browserStorage.setInteractionInProgress(true); + this.browserStorage.setInteractionInProgress( + true, + INTERACTION_TYPE.SIGNOUT + ); } await this.navigationClient.navigateExternal( logoutUri, diff --git a/lib/msal-browser/src/interaction_client/SilentAuthCodeClient.ts b/lib/msal-browser/src/interaction_client/SilentAuthCodeClient.ts index 03af623e61..53da018ba4 100644 --- a/lib/msal-browser/src/interaction_client/SilentAuthCodeClient.ts +++ b/lib/msal-browser/src/interaction_client/SilentAuthCodeClient.ts @@ -11,9 +11,9 @@ import { IPerformanceClient, PerformanceEvents, invokeAsync, + CommonAuthorizationUrlRequest, } from "@azure/msal-common/browser"; import { StandardInteractionClient } from "./StandardInteractionClient.js"; -import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js"; import { BrowserConfiguration } from "../config/Configuration.js"; import { BrowserCacheManager } from "../cache/BrowserCacheManager.js"; import { EventHandler } from "../event/EventHandler.js"; @@ -25,9 +25,9 @@ import { import { InteractionType, ApiId } from "../utils/BrowserConstants.js"; import { AuthorizationCodeRequest } from "../request/AuthorizationCodeRequest.js"; import { HybridSpaAuthorizationCodeClient } from "./HybridSpaAuthorizationCodeClient.js"; -import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; import { InteractionHandler } from "../interaction_handler/InteractionHandler.js"; +import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js"; export class SilentAuthCodeClient extends StandardInteractionClient { private apiId: ApiId; @@ -41,7 +41,7 @@ export class SilentAuthCodeClient extends StandardInteractionClient { navigationClient: INavigationClient, apiId: ApiId, performanceClient: IPerformanceClient, - nativeMessageHandler?: NativeMessageHandler, + platformAuthProvider?: IPlatformAuthHandler, correlationId?: string ) { super( @@ -52,7 +52,7 @@ export class SilentAuthCodeClient extends StandardInteractionClient { eventHandler, navigationClient, performanceClient, - nativeMessageHandler, + platformAuthProvider, correlationId ); this.apiId = apiId; @@ -73,7 +73,7 @@ export class SilentAuthCodeClient extends StandardInteractionClient { } // Create silent request - const silentRequest: AuthorizationUrlRequest = await invokeAsync( + const silentRequest: CommonAuthorizationUrlRequest = await invokeAsync( this.initializeAuthorizationRequest.bind(this), PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest, this.logger, diff --git a/lib/msal-browser/src/interaction_client/SilentCacheClient.ts b/lib/msal-browser/src/interaction_client/SilentCacheClient.ts index 8f3f606a73..e707b87b75 100644 --- a/lib/msal-browser/src/interaction_client/SilentCacheClient.ts +++ b/lib/msal-browser/src/interaction_client/SilentCacheClient.ts @@ -90,6 +90,9 @@ export class SilentCacheClient extends StandardInteractionClient { logout(logoutRequest?: ClearCacheRequest): Promise { this.logger.verbose("logoutRedirect called"); const validLogoutRequest = this.initializeLogoutRequest(logoutRequest); - return this.clearCacheOnLogout(validLogoutRequest?.account); + return this.clearCacheOnLogout( + validLogoutRequest.correlationId, + validLogoutRequest?.account + ); } } diff --git a/lib/msal-browser/src/interaction_client/SilentIframeClient.ts b/lib/msal-browser/src/interaction_client/SilentIframeClient.ts index 7229e782ca..aa51d78fd6 100644 --- a/lib/msal-browser/src/interaction_client/SilentIframeClient.ts +++ b/lib/msal-browser/src/interaction_client/SilentIframeClient.ts @@ -7,17 +7,16 @@ import { ICrypto, Logger, PromptValue, - CommonAuthorizationCodeRequest, AuthorizationCodeClient, AuthError, - ProtocolUtils, IPerformanceClient, PerformanceEvents, invokeAsync, invoke, + ProtocolMode, + CommonAuthorizationUrlRequest, } from "@azure/msal-common/browser"; import { StandardInteractionClient } from "./StandardInteractionClient.js"; -import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js"; import { BrowserConfiguration } from "../config/Configuration.js"; import { BrowserCacheManager } from "../cache/BrowserCacheManager.js"; import { EventHandler } from "../event/EventHandler.js"; @@ -32,16 +31,19 @@ import { BrowserConstants, } from "../utils/BrowserConstants.js"; import { - initiateAuthRequest, + initiateCodeRequest, + initiateEarRequest, monitorIframeForHash, } from "../interaction_handler/SilentHandler.js"; import { SsoSilentRequest } from "../request/SsoSilentRequest.js"; -import { NativeMessageHandler } from "../broker/nativeBroker/NativeMessageHandler.js"; -import { NativeInteractionClient } from "./NativeInteractionClient.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; -import { InteractionHandler } from "../interaction_handler/InteractionHandler.js"; import * as BrowserUtils from "../utils/BrowserUtils.js"; import * as ResponseHandler from "../response/ResponseHandler.js"; +import * as Authorize from "../protocol/Authorize.js"; +import { generatePkceCodes } from "../crypto/PkceGenerator.js"; +import { isPlatformAuthAllowed } from "../broker/nativeBroker/PlatformAuthProvider.js"; +import { generateEarKey } from "../crypto/BrowserCrypto.js"; +import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js"; export class SilentIframeClient extends StandardInteractionClient { protected apiId: ApiId; @@ -57,7 +59,7 @@ export class SilentIframeClient extends StandardInteractionClient { apiId: ApiId, performanceClient: IPerformanceClient, nativeStorageImpl: BrowserCacheManager, - nativeMessageHandler?: NativeMessageHandler, + platformAuthProvider?: IPlatformAuthHandler, correlationId?: string ) { super( @@ -68,7 +70,7 @@ export class SilentIframeClient extends StandardInteractionClient { eventHandler, navigationClient, performanceClient, - nativeMessageHandler, + platformAuthProvider, correlationId ); this.apiId = apiId; @@ -114,21 +116,41 @@ export class SilentIframeClient extends StandardInteractionClient { } // Create silent request - const silentRequest: AuthorizationUrlRequest = await invokeAsync( + const silentRequest: CommonAuthorizationUrlRequest = await invokeAsync( this.initializeAuthorizationRequest.bind(this), PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest, this.logger, this.performanceClient, request.correlationId )(inputRequest, InteractionType.Silent); + silentRequest.platformBroker = isPlatformAuthAllowed( + this.config, + this.logger, + this.platformAuthProvider, + silentRequest.authenticationScheme + ); BrowserUtils.preconnect(silentRequest.authority); + if (this.config.auth.protocolMode === ProtocolMode.EAR) { + return this.executeEarFlow(silentRequest); + } else { + return this.executeCodeFlow(silentRequest); + } + } + + /** + * Executes auth code + PKCE flow + * @param request + * @returns + */ + async executeCodeFlow( + request: CommonAuthorizationUrlRequest + ): Promise { + let authClient: AuthorizationCodeClient | undefined; const serverTelemetryManager = this.initializeServerTelemetryManager( this.apiId ); - let authClient: AuthorizationCodeClient | undefined; - try { // Initialize the client authClient = await invokeAsync( @@ -139,10 +161,10 @@ export class SilentIframeClient extends StandardInteractionClient { request.correlationId )({ serverTelemetryManager, - requestAuthority: silentRequest.authority, - requestAzureCloudOptions: silentRequest.azureCloudOptions, - requestExtraQueryParameters: silentRequest.extraQueryParameters, - account: silentRequest.account, + requestAuthority: request.authority, + requestAzureCloudOptions: request.azureCloudOptions, + requestExtraQueryParameters: request.extraQueryParameters, + account: request.account, }); return await invokeAsync( @@ -151,7 +173,7 @@ export class SilentIframeClient extends StandardInteractionClient { this.logger, this.performanceClient, request.correlationId - )(authClient, silentRequest); + )(authClient, request); } catch (e) { if (e instanceof AuthError) { (e as AuthError).setCorrelationId(this.correlationId); @@ -173,25 +195,109 @@ export class SilentIframeClient extends StandardInteractionClient { this.correlationId ); - const retrySilentRequest: AuthorizationUrlRequest = - await invokeAsync( - this.initializeAuthorizationRequest.bind(this), - PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest, - this.logger, - this.performanceClient, - request.correlationId - )(inputRequest, InteractionType.Silent); - return await invokeAsync( this.silentTokenHelper.bind(this), PerformanceEvents.SilentIframeClientTokenHelper, this.logger, this.performanceClient, this.correlationId - )(authClient, retrySilentRequest); + )(authClient, request); } } + /** + * Executes EAR flow + * @param request + */ + async executeEarFlow( + request: CommonAuthorizationUrlRequest + ): Promise { + const correlationId = request.correlationId; + const discoveredAuthority = await invokeAsync( + this.getDiscoveredAuthority.bind(this), + PerformanceEvents.StandardInteractionClientGetDiscoveredAuthority, + this.logger, + this.performanceClient, + correlationId + )({ + requestAuthority: request.authority, + requestAzureCloudOptions: request.azureCloudOptions, + requestExtraQueryParameters: request.extraQueryParameters, + account: request.account, + }); + + const earJwk = await invokeAsync( + generateEarKey, + PerformanceEvents.GenerateEarKey, + this.logger, + this.performanceClient, + correlationId + )(); + const silentRequest = { + ...request, + earJwk: earJwk, + }; + const msalFrame = await invokeAsync( + initiateEarRequest, + PerformanceEvents.SilentHandlerInitiateAuthRequest, + this.logger, + this.performanceClient, + correlationId + )( + this.config, + discoveredAuthority, + silentRequest, + this.logger, + this.performanceClient + ); + + const responseType = this.config.auth.OIDCOptions.serverResponseType; + // 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 invokeAsync( + monitorIframeForHash, + PerformanceEvents.SilentHandlerMonitorIframeForHash, + this.logger, + this.performanceClient, + correlationId + )( + msalFrame, + this.config.system.iframeHashTimeout, + this.config.system.pollIntervalMilliseconds, + this.performanceClient, + this.logger, + correlationId, + responseType + ); + + const serverParams = invoke( + ResponseHandler.deserializeResponse, + PerformanceEvents.DeserializeResponse, + this.logger, + this.performanceClient, + correlationId + )(responseString, responseType, this.logger); + + return invokeAsync( + Authorize.handleResponseEAR, + PerformanceEvents.HandleResponseEar, + this.logger, + this.performanceClient, + correlationId + )( + silentRequest, + serverParams, + this.apiId, + this.config, + discoveredAuthority, + this.browserStorage, + this.nativeStorage, + this.eventHandler, + this.logger, + this.performanceClient, + this.platformAuthProvider + ); + } + /** * Currently Unsupported */ @@ -212,52 +318,43 @@ export class SilentIframeClient extends StandardInteractionClient { */ protected async silentTokenHelper( authClient: AuthorizationCodeClient, - silentRequest: AuthorizationUrlRequest + request: CommonAuthorizationUrlRequest ): Promise { - const correlationId = silentRequest.correlationId; + const correlationId = request.correlationId; this.performanceClient.addQueueMeasurement( PerformanceEvents.SilentIframeClientTokenHelper, correlationId ); + const pkceCodes = await invokeAsync( + generatePkceCodes, + PerformanceEvents.GeneratePkceCodes, + this.logger, + this.performanceClient, + correlationId + )(this.performanceClient, this.logger, correlationId); - // Create auth code request and generate PKCE params - const authCodeRequest: CommonAuthorizationCodeRequest = - await invokeAsync( - this.initializeAuthorizationCodeRequest.bind(this), - PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest, - this.logger, - this.performanceClient, - correlationId - )(silentRequest); - + const silentRequest = { + ...request, + codeChallenge: pkceCodes.challenge, + }; // Create authorize request url const navigateUrl = await invokeAsync( - authClient.getAuthCodeUrl.bind(authClient), + Authorize.getAuthCodeRequestUrl, PerformanceEvents.GetAuthCodeUrl, this.logger, this.performanceClient, correlationId - )({ - ...silentRequest, - platformBroker: NativeMessageHandler.isPlatformBrokerAvailable( - this.config, - this.logger, - this.nativeMessageHandler, - silentRequest.authenticationScheme - ), - }); - - // Create silent handler - const interactionHandler = new InteractionHandler( - authClient, - this.browserStorage, - authCodeRequest, + )( + this.config, + authClient.authority, + silentRequest, this.logger, this.performanceClient ); + // Get the frame handle for the silent request const msalFrame = await invokeAsync( - initiateAuthRequest, + initiateCodeRequest, PerformanceEvents.SilentHandlerInitiateAuthRequest, this.logger, this.performanceClient, @@ -269,6 +366,7 @@ export class SilentIframeClient extends StandardInteractionClient { correlationId, this.config.system.navigateFrameWait ); + const responseType = this.config.auth.OIDCOptions.serverResponseType; // 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 invokeAsync( @@ -291,58 +389,28 @@ export class SilentIframeClient extends StandardInteractionClient { PerformanceEvents.DeserializeResponse, this.logger, this.performanceClient, - this.correlationId + correlationId )(responseString, responseType, this.logger); - if (serverParams.accountId) { - this.logger.verbose( - "Account id found in hash, calling WAM for token" - ); - if (!this.nativeMessageHandler) { - throw createBrowserAuthError( - BrowserAuthErrorCodes.nativeConnectionNotEstablished - ); - } - const nativeInteractionClient = new NativeInteractionClient( - this.config, - this.browserStorage, - this.browserCrypto, - this.logger, - this.eventHandler, - this.navigationClient, - this.apiId, - this.performanceClient, - this.nativeMessageHandler, - serverParams.accountId, - this.browserStorage, - correlationId - ); - const { userRequestState } = ProtocolUtils.parseRequestState( - this.browserCrypto, - silentRequest.state - ); - return invokeAsync( - nativeInteractionClient.acquireToken.bind( - nativeInteractionClient - ), - PerformanceEvents.NativeInteractionClientAcquireToken, - this.logger, - this.performanceClient, - correlationId - )({ - ...silentRequest, - state: userRequestState, - prompt: silentRequest.prompt || PromptValue.NONE, - }); - } - - // Handle response from hash string return invokeAsync( - interactionHandler.handleCodeResponse.bind(interactionHandler), - PerformanceEvents.HandleCodeResponse, + Authorize.handleResponseCode, + PerformanceEvents.HandleResponseCode, this.logger, this.performanceClient, correlationId - )(serverParams, silentRequest); + )( + request, + serverParams, + pkceCodes.verifier, + this.apiId, + this.config, + authClient, + this.browserStorage, + this.nativeStorage, + this.eventHandler, + this.logger, + this.performanceClient, + this.platformAuthProvider + ); } } diff --git a/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts b/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts index 5ac536e11b..6a4cf8d4b0 100644 --- a/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts +++ b/lib/msal-browser/src/interaction_client/StandardInteractionClient.ts @@ -5,7 +5,6 @@ import { ServerTelemetryManager, - CommonAuthorizationCodeRequest, Constants, AuthorizationCodeClient, ClientConfiguration, @@ -20,10 +19,9 @@ import { invokeAsync, BaseAuthRequest, StringDict, - PkceCodes, + CommonAuthorizationUrlRequest, } from "@azure/msal-common/browser"; import { BaseInteractionClient } from "./BaseInteractionClient.js"; -import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js"; import { BrowserConstants, InteractionType, @@ -35,7 +33,6 @@ import * as BrowserUtils from "../utils/BrowserUtils.js"; import { RedirectRequest } from "../request/RedirectRequest.js"; import { PopupRequest } from "../request/PopupRequest.js"; import { SsoSilentRequest } from "../request/SsoSilentRequest.js"; -import { generatePkceCodes } from "../crypto/PkceGenerator.js"; import { createNewGuid } from "../crypto/BrowserCrypto.js"; import { initializeBaseRequest } from "../request/RequestHelpers.js"; @@ -43,43 +40,6 @@ import { initializeBaseRequest } from "../request/RequestHelpers.js"; * Defines the class structure and helper functions used by the "standard", non-brokered auth flows (popup, redirect, silent (RT), silent (iframe)) */ export abstract class StandardInteractionClient extends BaseInteractionClient { - /** - * Generates an auth code request tied to the url request. - * @param request - * @param pkceCodes - */ - protected async initializeAuthorizationCodeRequest( - request: AuthorizationUrlRequest, - pkceCodes?: PkceCodes - ): Promise { - this.performanceClient.addQueueMeasurement( - PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest, - this.correlationId - ); - - const generatedPkceParams: PkceCodes = - pkceCodes || - (await invokeAsync( - generatePkceCodes, - PerformanceEvents.GeneratePkceCodes, - this.logger, - this.performanceClient, - this.correlationId - )(this.performanceClient, this.logger, this.correlationId)); - - const authCodeRequest: CommonAuthorizationCodeRequest = { - ...request, - redirectUri: request.redirectUri, - code: Constants.EMPTY_STRING, - codeVerifier: generatedPkceParams.verifier, - }; - - request.codeChallenge = generatedPkceParams.challenge; - request.codeChallengeMethod = Constants.S256_CODE_CHALLENGE_METHOD; - - return authCodeRequest; - } - /** * Initializer for the logout request. * @param logoutRequest @@ -330,7 +290,7 @@ export abstract class StandardInteractionClient extends BaseInteractionClient { protected async initializeAuthorizationRequest( request: RedirectRequest | PopupRequest | SsoSilentRequest, interactionType: InteractionType - ): Promise { + ): Promise { this.performanceClient.addQueueMeasurement( PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest, this.correlationId @@ -359,7 +319,7 @@ export abstract class StandardInteractionClient extends BaseInteractionClient { this.logger ); - const validatedRequest: AuthorizationUrlRequest = { + const validatedRequest: CommonAuthorizationUrlRequest = { ...baseRequest, redirectUri: redirectUri, state: state, @@ -374,7 +334,8 @@ export abstract class StandardInteractionClient extends BaseInteractionClient { } const account = - request.account || this.browserStorage.getActiveAccount(); + request.account || + this.browserStorage.getActiveAccount(this.correlationId); if (account) { this.logger.verbose( "Setting validated request account", diff --git a/lib/msal-browser/src/interaction_handler/InteractionHandler.ts b/lib/msal-browser/src/interaction_handler/InteractionHandler.ts index efb8850341..2b033625be 100644 --- a/lib/msal-browser/src/interaction_handler/InteractionHandler.ts +++ b/lib/msal-browser/src/interaction_handler/InteractionHandler.ts @@ -14,7 +14,9 @@ import { PerformanceEvents, invokeAsync, CcsCredentialType, - ServerAuthorizationCodeResponse, + AuthorizeResponse, + AuthorizeProtocol, + CommonAuthorizationUrlRequest, } from "@azure/msal-common/browser"; import { BrowserCacheManager } from "../cache/BrowserCacheManager.js"; @@ -23,7 +25,6 @@ import { BrowserAuthErrorCodes, } from "../error/BrowserAuthError.js"; import { AuthenticationResult } from "../response/AuthenticationResult.js"; -import { AuthorizationUrlRequest } from "../request/AuthorizationUrlRequest.js"; /** * Abstract class which defines operations for a browser interaction handling class. @@ -54,8 +55,8 @@ export class InteractionHandler { * @param locationHash */ async handleCodeResponse( - response: ServerAuthorizationCodeResponse, - request: AuthorizationUrlRequest + response: AuthorizeResponse, + request: CommonAuthorizationUrlRequest ): Promise { this.performanceClient.addQueueMeasurement( PerformanceEvents.HandleCodeResponse, @@ -64,7 +65,7 @@ export class InteractionHandler { let authCodeResponse; try { - authCodeResponse = this.authModule.handleFragmentResponse( + authCodeResponse = AuthorizeProtocol.getAuthorizationCodePayload( response, request.state ); @@ -101,7 +102,7 @@ export class InteractionHandler { */ async handleCodeResponseFromServer( authCodeResponse: AuthorizationCodePayload, - request: AuthorizationUrlRequest, + request: CommonAuthorizationUrlRequest, validateNonce: boolean = true ): Promise { this.performanceClient.addQueueMeasurement( @@ -159,7 +160,7 @@ export class InteractionHandler { * Build ccs creds if available */ protected createCcsCredentials( - request: AuthorizationUrlRequest + request: CommonAuthorizationUrlRequest ): CcsCredential | null { if (request.account) { return { diff --git a/lib/msal-browser/src/interaction_handler/RedirectHandler.ts b/lib/msal-browser/src/interaction_handler/RedirectHandler.ts deleted file mode 100644 index 8362eaddd1..0000000000 --- a/lib/msal-browser/src/interaction_handler/RedirectHandler.ts +++ /dev/null @@ -1,251 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { - AuthorizationCodeClient, - CommonAuthorizationCodeRequest, - Logger, - ServerError, - IPerformanceClient, - createClientAuthError, - ClientAuthErrorCodes, - CcsCredential, - invokeAsync, - PerformanceEvents, - ServerAuthorizationCodeResponse, -} from "@azure/msal-common/browser"; -import { - createBrowserAuthError, - BrowserAuthErrorCodes, -} from "../error/BrowserAuthError.js"; -import { ApiId, TemporaryCacheKeys } from "../utils/BrowserConstants.js"; -import { BrowserCacheManager } from "../cache/BrowserCacheManager.js"; -import { INavigationClient } from "../navigation/INavigationClient.js"; -import { NavigationOptions } from "../navigation/NavigationOptions.js"; -import { AuthenticationResult } from "../response/AuthenticationResult.js"; - -export type RedirectParams = { - navigationClient: INavigationClient; - redirectTimeout: number; - redirectStartPage: string; - onRedirectNavigate?: (url: string) => void | boolean; -}; - -export class RedirectHandler { - authModule: AuthorizationCodeClient; - browserStorage: BrowserCacheManager; - authCodeRequest: CommonAuthorizationCodeRequest; - logger: Logger; - performanceClient: IPerformanceClient; - - constructor( - authCodeModule: AuthorizationCodeClient, - storageImpl: BrowserCacheManager, - authCodeRequest: CommonAuthorizationCodeRequest, - logger: Logger, - performanceClient: IPerformanceClient - ) { - this.authModule = authCodeModule; - this.browserStorage = storageImpl; - this.authCodeRequest = authCodeRequest; - this.logger = logger; - this.performanceClient = performanceClient; - } - - /** - * Redirects window to given URL. - * @param urlNavigate - */ - async initiateAuthRequest( - requestUrl: string, - params: RedirectParams - ): Promise { - this.logger.verbose("RedirectHandler.initiateAuthRequest called"); - // Navigate if valid URL - if (requestUrl) { - // Cache start page, returns to this page after redirectUri if navigateToLoginRequestUrl is true - if (params.redirectStartPage) { - this.logger.verbose( - "RedirectHandler.initiateAuthRequest: redirectStartPage set, caching start page" - ); - this.browserStorage.setTemporaryCache( - TemporaryCacheKeys.ORIGIN_URI, - params.redirectStartPage, - true - ); - } - - // Set interaction status in the library. - this.browserStorage.setTemporaryCache( - TemporaryCacheKeys.CORRELATION_ID, - this.authCodeRequest.correlationId, - true - ); - this.browserStorage.cacheCodeRequest(this.authCodeRequest); - this.logger.infoPii( - `RedirectHandler.initiateAuthRequest: Navigate to: ${requestUrl}` - ); - const navigationOptions: NavigationOptions = { - apiId: ApiId.acquireTokenRedirect, - timeout: params.redirectTimeout, - noHistory: false, - }; - - // If onRedirectNavigate is implemented, invoke it and provide requestUrl - if (typeof params.onRedirectNavigate === "function") { - this.logger.verbose( - "RedirectHandler.initiateAuthRequest: Invoking onRedirectNavigate callback" - ); - const navigate = params.onRedirectNavigate(requestUrl); - - // Returning false from onRedirectNavigate will stop navigation - if (navigate !== false) { - this.logger.verbose( - "RedirectHandler.initiateAuthRequest: onRedirectNavigate did not return false, navigating" - ); - await params.navigationClient.navigateExternal( - requestUrl, - navigationOptions - ); - return; - } else { - this.logger.verbose( - "RedirectHandler.initiateAuthRequest: onRedirectNavigate returned false, stopping navigation" - ); - return; - } - } else { - // Navigate window to request URL - this.logger.verbose( - "RedirectHandler.initiateAuthRequest: Navigating window to navigate url" - ); - await params.navigationClient.navigateExternal( - requestUrl, - navigationOptions - ); - return; - } - } else { - // Throw error if request URL is empty. - this.logger.info( - "RedirectHandler.initiateAuthRequest: Navigate url is empty" - ); - throw createBrowserAuthError( - BrowserAuthErrorCodes.emptyNavigateUri - ); - } - } - - /** - * Handle authorization code response in the window. - * @param hash - */ - async handleCodeResponse( - response: ServerAuthorizationCodeResponse, - state: string - ): Promise { - this.logger.verbose("RedirectHandler.handleCodeResponse called"); - - // Interaction is completed - remove interaction status. - this.browserStorage.setInteractionInProgress(false); - - // Handle code response. - const stateKey = this.browserStorage.generateStateKey(state); - const requestState = this.browserStorage.getTemporaryCache(stateKey); - if (!requestState) { - throw createClientAuthError( - ClientAuthErrorCodes.stateNotFound, - "Cached State" - ); - } - - let authCodeResponse; - try { - authCodeResponse = this.authModule.handleFragmentResponse( - response, - requestState - ); - } catch (e) { - if ( - e instanceof ServerError && - e.subError === BrowserAuthErrorCodes.userCancelled - ) { - // Translate server error caused by user closing native prompt to corresponding first class MSAL error - throw createBrowserAuthError( - BrowserAuthErrorCodes.userCancelled - ); - } else { - throw e; - } - } - - // Get cached items - const nonceKey = this.browserStorage.generateNonceKey(requestState); - const cachedNonce = this.browserStorage.getTemporaryCache(nonceKey); - - // Assign code to request - this.authCodeRequest.code = authCodeResponse.code; - - // Check for new cloud instance - if (authCodeResponse.cloud_instance_host_name) { - await invokeAsync( - this.authModule.updateAuthority.bind(this.authModule), - PerformanceEvents.UpdateTokenEndpointAuthority, - this.logger, - this.performanceClient, - this.authCodeRequest.correlationId - )( - authCodeResponse.cloud_instance_host_name, - this.authCodeRequest.correlationId - ); - } - - authCodeResponse.nonce = cachedNonce || undefined; - authCodeResponse.state = requestState; - - // Add CCS parameters if available - if (authCodeResponse.client_info) { - this.authCodeRequest.clientInfo = authCodeResponse.client_info; - } else { - const cachedCcsCred = this.checkCcsCredentials(); - if (cachedCcsCred) { - this.authCodeRequest.ccsCredential = cachedCcsCred; - } - } - - // Acquire token with retrieved code. - const tokenResponse = (await this.authModule.acquireToken( - this.authCodeRequest, - authCodeResponse - )) as AuthenticationResult; - - this.browserStorage.cleanRequestByState(state); - return tokenResponse; - } - - /** - * Looks up ccs creds in the cache - */ - protected checkCcsCredentials(): CcsCredential | null { - // Look up ccs credential in temp cache - const cachedCcsCred = this.browserStorage.getTemporaryCache( - TemporaryCacheKeys.CCS_CREDENTIAL, - true - ); - if (cachedCcsCred) { - try { - return JSON.parse(cachedCcsCred) as CcsCredential; - } catch (e) { - this.authModule.logger.error( - "Cache credential could not be parsed" - ); - this.authModule.logger.errorPii( - `Cache credential could not be parsed: ${cachedCcsCred}` - ); - } - } - return null; - } -} diff --git a/lib/msal-browser/src/interaction_handler/SilentHandler.ts b/lib/msal-browser/src/interaction_handler/SilentHandler.ts index 8479879124..e7e9c55d66 100644 --- a/lib/msal-browser/src/interaction_handler/SilentHandler.ts +++ b/lib/msal-browser/src/interaction_handler/SilentHandler.ts @@ -10,19 +10,25 @@ import { invokeAsync, invoke, ServerResponseType, + Authority, + CommonAuthorizationUrlRequest, } from "@azure/msal-common/browser"; import { createBrowserAuthError, BrowserAuthErrorCodes, } from "../error/BrowserAuthError.js"; -import { DEFAULT_IFRAME_TIMEOUT_MS } from "../config/Configuration.js"; +import { + BrowserConfiguration, + DEFAULT_IFRAME_TIMEOUT_MS, +} from "../config/Configuration.js"; +import { getEARForm } from "../protocol/Authorize.js"; /** * Creates a hidden iframe to given URL using user-requested scopes as an id. * @param urlNavigate * @param userRequestScopes */ -export async function initiateAuthRequest( +export async function initiateCodeRequest( requestUrl: string, performanceClient: IPerformanceClient, logger: Logger, @@ -57,6 +63,29 @@ export async function initiateAuthRequest( )(requestUrl); } +export async function initiateEarRequest( + config: BrowserConfiguration, + authority: Authority, + request: CommonAuthorizationUrlRequest, + logger: Logger, + performanceClient: IPerformanceClient +): Promise { + const frame = createHiddenIframe(); + if (!frame.contentDocument) { + throw "No document associated with iframe!"; + } + const form = await getEARForm( + frame.contentDocument, + config, + authority, + request, + logger, + performanceClient + ); + form.submit(); + return frame; +} + /** * Monitors an iframe content window until it loads a url with a known hash, or hits a specified timeout. * @param iframe diff --git a/lib/msal-browser/src/navigation/NavigationClient.ts b/lib/msal-browser/src/navigation/NavigationClient.ts index 07af57fe7c..a9ff74766d 100644 --- a/lib/msal-browser/src/navigation/NavigationClient.ts +++ b/lib/msal-browser/src/navigation/NavigationClient.ts @@ -3,6 +3,10 @@ * Licensed under the MIT License. */ +import { + BrowserAuthErrorCodes, + createBrowserAuthError, +} from "../error/BrowserAuthError.js"; import { INavigationClient } from "./INavigationClient.js"; import { NavigationOptions } from "./NavigationOptions.js"; @@ -41,14 +45,19 @@ export class NavigationClient implements INavigationClient { options: NavigationOptions ): Promise { if (options.noHistory) { - window.location.replace(url); + window.location.replace(url); // CodeQL [SM03712] Application owner controls the URL. User can't change it. } else { - window.location.assign(url); + window.location.assign(url); // CodeQL [SM03712] Application owner controls the URL. User can't change it. } - return new Promise((resolve) => { + return new Promise((resolve, reject) => { setTimeout(() => { - resolve(true); + reject( + createBrowserAuthError( + BrowserAuthErrorCodes.timedOut, + "failed_to_redirect" + ) + ); }, options.timeout); }); } diff --git a/lib/msal-browser/src/network/FetchClient.ts b/lib/msal-browser/src/network/FetchClient.ts index 7121694d84..9aaa5d5498 100644 --- a/lib/msal-browser/src/network/FetchClient.ts +++ b/lib/msal-browser/src/network/FetchClient.ts @@ -39,10 +39,15 @@ export class FetchClient implements INetworkModule { headers: reqHeaders, }); } catch (e) { - throw createBrowserAuthError( - window.navigator.onLine - ? BrowserAuthErrorCodes.getRequestFailed - : BrowserAuthErrorCodes.noNetworkConnectivity + throw createNetworkError( + createBrowserAuthError( + window.navigator.onLine + ? BrowserAuthErrorCodes.getRequestFailed + : BrowserAuthErrorCodes.noNetworkConnectivity + ), + undefined, + undefined, + e as Error ); } @@ -60,7 +65,8 @@ export class FetchClient implements INetworkModule { BrowserAuthErrorCodes.failedToParseResponse ), responseStatus, - responseHeaders + responseHeaders, + e as Error ); } } @@ -88,10 +94,15 @@ export class FetchClient implements INetworkModule { body: reqBody, }); } catch (e) { - throw createBrowserAuthError( - window.navigator.onLine - ? BrowserAuthErrorCodes.postRequestFailed - : BrowserAuthErrorCodes.noNetworkConnectivity + throw createNetworkError( + createBrowserAuthError( + window.navigator.onLine + ? BrowserAuthErrorCodes.postRequestFailed + : BrowserAuthErrorCodes.noNetworkConnectivity + ), + undefined, + undefined, + e as Error ); } @@ -109,7 +120,8 @@ export class FetchClient implements INetworkModule { BrowserAuthErrorCodes.failedToParseResponse ), responseStatus, - responseHeaders + responseHeaders, + e as Error ); } } @@ -131,8 +143,11 @@ function getFetchHeaders(options?: NetworkRequestOptions): Headers { }); return headers; } catch (e) { - throw createBrowserAuthError( - BrowserAuthErrorCodes.failedToBuildHeaders + throw createNetworkError( + createBrowserAuthError(BrowserAuthErrorCodes.failedToBuildHeaders), + undefined, + undefined, + e as Error ); } } diff --git a/lib/msal-browser/src/packageMetadata.ts b/lib/msal-browser/src/packageMetadata.ts index 05c2981dfa..f1215c29b8 100644 --- a/lib/msal-browser/src/packageMetadata.ts +++ b/lib/msal-browser/src/packageMetadata.ts @@ -1,3 +1,3 @@ /* eslint-disable header/header */ export const name = "@azure/msal-browser"; -export const version = "4.7.0"; +export const version = "4.15.0"; diff --git a/lib/msal-browser/src/protocol/Authorize.ts b/lib/msal-browser/src/protocol/Authorize.ts new file mode 100644 index 0000000000..a1d7c30313 --- /dev/null +++ b/lib/msal-browser/src/protocol/Authorize.ts @@ -0,0 +1,502 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + AuthenticationScheme, + Authority, + AuthorizeProtocol, + ClientConfigurationErrorCodes, + CommonAuthorizationUrlRequest, + createClientConfigurationError, + invokeAsync, + IPerformanceClient, + Logger, + PerformanceEvents, + PopTokenGenerator, + ProtocolMode, + RequestParameterBuilder, + OAuthResponseType, + Constants, + CommonAuthorizationCodeRequest, + AuthorizationCodeClient, + ProtocolUtils, + ThrottlingUtils, + AuthorizeResponse, + ResponseHandler, + TimeUtils, + AuthorizationCodePayload, + ServerAuthorizationTokenResponse, +} from "@azure/msal-common/browser"; +import { BrowserConfiguration } from "../config/Configuration.js"; +import { ApiId, BrowserConstants } from "../utils/BrowserConstants.js"; +import { version } from "../packageMetadata.js"; +import { CryptoOps } from "../crypto/CryptoOps.js"; +import { + BrowserAuthErrorCodes, + createBrowserAuthError, +} from "../error/BrowserAuthError.js"; +import { AuthenticationResult } from "../response/AuthenticationResult.js"; +import { InteractionHandler } from "../interaction_handler/InteractionHandler.js"; +import { BrowserCacheManager } from "../cache/BrowserCacheManager.js"; +import { PlatformAuthInteractionClient } from "../interaction_client/PlatformAuthInteractionClient.js"; +import { EventHandler } from "../event/EventHandler.js"; +import { decryptEarResponse } from "../crypto/BrowserCrypto.js"; +import { IPlatformAuthHandler } from "../broker/nativeBroker/IPlatformAuthHandler.js"; + +/** + * Returns map of parameters that are applicable to all calls to /authorize whether using PKCE or EAR + * @param config + * @param authority + * @param request + * @param logger + * @param performanceClient + * @returns + */ +async function getStandardParameters( + config: BrowserConfiguration, + authority: Authority, + request: CommonAuthorizationUrlRequest, + logger: Logger, + performanceClient: IPerformanceClient +): Promise> { + const parameters = AuthorizeProtocol.getStandardAuthorizeRequestParameters( + { ...config.auth, authority: authority }, + request, + logger, + performanceClient + ); + RequestParameterBuilder.addLibraryInfo(parameters, { + sku: BrowserConstants.MSAL_SKU, + version: version, + os: "", + cpu: "", + }); + if (config.auth.protocolMode !== ProtocolMode.OIDC) { + RequestParameterBuilder.addApplicationTelemetry( + parameters, + config.telemetry.application + ); + } + + if (request.platformBroker) { + // signal ests that this is a WAM call + RequestParameterBuilder.addNativeBroker(parameters); + + // pass the req_cnf for POP + if (request.authenticationScheme === AuthenticationScheme.POP) { + const cryptoOps = new CryptoOps(logger, performanceClient); + const popTokenGenerator = new PopTokenGenerator(cryptoOps); + + // req_cnf is always sent as a string for SPAs + let reqCnfData; + if (!request.popKid) { + const generatedReqCnfData = await invokeAsync( + popTokenGenerator.generateCnf.bind(popTokenGenerator), + PerformanceEvents.PopTokenGenerateCnf, + logger, + performanceClient, + request.correlationId + )(request, logger); + reqCnfData = generatedReqCnfData.reqCnfString; + } else { + reqCnfData = cryptoOps.encodeKid(request.popKid); + } + RequestParameterBuilder.addPopToken(parameters, reqCnfData); + } + } + + RequestParameterBuilder.instrumentBrokerParams( + parameters, + request.correlationId, + performanceClient + ); + + return parameters; +} + +/** + * Gets the full /authorize URL with request parameters when using Auth Code + PKCE + * @param config + * @param authority + * @param request + * @param logger + * @param performanceClient + * @returns + */ +export async function getAuthCodeRequestUrl( + config: BrowserConfiguration, + authority: Authority, + request: CommonAuthorizationUrlRequest, + logger: Logger, + performanceClient: IPerformanceClient +): Promise { + if (!request.codeChallenge) { + throw createClientConfigurationError( + ClientConfigurationErrorCodes.pkceParamsMissing + ); + } + + const parameters = await invokeAsync( + getStandardParameters, + PerformanceEvents.GetStandardParams, + logger, + performanceClient, + request.correlationId + )(config, authority, request, logger, performanceClient); + RequestParameterBuilder.addResponseType(parameters, OAuthResponseType.CODE); + + RequestParameterBuilder.addCodeChallengeParams( + parameters, + request.codeChallenge, + Constants.S256_CODE_CHALLENGE_METHOD + ); + + RequestParameterBuilder.addExtraQueryParameters( + parameters, + request.extraQueryParameters || {} + ); + + return AuthorizeProtocol.getAuthorizeUrl( + authority, + parameters, + config.auth.encodeExtraQueryParams, + request.extraQueryParameters + ); +} + +/** + * Gets the form that will be posted to /authorize with request parameters when using EAR + */ +export async function getEARForm( + frame: Document, + config: BrowserConfiguration, + authority: Authority, + request: CommonAuthorizationUrlRequest, + logger: Logger, + performanceClient: IPerformanceClient +): Promise { + if (!request.earJwk) { + throw createBrowserAuthError(BrowserAuthErrorCodes.earJwkEmpty); + } + + const parameters = await getStandardParameters( + config, + authority, + request, + logger, + performanceClient + ); + + RequestParameterBuilder.addResponseType( + parameters, + OAuthResponseType.IDTOKEN_TOKEN_REFRESHTOKEN + ); + RequestParameterBuilder.addEARParameters(parameters, request.earJwk); + + const queryParams = new Map(); + RequestParameterBuilder.addExtraQueryParameters( + queryParams, + request.extraQueryParameters || {} + ); + const url = AuthorizeProtocol.getAuthorizeUrl( + authority, + queryParams, + config.auth.encodeExtraQueryParams, + request.extraQueryParameters + ); + + return createForm(frame, url, parameters); +} + +/** + * Creates form element in the provided document with auth parameters in the post body + * @param frame + * @param authorizeUrl + * @param parameters + * @returns + */ +function createForm( + frame: Document, + authorizeUrl: string, + parameters: Map +): HTMLFormElement { + const form = frame.createElement("form"); + form.method = "post"; + form.action = authorizeUrl; + + parameters.forEach((value: string, key: string) => { + const param = frame.createElement("input"); + param.hidden = true; + param.name = key; + param.value = value; + + form.appendChild(param); + }); + + frame.body.appendChild(form); + return form; +} + +/** + * Response handler when server returns accountId on the /authorize request + * @param request + * @param accountId + * @param apiId + * @param config + * @param browserStorage + * @param nativeStorage + * @param eventHandler + * @param logger + * @param performanceClient + * @param nativeMessageHandler + * @returns + */ +export async function handleResponsePlatformBroker( + request: CommonAuthorizationUrlRequest, + accountId: string, + apiId: ApiId, + config: BrowserConfiguration, + browserStorage: BrowserCacheManager, + nativeStorage: BrowserCacheManager, + eventHandler: EventHandler, + logger: Logger, + performanceClient: IPerformanceClient, + platformAuthProvider?: IPlatformAuthHandler +): Promise { + logger.verbose("Account id found, calling WAM for token"); + + if (!platformAuthProvider) { + throw createBrowserAuthError( + BrowserAuthErrorCodes.nativeConnectionNotEstablished + ); + } + const browserCrypto = new CryptoOps(logger, performanceClient); + const nativeInteractionClient = new PlatformAuthInteractionClient( + config, + browserStorage, + browserCrypto, + logger, + eventHandler, + config.system.navigationClient, + apiId, + performanceClient, + platformAuthProvider, + accountId, + nativeStorage, + request.correlationId + ); + const { userRequestState } = ProtocolUtils.parseRequestState( + browserCrypto, + request.state + ); + return invokeAsync( + nativeInteractionClient.acquireToken.bind(nativeInteractionClient), + PerformanceEvents.NativeInteractionClientAcquireToken, + logger, + performanceClient, + request.correlationId + )({ + ...request, + state: userRequestState, + prompt: undefined, // Server should handle the prompt, ideally native broker can do this part silently + }); +} + +/** + * Response handler when server returns code on the /authorize request + * @param request + * @param response + * @param codeVerifier + * @param authClient + * @param browserStorage + * @param logger + * @param performanceClient + * @returns + */ +export async function handleResponseCode( + request: CommonAuthorizationUrlRequest, + response: AuthorizeResponse, + codeVerifier: string, + apiId: ApiId, + config: BrowserConfiguration, + authClient: AuthorizationCodeClient, + browserStorage: BrowserCacheManager, + nativeStorage: BrowserCacheManager, + eventHandler: EventHandler, + logger: Logger, + performanceClient: IPerformanceClient, + platformAuthProvider?: IPlatformAuthHandler +): Promise { + // Remove throttle if it exists + ThrottlingUtils.removeThrottle( + browserStorage, + config.auth.clientId, + request + ); + if (response.accountId) { + return invokeAsync( + handleResponsePlatformBroker, + PerformanceEvents.HandleResponsePlatformBroker, + logger, + performanceClient, + request.correlationId + )( + request, + response.accountId, + apiId, + config, + browserStorage, + nativeStorage, + eventHandler, + logger, + performanceClient, + platformAuthProvider + ); + } + const authCodeRequest: CommonAuthorizationCodeRequest = { + ...request, + code: response.code || "", + codeVerifier: codeVerifier, + }; + // Create popup interaction handler. + const interactionHandler = new InteractionHandler( + authClient, + browserStorage, + authCodeRequest, + logger, + performanceClient + ); + // Handle response from hash string. + const result = await invokeAsync( + interactionHandler.handleCodeResponse.bind(interactionHandler), + PerformanceEvents.HandleCodeResponse, + logger, + performanceClient, + request.correlationId + )(response, request); + + return result; +} + +/** + * Response handler when server returns ear_jwe on the /authorize request + * @param request + * @param response + * @param apiId + * @param config + * @param authority + * @param browserStorage + * @param nativeStorage + * @param eventHandler + * @param logger + * @param performanceClient + * @param nativeMessageHandler + * @returns + */ +export async function handleResponseEAR( + request: CommonAuthorizationUrlRequest, + response: AuthorizeResponse, + apiId: ApiId, + config: BrowserConfiguration, + authority: Authority, + browserStorage: BrowserCacheManager, + nativeStorage: BrowserCacheManager, + eventHandler: EventHandler, + logger: Logger, + performanceClient: IPerformanceClient, + platformAuthProvider?: IPlatformAuthHandler +): Promise { + // Remove throttle if it exists + ThrottlingUtils.removeThrottle( + browserStorage, + config.auth.clientId, + request + ); + + // Validate state & check response for errors + AuthorizeProtocol.validateAuthorizationResponse(response, request.state); + + if (!response.ear_jwe) { + throw createBrowserAuthError(BrowserAuthErrorCodes.earJweEmpty); + } + + if (!request.earJwk) { + throw createBrowserAuthError(BrowserAuthErrorCodes.earJwkEmpty); + } + + const decryptedData = JSON.parse( + await invokeAsync( + decryptEarResponse, + PerformanceEvents.DecryptEarResponse, + logger, + performanceClient, + request.correlationId + )(request.earJwk, response.ear_jwe) + ) as AuthorizeResponse & ServerAuthorizationTokenResponse; + + if (decryptedData.accountId) { + return invokeAsync( + handleResponsePlatformBroker, + PerformanceEvents.HandleResponsePlatformBroker, + logger, + performanceClient, + request.correlationId + )( + request, + decryptedData.accountId, + apiId, + config, + browserStorage, + nativeStorage, + eventHandler, + logger, + performanceClient, + platformAuthProvider + ); + } + + const responseHandler = new ResponseHandler( + config.auth.clientId, + browserStorage, + new CryptoOps(logger, performanceClient), + logger, + null, + null, + performanceClient + ); + + // Validate response. This function throws a server error if an error is returned by the server. + responseHandler.validateTokenResponse(decryptedData); + + // Temporary until response handler is refactored to be more flow agnostic. + const additionalData: AuthorizationCodePayload = { + code: "", + state: request.state, + nonce: request.nonce, + client_info: decryptedData.client_info, + cloud_graph_host_name: decryptedData.cloud_graph_host_name, + cloud_instance_host_name: decryptedData.cloud_instance_host_name, + cloud_instance_name: decryptedData.cloud_instance_name, + msgraph_host: decryptedData.msgraph_host, + }; + + return (await invokeAsync( + responseHandler.handleServerTokenResponse.bind(responseHandler), + PerformanceEvents.HandleServerTokenResponse, + logger, + performanceClient, + request.correlationId + )( + decryptedData, + authority, + TimeUtils.nowSeconds(), + request, + additionalData, + undefined, + undefined, + undefined, + undefined + )) as AuthenticationResult; +} diff --git a/lib/msal-browser/src/request/AuthorizationUrlRequest.ts b/lib/msal-browser/src/request/AuthorizationUrlRequest.ts index c9ca87debc..f563e53703 100644 --- a/lib/msal-browser/src/request/AuthorizationUrlRequest.ts +++ b/lib/msal-browser/src/request/AuthorizationUrlRequest.ts @@ -7,11 +7,9 @@ import { CommonAuthorizationUrlRequest } from "@azure/msal-common/browser"; /** * This type is deprecated and will be removed on the next major version update + * @deprecated Will be removed in future version */ export type AuthorizationUrlRequest = Omit< CommonAuthorizationUrlRequest, - "state" | "nonce" | "requestedClaimsHash" | "platformBroker" -> & { - state: string; - nonce: string; -}; + "requestedClaimsHash" | "platformBroker" +>; diff --git a/lib/msal-browser/src/request/PopupRequest.ts b/lib/msal-browser/src/request/PopupRequest.ts index fe1b4dfbbc..a25a568756 100644 --- a/lib/msal-browser/src/request/PopupRequest.ts +++ b/lib/msal-browser/src/request/PopupRequest.ts @@ -40,6 +40,7 @@ export type PopupRequest = Partial< CommonAuthorizationUrlRequest, | "responseMode" | "scopes" + | "earJwk" | "codeChallenge" | "codeChallengeMethod" | "requestedClaimsHash" diff --git a/lib/msal-browser/src/request/RedirectRequest.ts b/lib/msal-browser/src/request/RedirectRequest.ts index c215867307..82236f7d88 100644 --- a/lib/msal-browser/src/request/RedirectRequest.ts +++ b/lib/msal-browser/src/request/RedirectRequest.ts @@ -38,6 +38,7 @@ export type RedirectRequest = Partial< CommonAuthorizationUrlRequest, | "responseMode" | "scopes" + | "earJwk" | "codeChallenge" | "codeChallengeMethod" | "requestedClaimsHash" diff --git a/lib/msal-browser/src/request/SsoSilentRequest.ts b/lib/msal-browser/src/request/SsoSilentRequest.ts index f531ab65c0..58ec006238 100644 --- a/lib/msal-browser/src/request/SsoSilentRequest.ts +++ b/lib/msal-browser/src/request/SsoSilentRequest.ts @@ -34,6 +34,7 @@ export type SsoSilentRequest = Partial< Omit< CommonAuthorizationUrlRequest, | "responseMode" + | "earJwk" | "codeChallenge" | "codeChallengeMethod" | "requestedClaimsHash" diff --git a/lib/msal-browser/src/response/ResponseHandler.ts b/lib/msal-browser/src/response/ResponseHandler.ts index 84e405f343..b71a456360 100644 --- a/lib/msal-browser/src/response/ResponseHandler.ts +++ b/lib/msal-browser/src/response/ResponseHandler.ts @@ -6,7 +6,7 @@ import { ICrypto, Logger, - ServerAuthorizationCodeResponse, + AuthorizeResponse, UrlUtils, } from "@azure/msal-common/browser"; import { @@ -20,7 +20,7 @@ export function deserializeResponse( responseString: string, responseLocation: string, logger: Logger -): ServerAuthorizationCodeResponse { +): AuthorizeResponse { // Deserialize hash fragment response parameters. const serverParams = UrlUtils.getDeserializedResponse(responseString); if (!serverParams) { @@ -49,7 +49,7 @@ export function deserializeResponse( * Returns the interaction type that the response object belongs to */ export function validateInteractionType( - response: ServerAuthorizationCodeResponse, + response: AuthorizeResponse, browserCrypto: ICrypto, interactionType: InteractionType ): void { diff --git a/lib/msal-browser/src/utils/BrowserConstants.ts b/lib/msal-browser/src/utils/BrowserConstants.ts index 0a21cf846d..ca2b1fb772 100644 --- a/lib/msal-browser/src/utils/BrowserConstants.ts +++ b/lib/msal-browser/src/utils/BrowserConstants.ts @@ -41,10 +41,15 @@ export const BrowserConstants = { MSAL_SKU: "msal.js.browser", }; -export const NativeConstants = { +export const PlatformAuthConstants = { CHANNEL_ID: "53ee284d-920a-4b59-9d30-a60315b26836", PREFERRED_EXTENSION_ID: "ppnbnpeolgkicgegkbkbjmhlideopiji", MATS_TELEMETRY: "MATS", + MICROSOFT_ENTRA_BROKERID: "MicrosoftEntra", + DOM_API_NAME: "DOM API", + PLATFORM_DOM_APIS: "get-token-and-sign-out", + PLATFORM_DOM_PROVIDER: "PlatformAuthDOMHandler", + PLATFORM_EXTENSION_PROVIDER: "PlatformAuthExtensionHandler", }; export const NativeExtensionMethod = { @@ -74,25 +79,23 @@ export const HTTP_REQUEST_TYPE = { export type HTTP_REQUEST_TYPE = (typeof HTTP_REQUEST_TYPE)[keyof typeof HTTP_REQUEST_TYPE]; +export const INTERACTION_TYPE = { + SIGNIN: "signin", + SIGNOUT: "signout", +} as const; +export type INTERACTION_TYPE = + (typeof INTERACTION_TYPE)[keyof typeof INTERACTION_TYPE]; + /** * Temporary cache keys for MSAL, deleted after any request. */ export const TemporaryCacheKeys = { - AUTHORITY: "authority", - ACQUIRE_TOKEN_ACCOUNT: "acquireToken.account", - SESSION_STATE: "session.state", - REQUEST_STATE: "request.state", - NONCE_IDTOKEN: "nonce.id_token", ORIGIN_URI: "request.origin", - RENEW_STATUS: "token.renew.status", URL_HASH: "urlHash", REQUEST_PARAMS: "request.params", - SCOPES: "scopes", + VERIFIER: "code.verifier", INTERACTION_STATUS_KEY: "interaction.status", - CCS_CREDENTIAL: "ccs.credential", - CORRELATION_ID: "request.correlationId", NATIVE_REQUEST: "request.native", - REDIRECT_CONTEXT: "request.redirect.context", } as const; export type TemporaryCacheKeys = (typeof TemporaryCacheKeys)[keyof typeof TemporaryCacheKeys]; @@ -100,6 +103,7 @@ export type TemporaryCacheKeys = export const StaticCacheKeys = { ACCOUNT_KEYS: "msal.account.keys", TOKEN_KEYS: "msal.token.keys", + VERSION: "msal.version", } as const; export type StaticCacheKeys = (typeof StaticCacheKeys)[keyof typeof StaticCacheKeys]; @@ -250,3 +254,5 @@ export const LOG_LEVEL_CACHE_KEY = "msal.browser.log.level"; export const LOG_PII_CACHE_KEY = "msal.browser.log.pii"; export const BROWSER_PERF_ENABLED_KEY = "msal.browser.performance.enabled"; + +export const PLATFORM_AUTH_DOM_SUPPORT = "msal.browser.platform.auth.dom"; diff --git a/lib/msal-browser/src/utils/BrowserUtils.ts b/lib/msal-browser/src/utils/BrowserUtils.ts index 34a96f5fd3..028a20b59c 100644 --- a/lib/msal-browser/src/utils/BrowserUtils.ts +++ b/lib/msal-browser/src/utils/BrowserUtils.ts @@ -14,7 +14,7 @@ import { BrowserConfigurationAuthErrorCodes, createBrowserConfigurationAuthError, } from "../error/BrowserConfigurationAuthError.js"; -import { BrowserConfiguration } from "../config/Configuration.js"; +import type { BrowserConfiguration } from "../config/Configuration.js"; /** * Clears hash from window url. diff --git a/lib/msal-browser/src/utils/MsalFrameStatsUtils.ts b/lib/msal-browser/src/utils/MsalFrameStatsUtils.ts new file mode 100644 index 0000000000..cc71305004 --- /dev/null +++ b/lib/msal-browser/src/utils/MsalFrameStatsUtils.ts @@ -0,0 +1,32 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { InProgressPerformanceEvent, Logger } from "@azure/msal-common/browser"; + +export function collectInstanceStats( + currentClientId: string, + performanceEvent: InProgressPerformanceEvent, + logger: Logger +): void { + const frameInstances: string[] = + // @ts-ignore + window.msal?.clientIds || []; + + const msalInstanceCount = frameInstances.length; + + const sameClientIdInstanceCount = frameInstances.filter( + (i) => i === currentClientId + ).length; + + if (sameClientIdInstanceCount > 1) { + logger.warning( + "There is already an instance of MSAL.js in the window with the same client id." + ); + } + performanceEvent.add({ + msalInstanceCount: msalInstanceCount, + sameClientIdInstanceCount: sameClientIdInstanceCount, + }); +} diff --git a/lib/msal-browser/test/app/PublicClientApplication.spec.ts b/lib/msal-browser/test/app/PublicClientApplication.spec.ts index 94946767a6..33c2123da1 100644 --- a/lib/msal-browser/test/app/PublicClientApplication.spec.ts +++ b/lib/msal-browser/test/app/PublicClientApplication.spec.ts @@ -5,8 +5,6 @@ import { PublicClientApplication } from "../../src/app/PublicClientApplication.js"; import { - DEFAULT_OPENID_CONFIG_RESPONSE, - DEFAULT_TENANT_DISCOVERY_RESPONSE, ID_TOKEN_ALT_CLAIMS, ID_TOKEN_CLAIMS, RANDOM_TEST_GUID, @@ -17,11 +15,10 @@ import { TEST_SSH_VALUES, TEST_STATE_VALUES, TEST_TOKEN_LIFETIMES, - TEST_TOKEN_RESPONSE, TEST_TOKENS, TEST_URIS, testLogoutUrl, - testNavUrlNoRequest, + verifyUrl, } from "../utils/StringConstants.js"; import { AccountEntity, @@ -33,7 +30,6 @@ import { CacheHelpers, CacheManager, ClientAuthErrorCodes, - CommonAuthorizationCodeRequest, CommonAuthorizationUrlRequest, CommonSilentFlowRequest, Constants, @@ -47,7 +43,6 @@ import { PerformanceClient, PerformanceEvent, PerformanceEvents, - PersistentCacheKeys, ProtocolMode, ProtocolUtils, RefreshTokenClient, @@ -64,7 +59,8 @@ import { BrowserConstants, CacheLookupPolicy, InteractionType, - NativeConstants, + StaticCacheKeys, + PlatformAuthConstants, TemporaryCacheKeys, WrapperSKU, } from "../../src/utils/BrowserConstants.js"; @@ -80,7 +76,6 @@ import { NavigationOptions } from "../../src/navigation/NavigationOptions.js"; import { EventMessage } from "../../src/event/EventMessage.js"; import { EventHandler } from "../../src/event/EventHandler.js"; import { SilentIframeClient } from "../../src/interaction_client/SilentIframeClient.js"; -import { base64Encode } from "../../src/encode/Base64Encode.js"; import { FetchClient } from "../../src/network/FetchClient.js"; import { BrowserAuthError, @@ -93,17 +88,12 @@ import { RedirectClient } from "../../src/interaction_client/RedirectClient.js"; import { PopupClient } from "../../src/interaction_client/PopupClient.js"; import { SilentCacheClient } from "../../src/interaction_client/SilentCacheClient.js"; import { SilentRefreshClient } from "../../src/interaction_client/SilentRefreshClient.js"; -import { BaseInteractionClient } from "../../src/interaction_client/BaseInteractionClient.js"; -import { - AuthorizationCodeRequest, - EndSessionRequest, -} from "../../src/index.js"; -import { RedirectHandler } from "../../src/interaction_handler/RedirectHandler.js"; import { SilentAuthCodeClient } from "../../src/interaction_client/SilentAuthCodeClient.js"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; -import { NativeMessageHandler } from "../../src/broker/nativeBroker/NativeMessageHandler.js"; -import { NativeInteractionClient } from "../../src/interaction_client/NativeInteractionClient.js"; -import { NativeTokenRequest } from "../../src/broker/nativeBroker/NativeRequest.js"; +import { PlatformAuthExtensionHandler } from "../../src/broker/nativeBroker/PlatformAuthExtensionHandler.js"; +import * as PlatformAuthProvider from "../../src/broker/nativeBroker/PlatformAuthProvider.js"; +import { PlatformAuthInteractionClient } from "../../src/interaction_client/PlatformAuthInteractionClient.js"; +import { PlatformAuthRequest } from "../../src/broker/nativeBroker/PlatformAuthRequest.js"; import { NativeAuthError } from "../../src/error/NativeAuthError.js"; import { StandardController } from "../../src/controllers/StandardController.js"; import { AuthenticationResult } from "../../src/response/AuthenticationResult.js"; @@ -121,7 +111,11 @@ import { buildIdToken, TestTimeUtils, } from "msal-test-utils"; -import { nativeConnectionNotEstablished } from "../../src/error/BrowserAuthErrorCodes.js"; +import { INTERACTION_TYPE } from "../../src/utils/BrowserConstants.js"; +import { version } from "../../src/packageMetadata.js"; +import { AuthorizationCodeRequest } from "../../src/request/AuthorizationCodeRequest.js"; +import { EndSessionRequest } from "../../src/request/EndSessionRequest.js"; +import { PlatformAuthDOMHandler } from "../../src/broker/nativeBroker/PlatformAuthDOMHandler.js"; const cacheConfig = { temporaryCacheLocation: BrowserCacheLocation.SessionStorage, @@ -142,7 +136,7 @@ let testAppConfig = { }, }; -function stubProvider(config: Configuration) { +function stubExtensionProvider(config: Configuration) { const browserEnvironment = typeof window !== "undefined"; const newConfig = buildConfiguration(config, browserEnvironment); @@ -152,11 +146,10 @@ function stubProvider(config: Configuration) { "unittest" ); const performanceClient = newConfig.telemetry.client; - return jest - .spyOn(NativeMessageHandler, "createProvider") + .spyOn(PlatformAuthExtensionHandler, "createProvider") .mockImplementation(async () => { - return new NativeMessageHandler( + return new PlatformAuthExtensionHandler( logger, 2000, performanceClient, @@ -165,10 +158,41 @@ function stubProvider(config: Configuration) { }); } +function stubDOMProvider(config: Configuration) { + const browserEnvironment = typeof window !== "undefined"; + + const newConfig = buildConfiguration(config, browserEnvironment); + const logger = new Logger( + newConfig.system.loggerOptions, + "unittest", + "unittest" + ); + const performanceClient = newConfig.telemetry.client; + return jest + .spyOn(PlatformAuthDOMHandler, "createProvider") + .mockImplementation(async () => { + return new PlatformAuthDOMHandler( + logger, + performanceClient, + "test-correlation-id" + ); + }); +} + +const testRequest: CommonAuthorizationUrlRequest = { + redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + authority: `${Constants.DEFAULT_AUTHORITY}`, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, + responseMode: ResponseMode.FRAGMENT, + state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, + nonce: ID_TOKEN_CLAIMS.nonce, +}; + describe("PublicClientApplication.ts Class Unit Tests", () => { - // eslint-disable-next-line @typescript-eslint/no-var-requires - globalThis.MessageChannel = require("worker_threads").MessageChannel; // jsdom does not include an implementation for MessageChannel let pca: PublicClientApplication; + let browserStorage: BrowserCacheManager; beforeEach(async () => { pca = new PublicClientApplication({ auth: { @@ -188,6 +212,9 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { await pca.initialize(); + // @ts-ignore + browserStorage = pca.controller.browserStorage; + // Navigation not allowed in tests jest.spyOn( NavigationClient.prototype, @@ -233,11 +260,20 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(pca instanceof PublicClientApplication).toBeTruthy(); done(); }); + + it("Sets isBroker to false", () => { + const config = { + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + }; + pca = new PublicClientApplication(config); + // @ts-ignore + expect(pca.isBroker).toBe(false); + }); }); describe("initialize tests", () => { - globalThis.MessageChannel = require("worker_threads").MessageChannel; // jsdom does not include an implementation for MessageChannel - beforeEach(() => { jest.spyOn(MessageEvent.prototype, "source", "get").mockReturnValue( window @@ -270,7 +306,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { ); // @ts-ignore const handshakeSpy: jest.SpyInstance = jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, // @ts-ignore "sendHandshakeRequest" ); @@ -283,8 +319,9 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { event.stopImmediatePropagation(); const request = event.data; const req = { - channel: NativeConstants.CHANNEL_ID, - extensionId: NativeConstants.PREFERRED_EXTENSION_ID, + channel: PlatformAuthConstants.CHANNEL_ID, + extensionId: + PlatformAuthConstants.PREFERRED_EXTENSION_ID, responseId: request.responseId, body: { method: "HandshakeResponse", @@ -323,7 +360,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { // @ts-ignore expect( (apps[i] as any).controller.getNativeExtensionProvider() - ).toBeInstanceOf(NativeMessageHandler); + ).toBeInstanceOf(PlatformAuthExtensionHandler); } } finally { for (const port of ports) { @@ -360,7 +397,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { ); // @ts-ignore const createProviderSpy: jest.SpyInstance = jest.spyOn( - NativeMessageHandler, + PlatformAuthExtensionHandler, "createProvider" ); @@ -372,8 +409,9 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { event.stopImmediatePropagation(); const request = event.data; const req = { - channel: NativeConstants.CHANNEL_ID, - extensionId: NativeConstants.PREFERRED_EXTENSION_ID, + channel: PlatformAuthConstants.CHANNEL_ID, + extensionId: + PlatformAuthConstants.PREFERRED_EXTENSION_ID, responseId: request.responseId, body: { method: "HandshakeResponse", @@ -431,7 +469,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { } }); - it("creates extension provider if allowPlatformBroker is true", async () => { + it("creates platform auth extension handler if allowPlatformBroker is true", async () => { const config = { auth: { clientId: TEST_CONFIG.MSAL_CLIENT_ID, @@ -440,9 +478,22 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { allowPlatformBroker: true, }, }; + + jest.spyOn( + PlatformAuthProvider, + "isDomEnabledForPlatformAuth" + ).mockImplementation(() => { + return false; + }); + + const getPlatformAuthProviderSpy = jest.spyOn( + PlatformAuthProvider, + "getPlatformAuthProvider" + ); + pca = new PublicClientApplication(config); - const createProviderSpy = stubProvider(config); + const createExtensionProviderSpy = stubExtensionProvider(config); await pca.initialize(); @@ -450,39 +501,82 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any pca = (pca as any).controller; - expect(createProviderSpy).toHaveBeenCalled(); + expect(getPlatformAuthProviderSpy).toHaveBeenCalled(); + expect(createExtensionProviderSpy).toHaveBeenCalled(); // @ts-ignore - expect(pca.nativeExtensionProvider).toBeInstanceOf( - NativeMessageHandler + expect(pca.platformAuthProvider).toBeInstanceOf( + PlatformAuthExtensionHandler ); }); it("does not create extension provider if allowPlatformBroker is false", async () => { - const createProviderSpy = jest.spyOn( - NativeMessageHandler, - "createProvider" + const getPlatformAuthProviderSpy = jest.spyOn( + PlatformAuthProvider, + "getPlatformAuthProvider" ); - pca = new PublicClientApplication({ + + const config = { auth: { clientId: TEST_CONFIG.MSAL_CLIENT_ID, }, system: { allowPlatformBroker: false, }, - }); + }; + + pca = new PublicClientApplication(config); + const createProviderSpy = stubExtensionProvider(config); + await pca.initialize(); //Implementation of PCA was moved to controller. pca = (pca as any).controller; expect(createProviderSpy).toHaveBeenCalledTimes(0); + expect(getPlatformAuthProviderSpy).not.toHaveBeenCalled(); + // @ts-ignore + expect(pca.platformAuthProvider).toBeUndefined(); + }); + + it("creates platform auth dom handler if allowPlatformBroker is true and dom APIs are present", async () => { + const getPlatformAuthProviderSpy = jest.spyOn( + PlatformAuthProvider, + "getPlatformAuthProvider" + ); + + const config = { + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + system: { + allowPlatformBroker: true, + }, + }; + + pca = new PublicClientApplication(config); + window.sessionStorage.setItem( + "msal.browser.platform.auth.dom", + "true" + ); + + const createDOMProviderSpy = stubDOMProvider(config); + await pca.initialize(); + + // Implementation of PCA was moved to controller. + // eslint-disable-next-line @typescript-eslint/no-explicit-any + pca = (pca as any).controller; + + expect(getPlatformAuthProviderSpy).toHaveBeenCalled(); + expect(createDOMProviderSpy).toHaveBeenCalled(); // @ts-ignore - expect(pca.nativeExtensionProvider).toBeUndefined(); + expect(pca.platformAuthProvider).toBeInstanceOf( + PlatformAuthDOMHandler + ); }); it("catches error if extension provider fails to initialize", async () => { const createProviderSpy = jest - .spyOn(NativeMessageHandler, "createProvider") + .spyOn(PlatformAuthExtensionHandler, "createProvider") .mockRejectedValue(new Error("testError")); pca = new PublicClientApplication({ auth: { @@ -499,7 +593,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(createProviderSpy).toHaveBeenCalled(); // @ts-ignore - expect(pca.nativeExtensionProvider).toBeUndefined(); + expect(pca.platformAuthProvider).toBeUndefined(); }); it("reports telemetry event using provided correlation id", (done) => { @@ -577,6 +671,26 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(preGenerateSpy).toHaveBeenCalledTimes(1); }); + + it("passes in isBroker in request", async () => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + system: { + allowPlatformBroker: false, + }, + }); + const initializeControllerSpy = jest.spyOn( + StandardController.prototype, + "initialize" + ); + await pca.initialize(); + expect(initializeControllerSpy).toHaveBeenCalledWith( + undefined, + false + ); + }); }); describe("handleRedirectPromise", () => { @@ -608,6 +722,15 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { tokenType: AuthenticationScheme.BEARER, }; + jest.spyOn( + BrowserCacheManager.prototype, + "isInteractionInProgress" + ).mockReturnValue(true); + jest.spyOn( + BrowserCacheManager.prototype, + "getCachedRequest" + ).mockReturnValue([testRequest, TEST_CONFIG.TEST_VERIFIER]); + const redirectClientSpy = jest .spyOn(RedirectClient.prototype, "handleRedirectPromise") .mockImplementation(() => { @@ -654,6 +777,15 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { tokenType: AuthenticationScheme.BEARER, }; + jest.spyOn( + BrowserCacheManager.prototype, + "isInteractionInProgress" + ).mockReturnValue(true); + jest.spyOn( + BrowserCacheManager.prototype, + "getCachedRequest" + ).mockReturnValue([testRequest, TEST_CONFIG.TEST_VERIFIER]); + jest.spyOn(pca, "getAllAccounts").mockReturnValue([testAccount]); jest.spyOn( RedirectClient.prototype, @@ -679,6 +811,31 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { pca.handleRedirectPromise(); }); + it("cleans temporary cache and rethrows if error is thrown", (done) => { + browserStorage.setInteractionInProgress(true); + browserStorage.cacheAuthorizeRequest( + testRequest, + TEST_CONFIG.TEST_VERIFIER + ); + const testError: AuthError = new AuthError( + "Unexpected error!", + "Unexpected error" + ); + jest.spyOn( + RedirectClient.prototype, + "handleRedirectPromise" + ).mockRejectedValue(testError); + pca.handleRedirectPromise().catch((e) => { + expect(e).toMatchObject(testError); + expect(window.localStorage.length).toEqual(0); + expect(window.sessionStorage.length).toEqual(1); + expect( + window.sessionStorage.getItem(StaticCacheKeys.VERSION) + ).toEqual(version); // Validate that the one item in sessionStorage is what we expect + done(); + }); + }); + it("Calls NativeInteractionClient.handleRedirectPromise and returns its response", async () => { const config = { auth: { @@ -690,7 +847,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); // Implementation of PCA was moved to controller. @@ -721,7 +878,12 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { fromNativeBroker: true, }; - const nativeRequest: NativeTokenRequest = { + jest.spyOn( + BrowserCacheManager.prototype, + "isInteractionInProgress" + ).mockReturnValue(true); + + const nativeRequest: PlatformAuthRequest = { authority: TEST_CONFIG.validAuthority, clientId: TEST_CONFIG.MSAL_CLIENT_ID, scope: TEST_CONFIG.DEFAULT_SCOPES.join(" "), @@ -738,7 +900,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { ); const redirectClientSpy: jest.SpyInstance = jest .spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "handleRedirectPromise" ) .mockImplementation(() => { @@ -779,7 +941,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }, }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); pca.initialize().then(() => { const callbackId = pca.addPerformanceCallback((events) => { @@ -831,8 +993,16 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { tokenType: AuthenticationScheme.BEARER, fromNativeBroker: true, }; + jest.spyOn( + BrowserCacheManager.prototype, + "isInteractionInProgress" + ).mockReturnValue(true); + jest.spyOn( + BrowserCacheManager.prototype, + "getCachedRequest" + ).mockReturnValue([testRequest, TEST_CONFIG.TEST_VERIFIER]); - const nativeRequest: NativeTokenRequest = { + const nativeRequest: PlatformAuthRequest = { authority: TEST_CONFIG.validAuthority, clientId: TEST_CONFIG.MSAL_CLIENT_ID, scope: TEST_CONFIG.DEFAULT_SCOPES.join(" "), @@ -851,7 +1021,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { testAccount, ]); jest.spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "handleRedirectPromise" ).mockResolvedValue(testTokenResponse); @@ -859,6 +1029,57 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); }); + it("Calls NativeInteractionClient.handleRedirectPromise and clears interaction_in_progress flag if it fails", (done) => { + const config = { + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + system: { + allowPlatformBroker: true, + }, + }; + pca = new PublicClientApplication(config); + + stubExtensionProvider(config); + + //@ts-ignore + pca.controller.browserStorage.setInteractionInProgress(true); + + const nativeRequest: PlatformAuthRequest = { + authority: TEST_CONFIG.validAuthority, + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + scope: TEST_CONFIG.DEFAULT_SCOPES.join(" "), + accountId: "1234", + redirectUri: window.location.href, + correlationId: RANDOM_TEST_GUID, + windowTitleSubstring: "test window", + }; + // @ts-ignore + pca.controller.browserStorage.setTemporaryCache( + TemporaryCacheKeys.NATIVE_REQUEST, + JSON.stringify(nativeRequest), + true + ); + const redirectClientSpy: jest.SpyInstance = jest + .spyOn( + PlatformAuthInteractionClient.prototype, + "handleRedirectPromise" + ) + .mockRejectedValue(new Error("testerror")); + + pca.initialize().then(() => + pca.handleRedirectPromise().catch((e) => { + expect(redirectClientSpy).toHaveBeenCalled(); + expect(e.message).toEqual("testerror"); + expect( + // @ts-ignore + pca.controller.browserStorage.isInteractionInProgress() + ).toEqual(false); + done(); + }) + ); + }); + it("Emits acquireToken success event if user was already signed in", async () => { const testAccount: AccountInfo = { homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, @@ -881,6 +1102,14 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { account: testAccount, tokenType: AuthenticationScheme.BEARER, }; + jest.spyOn( + BrowserCacheManager.prototype, + "isInteractionInProgress" + ).mockReturnValue(true); + jest.spyOn( + BrowserCacheManager.prototype, + "getCachedRequest" + ).mockReturnValue([testRequest, TEST_CONFIG.TEST_VERIFIER]); jest.spyOn(pca, "getAllAccounts").mockReturnValue([testAccount]); const redirectClientSpy: jest.SpyInstance = jest .spyOn(RedirectClient.prototype, "handleRedirectPromise") @@ -899,10 +1128,18 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(acquireTokenSuccessFired).toBe(true); }); - it("Emits login failure event if user was already signed in", async () => { + it("Emits login failure event if user was already signed in", (done) => { const redirectClientSpy: jest.SpyInstance = jest .spyOn(RedirectClient.prototype, "handleRedirectPromise") .mockRejectedValue(new Error("Error")); + jest.spyOn( + BrowserCacheManager.prototype, + "isInteractionInProgress" + ).mockReturnValue(true); + jest.spyOn( + BrowserCacheManager.prototype, + "getCachedRequest" + ).mockReturnValue([testRequest, TEST_CONFIG.TEST_VERIFIER]); let loginFailureFired = false; jest.spyOn(EventHandler.prototype, "emitEvent").mockImplementation( (eventType) => { @@ -911,13 +1148,14 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { } } ); - await pca.handleRedirectPromise().catch(() => { + pca.handleRedirectPromise().catch(() => { expect(redirectClientSpy).toHaveBeenCalledTimes(1); expect(loginFailureFired).toBe(true); + done(); }); }); - it("Emits acquireToken failure event if user was already signed in", async () => { + it("Emits acquireToken failure event if user was already signed in", (done) => { const testAccount: AccountInfo = { homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID, @@ -925,6 +1163,14 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { tenantId: "3338040d-6c67-4c5b-b112-36a304b66dad", username: "AbeLi@microsoft.com", }; + jest.spyOn( + BrowserCacheManager.prototype, + "isInteractionInProgress" + ).mockReturnValue(true); + jest.spyOn( + BrowserCacheManager.prototype, + "getCachedRequest" + ).mockReturnValue([testRequest, TEST_CONFIG.TEST_VERIFIER]); jest.spyOn( StandardController.prototype, "getAllAccounts" @@ -941,58 +1187,37 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { } ); - await pca.handleRedirectPromise().catch(() => { + pca.handleRedirectPromise().catch(() => { expect(redirectClientSpy).toHaveBeenCalledTimes(1); expect(acquireTokenFailureFired).toBe(true); + done(); }); }); it("Multiple concurrent calls to handleRedirectPromise return the same promise", async () => { - const stateString = TEST_STATE_VALUES.TEST_STATE_REDIRECT; - const browserCrypto = new CryptoOps(new Logger({})); - const stateId = ProtocolUtils.parseRequestState( - browserCrypto, - stateString - ).libraryState.id; - window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.ORIGIN_URI}`, TEST_URIS.TEST_REDIR_URI ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.AUTHORITY}.${stateId}`, - TEST_CONFIG.validAuthority - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}`, TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT ); window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, - TEST_CONFIG.MSAL_CLIENT_ID - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.NONCE_IDTOKEN}.${stateId}`, - "123523" - ); - const testTokenReq: CommonAuthorizationCodeRequest = { - redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, - code: "thisIsATestCode", - scopes: TEST_CONFIG.DEFAULT_SCOPES, - codeVerifier: TEST_CONFIG.TEST_VERIFIER, - authority: `${Constants.DEFAULT_AUTHORITY}`, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - }; - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_PARAMS}`, - base64Encode(JSON.stringify(testTokenReq)) + JSON.stringify({ + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + type: INTERACTION_TYPE.SIGNIN, + }) ); + jest.spyOn( + BrowserCacheManager.prototype, + "isInteractionInProgress" + ).mockReturnValue(true); + jest.spyOn( + BrowserCacheManager.prototype, + "getCachedRequest" + ).mockReturnValue([testRequest, TEST_CONFIG.TEST_VERIFIER]); const testServerTokenResponse = { headers: {}, status: 200, @@ -1119,6 +1344,14 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { jest.spyOn(RedirectClient.prototype, "getRedirectResponse") // @ts-ignore .mockReturnValue([null, ""]); + jest.spyOn( + BrowserCacheManager.prototype, + "isInteractionInProgress" + ).mockReturnValue(true); + jest.spyOn( + BrowserCacheManager.prototype, + "getCachedRequest" + ).mockReturnValue([testRequest, TEST_CONFIG.TEST_VERIFIER]); const callbackId = pca.addPerformanceCallback((events) => { expect(events.length).toEqual(1); @@ -1128,13 +1361,12 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { done(); }); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.CORRELATION_ID}`, - RANDOM_TEST_GUID - ); window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, - TEST_CONFIG.MSAL_CLIENT_ID + JSON.stringify({ + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + type: INTERACTION_TYPE.SIGNIN, + }) ); pca.handleRedirectPromise(); }); @@ -1164,6 +1396,57 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { await pca.handleRedirectPromise(); expect(emitSpy).toHaveBeenCalledTimes(0); }); + + it("removes interaction_in_progress and returns null after sign-out", async () => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + }); + await pca.initialize(); + + window.sessionStorage.setItem( + `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, + JSON.stringify({ + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + type: INTERACTION_TYPE.SIGNOUT, + }) + ); + const res = await pca.handleRedirectPromise(); + expect(res).toBeNull(); + expect( + // @ts-ignore + pca.controller.browserStorage.getInteractionInProgress() + ).toBeFalsy(); + }); + + it("removes interaction_in_progress and throws after sign-in when there are no tokens in cache", async () => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + }); + await pca.initialize(); + + window.sessionStorage.setItem( + `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, + JSON.stringify({ + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + type: INTERACTION_TYPE.SIGNIN, + }) + ); + try { + await pca.handleRedirectPromise(); + throw "Unexpected code path"; + } catch (e) { + // @ts-ignore + expect(e.errorCode).toEqual("no_token_request_cache_error"); + } + expect( + // @ts-ignore + pca.controller.browserStorage.getInteractionInProgress() + ).toBeFalsy(); + }); }); describe("OIDC Protocol Mode tests", () => { beforeEach(async () => { @@ -1194,6 +1477,24 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { RedirectClient.prototype, "getRedirectResponse" ); + + const request: CommonAuthorizationUrlRequest = { + redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + authority: `${Constants.DEFAULT_AUTHORITY}`, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: + TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, + responseMode: ResponseMode.QUERY, + state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, + nonce: RANDOM_TEST_GUID, + }; + // @ts-ignore + pca.controller.browserStorage.cacheAuthorizeRequest( + request, + RANDOM_TEST_GUID + ); + jest.spyOn( BrowserCacheManager.prototype, "isInteractionInProgress" @@ -1213,6 +1514,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { { code: "authCode", state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, + nonce: RANDOM_TEST_GUID, }, responseString, ]); @@ -1286,12 +1588,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { it("navigates to created login url, with empty request", (done) => { jest.spyOn( - RedirectHandler.prototype, + RedirectClient.prototype, "initiateAuthRequest" ).mockImplementation((navigateUrl): Promise => { - expect( - navigateUrl.startsWith(testNavUrlNoRequest) - ).toBeTruthy(); + verifyUrl(navigateUrl); return Promise.resolve(done()); }); jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ @@ -1341,7 +1641,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. @@ -1362,7 +1662,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const nativeAcquireTokenSpy = jest .spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireTokenRedirect" ) .mockResolvedValue(); @@ -1396,7 +1696,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); const callbackId = pca.addPerformanceCallback((events) => { expect(events.length).toBeGreaterThanOrEqual(1); @@ -1435,7 +1735,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { pca = new PublicClientApplication(config); await pca.initialize(); - stubProvider(config); + stubExtensionProvider(config); //Implementation of PCA was moved to controller. pca = (pca as any).controller; @@ -1450,7 +1750,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy = jest.spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireTokenRedirect" ); const redirectSpy: jest.SpyInstance = jest @@ -1477,7 +1777,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); // Implementation of PCA was moved to controller. @@ -1495,7 +1795,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const nativeAcquireTokenSpy: jest.SpyInstance = jest .spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireTokenRedirect" ) .mockRejectedValue( @@ -1524,7 +1824,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); // Implementation of PCA was moved to controller. @@ -1542,7 +1842,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const nativeAcquireTokenSpy: jest.SpyInstance = jest .spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireTokenRedirect" ) .mockRejectedValue( @@ -1573,7 +1873,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //PCA implementation moved to controller @@ -1590,7 +1890,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { const nativeAcquireTokenSpy: jest.SpyInstance = jest .spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireTokenRedirect" ) .mockRejectedValue(new Error("testError")); @@ -1784,6 +2084,33 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(redirectClientSpy).toHaveBeenCalledTimes(1); }); + it("Cleans temporary cache if error is thrown by RedirectClient.acquireToken", (done) => { + const redirectClientSpy: jest.SpyInstance = jest + .spyOn(RedirectClient.prototype, "acquireToken") + .mockImplementation(() => { + expect(browserStorage.isInteractionInProgress()).toBe(true); + browserStorage.cacheAuthorizeRequest( + testRequest, + TEST_CONFIG.TEST_VERIFIER + ); + expect(window.sessionStorage.length).toBe(3); + return Promise.reject(new Error("testerror")); + }); + + pca.acquireTokenRedirect({ + scopes: ["openid"], + }).catch((e) => { + expect(redirectClientSpy).toHaveBeenCalledTimes(1); + expect(browserStorage.isInteractionInProgress()).toBe(false); + expect(window.localStorage.length).toBe(0); + expect(window.sessionStorage.length).toBe(1); + expect( + window.sessionStorage.getItem(StaticCacheKeys.VERSION) + ).toEqual(version); // Validate that the one item in sessionStorage is what we expect + done(); + }); + }); + it("Emits acquireToken Start and Failure events if user is already logged in", async () => { const testAccount: AccountInfo = { homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, @@ -1853,7 +2180,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }, }; - stubProvider(config); + stubExtensionProvider(config); pca = new PublicClientApplication({ ...config, telemetry: { @@ -2193,7 +2520,11 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { beforeEach(async () => { const popupWindow = { ...window, + location: { + assign: () => {}, + }, close: () => {}, + focus: () => {}, }; // @ts-ignore jest.spyOn(window, "open").mockReturnValue(popupWindow); @@ -2261,7 +2592,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. @@ -2295,7 +2626,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { ); const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockImplementation(async (request) => { expect(request.correlationId).toBe(RANDOM_TEST_GUID); return testTokenResponse; @@ -2324,7 +2655,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); const testAccount: AccountInfo = { @@ -2351,7 +2682,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy: jest.SpyInstance = jest.spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireToken" ); const popupSpy: jest.SpyInstance = jest @@ -2379,7 +2710,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. @@ -2409,7 +2740,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockRejectedValue( new NativeAuthError("ContentError", "error in extension") ); @@ -2437,7 +2768,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. @@ -2467,7 +2798,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockImplementation(() => { throw createInteractionRequiredAuthError( InteractionRequiredAuthErrorCodes.nativeAccountUnavailable @@ -2497,7 +2828,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //PCA implementation moved to controller @@ -2513,7 +2844,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockRejectedValue(new Error("testError")); const popupSpy: jest.SpyInstance = jest .spyOn(PopupClient.prototype, "acquireToken") @@ -2906,10 +3237,14 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, }; + jest.spyOn( + PopupClient.prototype, + "monitorPopupForHash" + ).mockRejectedValue("Not important for this test"); + try { await testPca.acquireTokenPopup(request); } catch (e) {} - expect(spyPreGeneratePkceCodes).toHaveBeenCalledTimes(2); expect(spyPopupClientAcquireToken).toHaveBeenCalledWith( request, @@ -2962,6 +3297,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, }; + jest.spyOn( + PopupClient.prototype, + "monitorPopupForHash" + ).mockRejectedValue("Not important for this test"); try { await testPca.acquireTokenPopup(request); } catch (e) {} @@ -3042,7 +3381,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. @@ -3072,7 +3411,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockResolvedValue(testTokenResponse); const silentSpy: jest.SpyInstance = jest .spyOn(SilentIframeClient.prototype, "acquireToken") @@ -3099,7 +3438,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. @@ -3129,7 +3468,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockRejectedValue( new NativeAuthError("ContentError", "error in extension") ); @@ -3157,7 +3496,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. @@ -3173,7 +3512,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockRejectedValue(new Error("testError")); const silentSpy: jest.SpyInstance = jest .spyOn(SilentIframeClient.prototype, "acquireToken") @@ -3448,7 +3787,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. @@ -3478,7 +3817,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockResolvedValue(testTokenResponse); const response = await pca.acquireTokenByCode({ scopes: ["User.Read"], @@ -3500,14 +3839,14 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. pca = (pca as any).controller; const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockRejectedValue( new NativeAuthError( "ContentError", @@ -3538,7 +3877,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { await pca.initialize(); const nativeAcquireTokenSpy = jest.spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireToken" ); @@ -3945,7 +4284,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. @@ -3975,7 +4314,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockResolvedValue(testTokenResponse); const silentSpy: jest.SpyInstance = jest .spyOn(SilentIframeClient.prototype, "acquireToken") @@ -4002,7 +4341,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. @@ -4032,7 +4371,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockRejectedValue( new NativeAuthError("ContentError", "error in extension") ); @@ -4062,7 +4401,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. pca = (pca as any).controller; @@ -4077,7 +4416,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn(PlatformAuthInteractionClient.prototype, "acquireToken") .mockRejectedValue(new Error("testError")); const silentSpy: jest.SpyInstance = jest .spyOn(SilentIframeClient.prototype, "acquireToken") @@ -5143,7 +5482,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(1); + expect(window.sessionStorage).toHaveLength(2); + expect( + window.sessionStorage.getItem(StaticCacheKeys.VERSION) + ).toEqual(version); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); @@ -5199,7 +5541,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { await silentRequest3.catch(() => {}); // Test that error was cached for telemetry purposes and then thrown expect(atsSpy).toHaveBeenCalledTimes(1); - expect(window.sessionStorage).toHaveLength(1); + expect(window.sessionStorage).toHaveLength(2); + expect( + window.sessionStorage.getItem(StaticCacheKeys.VERSION) + ).toEqual(version); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); @@ -5907,7 +6252,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(silentIframeSpy).toHaveBeenCalledTimes(0); }); - it("Calls SilentCacheClient.acquireToken, and calls NativeInteractionClient.acquireToken when CacheLookupPolicy is set to AccessToken", async () => { + it("Calls SilentCacheClient.acquireToken, and calls PlatformAuthInteractionClient.acquireToken when CacheLookupPolicy is set to AccessToken", async () => { const silentCacheSpy: jest.SpyInstance = jest .spyOn(SilentCacheClient.prototype, "acquireToken") .mockRejectedValue(refreshRequiredCacheError); @@ -5918,11 +6263,14 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { .spyOn(SilentIframeClient.prototype, "acquireToken") .mockImplementation(); - const isPlatformBrokerAvailableSpy = jest - .spyOn(NativeMessageHandler, "isPlatformBrokerAvailable") + const isPlatformAuthAllowedSpy = jest + .spyOn(PlatformAuthProvider, "isPlatformAuthAllowed") .mockReturnValue(true); const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn( + PlatformAuthInteractionClient.prototype, + "acquireToken" + ) .mockImplementation(); const cacheAccount = testAccount; cacheAccount.nativeAccountId = "nativeAccountId"; @@ -5946,7 +6294,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(0); nativeAcquireTokenSpy.mockRestore(); - isPlatformBrokerAvailableSpy.mockRestore(); + isPlatformAuthAllowedSpy.mockRestore(); }); it("Calls SilentRefreshClient.acquireToken, and does not call SilentCacheClient.acquireToken or SilentIframeClient.acquireToken if refresh token is expired when CacheLookupPolicy is set to RefreshToken", async () => { @@ -5983,13 +6331,16 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { .spyOn(SilentIframeClient.prototype, "acquireToken") .mockImplementation(); const nativeAcquireTokenSpy: jest.SpyInstance = jest - .spyOn(NativeInteractionClient.prototype, "acquireToken") + .spyOn( + PlatformAuthInteractionClient.prototype, + "acquireToken" + ) .mockImplementation(); const cacheAccount = testAccount; cacheAccount.nativeAccountId = "nativeAccountId"; - const isPlatformBrokerAvailableSpy = jest - .spyOn(NativeMessageHandler, "isPlatformBrokerAvailable") + const isPlatformAuthAllowedSpy = jest + .spyOn(PlatformAuthProvider, "isPlatformAuthAllowed") .mockReturnValue(true); testAccount.nativeAccountId = "nativeAccountId"; @@ -6012,7 +6363,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(silentIframeSpy).toHaveBeenCalledTimes(0); expect(nativeAcquireTokenSpy).toHaveBeenCalledTimes(0); nativeAcquireTokenSpy.mockRestore(); - isPlatformBrokerAvailableSpy.mockRestore(); + isPlatformAuthAllowedSpy.mockRestore(); }); it("Calls SilentRefreshClient.acquireToken, and does not call SilentCacheClient.acquireToken or SilentIframeClient.acquireToken if refresh token is expired when CacheLookupPolicy is set to RefreshToken", async () => { @@ -6383,6 +6734,33 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(accounts).toEqual([]); }); + it("getAllAccounts throws if called before initialize", (done) => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + cache: { + cacheLocation: "localStorage", + }, + }); + + window.localStorage.setItem( + "msal.account.keys", + JSON.stringify([testAccount1.generateAccountKey()]) + ); + + try { + pca.getAllAccounts(); + } catch (e) { + expect(e).toEqual( + createBrowserAuthError( + BrowserAuthErrorCodes.uninitializedPublicClientApplication + ) + ); + done(); + } + }); + it("getAccountByUsername returns account specified", () => { const account = pca.getAccountByUsername( ID_TOKEN_CLAIMS.preferred_username @@ -6418,6 +6796,33 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(account).toBe(null); }); + it("getAccountByUsername throws if called before initialize", (done) => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + cache: { + cacheLocation: "localStorage", + }, + }); + + window.localStorage.setItem( + "msal.account.keys", + JSON.stringify([testAccount1.generateAccountKey()]) + ); + + try { + pca.getAccountByUsername(testAccount1.username); + } catch (e) { + expect(e).toEqual( + createBrowserAuthError( + BrowserAuthErrorCodes.uninitializedPublicClientApplication + ) + ); + done(); + } + }); + it("getAccountByHomeId returns account specified", () => { const account = pca.getAccountByHomeId( testAccountInfo1.homeAccountId @@ -6437,6 +6842,33 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(account).toBe(null); }); + it("getAccountByUsername throws if called before initialize", (done) => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + cache: { + cacheLocation: "localStorage", + }, + }); + + window.localStorage.setItem( + "msal.account.keys", + JSON.stringify([testAccount1.generateAccountKey()]) + ); + + try { + pca.getAccountByHomeId(testAccount1.homeAccountId); + } catch (e) { + expect(e).toEqual( + createBrowserAuthError( + BrowserAuthErrorCodes.uninitializedPublicClientApplication + ) + ); + done(); + } + }); + it("getAccountByLocalId returns account specified", () => { const account = pca.getAccountByLocalId(ID_TOKEN_CLAIMS.oid); expect(account?.idTokenClaims).not.toBeUndefined(); @@ -6454,12 +6886,66 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(account).toBe(null); }); + it("getAccountByLocalId throws if called before initialize", (done) => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + cache: { + cacheLocation: "localStorage", + }, + }); + + window.localStorage.setItem( + "msal.account.keys", + JSON.stringify([testAccount1.generateAccountKey()]) + ); + + try { + pca.getAccountByLocalId(testAccount1.localAccountId); + } catch (e) { + expect(e).toEqual( + createBrowserAuthError( + BrowserAuthErrorCodes.uninitializedPublicClientApplication + ) + ); + done(); + } + }); + describe("getAccount", () => { it("getAccount returns null if empty filter is passed in", () => { const account = pca.getAccount({}); expect(account).toBe(null); }); + it("getAccount throws if called before initialize", (done) => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + cache: { + cacheLocation: "localStorage", + }, + }); + + window.localStorage.setItem( + "msal.account.keys", + JSON.stringify([testAccount1.generateAccountKey()]) + ); + + try { + pca.getAccount({ username: testAccount1.username }); + } catch (e) { + expect(e).toEqual( + createBrowserAuthError( + BrowserAuthErrorCodes.uninitializedPublicClientApplication + ) + ); + done(); + } + }); + describe("loginHint filter", () => { it("getAccount returns account specified using login_hint", () => { const account = pca.getAccount({ @@ -6590,6 +7076,40 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(pca.getActiveAccount()).toBe(null); }); + it("getActiveAccount throws if called before initialize", (done) => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + cache: { + cacheLocation: "localStorage", + }, + }); + + window.localStorage.setItem( + `msal.${TEST_CONFIG.MSAL_CLIENT_ID}.active-account-filters`, + JSON.stringify({ + homeAccountId: testAccount1.homeAccountId, + localAccountId: testAccount1.localAccountId, + }) + ); + window.localStorage.setItem( + "msal.account.keys", + JSON.stringify([testAccount1.generateAccountKey()]) + ); + + try { + pca.getActiveAccount(); + } catch (e) { + expect(e).toEqual( + createBrowserAuthError( + BrowserAuthErrorCodes.uninitializedPublicClientApplication + ) + ); + done(); + } + }); + it("setActiveAccount() sets the active account local id value correctly", () => { expect(pca.getActiveAccount()).toBe(null); pca.setActiveAccount(testAccountInfo1); @@ -6870,7 +7390,7 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { }; pca = new PublicClientApplication(config); - stubProvider(config); + stubExtensionProvider(config); await pca.initialize(); //Implementation of PCA was moved to controller. @@ -7259,7 +7779,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { // Ensure account is present in the cache before removing it const cacheKey = AccountEntity.generateAccountCacheKey(accountInfo); - secondBrowserStorageInstance.removeAccount(cacheKey); + secondBrowserStorageInstance.removeAccount( + cacheKey, + RANDOM_TEST_GUID + ); }); }); @@ -7284,7 +7807,10 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { .setAccount(accountEntity, TEST_CONFIG.CORRELATION_ID) .then(() => { // Ensure account is present in the cache before setting it as active - secondBrowserStorageInstance.setActiveAccount(accountInfo); + secondBrowserStorageInstance.setActiveAccount( + accountInfo, + RANDOM_TEST_GUID + ); }); }); }); @@ -7334,4 +7860,123 @@ describe("PublicClientApplication.ts Class Unit Tests", () => { expect(pkce1?.challenge !== pkce2?.challenge).toBeTruthy(); }); }); + describe("Multi-instance tests", () => { + afterEach(() => { + // @ts-ignore + window.msal.clientIds = []; + // @ts-ignore + window.msal = {}; + }); + it("Logs warning if there are two applications with the same client id in the same frame", async () => { + const msalConfig: Configuration = { + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + system: { + allowPlatformBroker: false, + loggerOptions: { + logLevel: LogLevel.Verbose, + loggerCallback: jest.fn(), + }, + }, + }; + pca = new PublicClientApplication(msalConfig); + await pca.initialize(); + const pca2 = new PublicClientApplication(msalConfig); + const logger = pca2.getLogger(); + const loggerCallbackStub = jest + .spyOn(logger, "executeCallback") + .mockImplementation(); + await pca2.initialize(); + expect(loggerCallbackStub).toHaveBeenCalledWith( + LogLevel.Warning, + expect.stringContaining( + "There is already an instance of MSAL.js in the window with the same client id." + ), + false + ); + }); + + it("Logs verbose if there are two applications with different client ids in the same frame", async () => { + const msalConfig: Configuration = { + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + system: { + allowPlatformBroker: false, + loggerOptions: { + logLevel: LogLevel.Verbose, + loggerCallback: jest.fn(), + }, + }, + }; + pca = new PublicClientApplication(msalConfig); + await pca.initialize(); + const pca2 = new PublicClientApplication({ + ...msalConfig, + auth: { clientId: "differentClientId" }, + }); + const logger = pca2.getLogger(); + const loggerCallbackStub = jest + .spyOn(logger, "executeCallback") + .mockImplementation(); + await pca2.initialize(); + expect(loggerCallbackStub).toHaveBeenCalledWith( + LogLevel.Verbose, + expect.stringContaining( + "There is already an instance of MSAL.js in the window." + ), + false + ); + }); + + it("Reports in telemetry the number of applications in the same frame (different client ids)", async () => { + await pca.initialize(); + + const pca2 = new PublicClientApplication({ + ...pca.getConfiguration(), + auth: { clientId: "different-client-id" }, + }); + + const telemetryPromise = new Promise((resolve) => { + const callbackId = pca2.addPerformanceCallback((events) => { + expect(events.length).toEqual(1); + const event = events[0]; + expect(event.name).toBe( + PerformanceEvents.InitializeClientApplication + ); + expect(event.msalInstanceCount).toEqual(2); + expect(event.sameClientIdInstanceCount).toEqual(1); + pca.removePerformanceCallback(callbackId); + resolve(); + }); + }); + + await pca2.initialize(); + await telemetryPromise; + }); + + it("Reports in telemetry the number of applications in the same frame (same client ids)", async () => { + await pca.initialize(); + + const pca2 = new PublicClientApplication(pca.getConfiguration()); + + const telemetryPromise = new Promise((resolve) => { + const callbackId = pca2.addPerformanceCallback((events) => { + expect(events.length).toEqual(1); + const event = events[0]; + expect(event.name).toBe( + PerformanceEvents.InitializeClientApplication + ); + expect(event.msalInstanceCount).toEqual(2); + expect(event.sameClientIdInstanceCount).toEqual(2); + pca.removePerformanceCallback(callbackId); + resolve(); + }); + }); + + await pca2.initialize(); + await telemetryPromise; + }); + }); }); diff --git a/lib/msal-browser/test/broker/PlatformAuthDOMHandler.spec.ts b/lib/msal-browser/test/broker/PlatformAuthDOMHandler.spec.ts new file mode 100644 index 0000000000..52c6d9f0ae --- /dev/null +++ b/lib/msal-browser/test/broker/PlatformAuthDOMHandler.spec.ts @@ -0,0 +1,553 @@ +import { + AuthError, + AuthErrorCodes, + IPerformanceClient, + Logger, + PromptValue, +} from "@azure/msal-common/browser"; +import { PlatformAuthConstants } from "../../src/utils/BrowserConstants.js"; +import { getDefaultPerformanceClient } from "../utils/TelemetryUtils.js"; +import { PlatformAuthDOMHandler } from "../../src/broker/nativeBroker/PlatformAuthDOMHandler.js"; +import { PlatformAuthResponse } from "../../src/broker/nativeBroker/PlatformAuthResponse.js"; +import { + TEST_CONFIG, + TEST_TOKENS, + TEST_URIS, +} from "../utils/StringConstants.js"; +import { PlatformAuthRequest } from "../../src/broker/nativeBroker/PlatformAuthRequest.js"; +import { NativeAuthError } from "../../src/error/NativeAuthError.js"; + +describe("PlatformAuthDOMHandler tests", () => { + let performanceClient: IPerformanceClient; + let logger: Logger; + + let getSupportedContractsMock: jest.Mock; + let executeGetTokenMock: jest.Mock; + beforeEach(() => { + performanceClient = getDefaultPerformanceClient(); + logger = new Logger({}, "test", "1.0.0"); + + getSupportedContractsMock = jest.fn(); + executeGetTokenMock = jest.fn(); + Object.defineProperty(window.navigator, "platformAuthentication", { + value: { + getSupportedContracts: getSupportedContractsMock, + executeGetToken: executeGetTokenMock, + }, + writable: true, + }); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("createProvider tests", () => { + it("should return PlatformAuthDOMHandler when DOM APIs are available for platform auth", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + expect(getSupportedContractsMock).toHaveBeenCalled(); + expect(platformAuthDOMHandler).toBeInstanceOf( + PlatformAuthDOMHandler + ); + }); + + it("should return undefined when DOM APIs are not available for platform auth", async () => { + getSupportedContractsMock.mockResolvedValue([]); + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + expect(getSupportedContractsMock).toHaveBeenCalled(); + expect(platformAuthDOMHandler).toBe(undefined); + }); + }); + + describe("getExtensionId tests", () => { + it("should return the correct extension ID", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + expect(platformAuthDOMHandler).toBeInstanceOf( + PlatformAuthDOMHandler + ); + const brokerId = platformAuthDOMHandler?.getExtensionId(); + expect(brokerId).toBe( + PlatformAuthConstants.MICROSOFT_ENTRA_BROKERID + ); + }); + }); + + describe("getExtensionVersion tests", () => { + it("should return empty string for extensionVersion", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + expect(platformAuthDOMHandler).toBeInstanceOf( + PlatformAuthDOMHandler + ); + const brokerId = platformAuthDOMHandler?.getExtensionVersion(); + expect(brokerId).toBe(""); + }); + }); + + describe("getExtensionName tests", () => { + it("should return corect extensionName", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + expect(platformAuthDOMHandler).toBeInstanceOf( + PlatformAuthDOMHandler + ); + const brokerId = platformAuthDOMHandler?.getExtensionName(); + expect(brokerId).toBe(PlatformAuthConstants.DOM_API_NAME); + }); + }); + + describe("sendMessage and validatePlatformBrokerResponse tests", () => { + it("returns PlatformBrokerResponse success response for valid request to DOM API", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + const testRequest: PlatformAuthRequest = { + accountId: "test-id", + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + authority: TEST_CONFIG.validAuthority, + redirectUri: TEST_URIS.TEST_REDIR_URI, + scope: "read openid", + correlationId: TEST_CONFIG.CORRELATION_ID, + windowTitleSubstring: "", + extraParameters: { + extendedExpiryToken: "true", + }, + }; + const testDOMResponse: object = { + isSuccess: true, + state: "", + accessToken: TEST_TOKENS.ACCESS_TOKEN, + expiresIn: 6000, + account: { + id: "test-id", + userName: "test-user", + properties: {}, + }, + clientInfo: "test-client-info", + idToken: TEST_TOKENS.IDTOKEN_V1, + scopes: "read openid", + error: {}, + properties: { + MATS: '{"MATS":"{"x_ms_clitelem":"1,0,0,,I","ui_visible":true}"}', + }, + extendedLifetimeToken: true, + }; + const validatedResponse: PlatformAuthResponse = { + access_token: TEST_TOKENS.ACCESS_TOKEN, + account: { + id: "test-id", + userName: "test-user", + properties: {}, + }, + client_info: "test-client-info", + expires_in: 6000, + id_token: TEST_TOKENS.IDTOKEN_V1, + properties: { + MATS: '{"MATS":"{"x_ms_clitelem":"1,0,0,,I","ui_visible":true}"}', + }, + scope: "read openid", + state: "", + extendedLifetimeToken: true, + shr: undefined, + }; + executeGetTokenMock.mockResolvedValue(testDOMResponse); + + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + const platformBrokerResponse = + await platformAuthDOMHandler?.sendMessage(testRequest); + expect(platformBrokerResponse).toEqual(validatedResponse); + }); + + it("returns unexpected_error when token response is missing required properties with isSuccess = true", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + const testRequest: PlatformAuthRequest = { + accountId: "test-id", + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + authority: TEST_CONFIG.validAuthority, + redirectUri: TEST_URIS.TEST_REDIR_URI, + scope: "read openid", + correlationId: TEST_CONFIG.CORRELATION_ID, + windowTitleSubstring: "", + extraParameters: { + extendedExpiryToken: "true", + }, + }; + const testDOMResponse: object = { + isSuccess: true, + state: "", + expiresIn: 6000, + account: { + id: "test-id", + userName: "test-user", + properties: {}, + }, + clientInfo: "test-client-info", + scopes: "read openid", + error: {}, + properties: { + MATS: '{"MATS":"{"x_ms_clitelem":"1,0,0,,I","ui_visible":true}"}', + }, + extendedLifetimeToken: true, + }; + + executeGetTokenMock.mockResolvedValue(testDOMResponse); + + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + try { + const platformBrokerResponse = + await platformAuthDOMHandler?.sendMessage(testRequest); + throw "sendMessage should have thrown an error"; + } catch (e) { + expect(e).toBeInstanceOf(AuthError); + expect((e as AuthError).errorCode).toEqual( + AuthErrorCodes.unexpectedError + ); + expect((e as AuthError).errorMessage).toContain( + "Response missing expected properties." + ); + } + }); + + it("returns unexpected_error when required error properties missing from response with isSuccess = false", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + const testRequest: PlatformAuthRequest = { + accountId: "test-id", + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + authority: TEST_CONFIG.validAuthority, + redirectUri: TEST_URIS.TEST_REDIR_URI, + scope: "read openid", + correlationId: TEST_CONFIG.CORRELATION_ID, + windowTitleSubstring: "", + extraParameters: { + extendedExpiryToken: "true", + }, + }; + const testDOMResponse: object = { + isSuccess: false, + expiresIn: 0, + extendedLifetimeToken: false, + error: {}, + }; + + executeGetTokenMock.mockResolvedValue(testDOMResponse); + + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + try { + const platformBrokerResponse = + await platformAuthDOMHandler?.sendMessage(testRequest); + throw "sendMessage should have thrown an error"; + } catch (e) { + expect(e).toBeInstanceOf(AuthError); + expect((e as AuthError).errorCode).toEqual( + AuthErrorCodes.unexpectedError + ); + expect((e as AuthError).errorMessage).toContain( + "Response missing expected properties." + ); + } + }); + + it("returns nativeAuthError response for unsuccessful token request", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + const testRequest: PlatformAuthRequest = { + accountId: "test-id", + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + authority: TEST_CONFIG.validAuthority, + redirectUri: TEST_URIS.TEST_REDIR_URI, + scope: "read openid", + correlationId: TEST_CONFIG.CORRELATION_ID, + windowTitleSubstring: "", + extraParameters: { + extendedExpiryToken: "true", + }, + }; + const testDOMResponse: object = { + isSuccess: false, + expiresIn: 0, + extendedLifetimeToken: false, + error: { + code: "OSError", + description: "there is an OSError", + errorCode: "-6000", + protocolError: "-5000", + status: "PERSISTENT_ERROR", + properties: {}, + }, + }; + executeGetTokenMock.mockResolvedValue(testDOMResponse); + + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + try { + const platformBrokerResponse = + await platformAuthDOMHandler?.sendMessage(testRequest); + throw "sendMessage should have thrown an error"; + } catch (e) { + expect(e).toBeInstanceOf(NativeAuthError); + expect((e as NativeAuthError).errorCode).toEqual("OSError"); + expect((e as NativeAuthError).errorMessage).toEqual( + (testDOMResponse as any).error.description + ); + } + }); + }); + + describe("initializePlatformDOMRequest tests", () => { + it("should return a valid PlatformDOMTokenRequest object", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + const testRequest: PlatformAuthRequest = { + accountId: "test-id", + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + authority: TEST_CONFIG.validAuthority, + redirectUri: TEST_URIS.TEST_REDIR_URI, + scope: "read openid", + correlationId: TEST_CONFIG.CORRELATION_ID, + windowTitleSubstring: "test-window-substring", + prompt: "login", + nonce: "test-nonce", + claims: "test-claims", + extendedExpiryToken: true, + }; + const platformDOMRequest = + //@ts-ignore + platformAuthDOMHandler.initializePlatformDOMRequest( + testRequest + ); + expect(platformDOMRequest).toEqual({ + accountId: testRequest.accountId, + brokerId: PlatformAuthConstants.MICROSOFT_ENTRA_BROKERID, + authority: testRequest.authority, + clientId: testRequest.clientId, + correlationId: testRequest.correlationId, + isSecurityTokenService: false, + extraParameters: { + windowTitleSubstring: "test-window-substring", + extendedExpiryToken: "true", + prompt: "login", + nonce: "test-nonce", + claims: "test-claims", + }, + redirectUri: testRequest.redirectUri, + scope: testRequest.scope, + state: undefined, + storeInCache: undefined, + embeddedClientId: undefined, + }); + }); + + it("returned DOM request object should include user input extra parameters", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + const testRequest: PlatformAuthRequest = { + accountId: "test-id", + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + authority: TEST_CONFIG.validAuthority, + redirectUri: TEST_URIS.TEST_REDIR_URI, + scope: "read openid", + correlationId: TEST_CONFIG.CORRELATION_ID, + windowTitleSubstring: "test-window-substring", + prompt: "login", + nonce: "test-nonce", + claims: "test-claims", + extendedExpiryToken: true, + extraParameters: { + customUserInput1: "test-user-input1", + customUserInput2: "test-user-input2", + }, + }; + const platformDOMRequest = + //@ts-ignore + platformAuthDOMHandler.initializePlatformDOMRequest( + testRequest + ); + expect(platformDOMRequest).toEqual({ + accountId: testRequest.accountId, + brokerId: PlatformAuthConstants.MICROSOFT_ENTRA_BROKERID, + authority: testRequest.authority, + clientId: testRequest.clientId, + correlationId: testRequest.correlationId, + isSecurityTokenService: false, + extraParameters: { + windowTitleSubstring: "test-window-substring", + extendedExpiryToken: "true", + prompt: "login", + nonce: "test-nonce", + claims: "test-claims", + customUserInput1: "test-user-input1", + customUserInput2: "test-user-input2", + }, + redirectUri: testRequest.redirectUri, + scope: testRequest.scope, + state: undefined, + storeInCache: undefined, + embeddedClientId: undefined, + }); + }); + }); + + describe("validatePlatformBrokerResponse tests", () => { + it("should return a valid PlatformBrokerResponse object", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + const testResponse: object = { + isSuccess: true, + state: "", + accessToken: TEST_TOKENS.ACCESS_TOKEN, + expiresIn: 6000, + account: { + id: "test-id", + userName: "test-user", + properties: {}, + }, + clientInfo: "test-client-info", + idToken: TEST_TOKENS.IDTOKEN_V1, + scopes: "read openid", + error: {}, + properties: { + MATS: '{"MATS":"{"x_ms_clitelem":"1,0,0,,I","ui_visible":true}"}', + }, + extendedLifetimeToken: true, + }; + const validatedResponse = + //@ts-ignore + platformAuthDOMHandler.validatePlatformBrokerResponse( + testResponse + ); + expect(validatedResponse).toEqual({ + access_token: TEST_TOKENS.ACCESS_TOKEN, + account: { + id: "test-id", + userName: "test-user", + properties: {}, + }, + client_info: "test-client-info", + expires_in: 6000, + id_token: TEST_TOKENS.IDTOKEN_V1, + properties: { + MATS: '{"MATS":"{"x_ms_clitelem":"1,0,0,,I","ui_visible":true}"}', + }, + scope: "read openid", + state: "", + extendedLifetimeToken: true, + }); + }); + }); + + describe("getDOMExtraParams tests", () => { + it("should return a valid DOMExtraParameters object", async () => { + getSupportedContractsMock.mockResolvedValue([ + PlatformAuthConstants.PLATFORM_DOM_APIS, + ]); + const platformAuthDOMHandler = + await PlatformAuthDOMHandler.createProvider( + logger, + performanceClient, + "test-correlation-id" + ); + const testExtraParameters = { + prompt: PromptValue.NONE, + nonce: "test-nonce", + claims: "test-claims", + instanceAware: true, + windowTitleSubstring: "test-window-substring", + extendedExpiryToken: true, + signPopToken: true, + }; + const domExtraParams = + //@ts-ignore + platformAuthDOMHandler.getDOMExtraParams(testExtraParameters); + expect(domExtraParams).toEqual({ + prompt: "none", + nonce: "test-nonce", + claims: "test-claims", + instanceAware: "true", + windowTitleSubstring: "test-window-substring", + extendedExpiryToken: "true", + signPopToken: "true", + }); + }); + }); +}); diff --git a/lib/msal-browser/test/broker/NativeMessageHandler.spec.ts b/lib/msal-browser/test/broker/PlatformAuthExtensionHandler.spec.ts similarity index 80% rename from lib/msal-browser/test/broker/NativeMessageHandler.spec.ts rename to lib/msal-browser/test/broker/PlatformAuthExtensionHandler.spec.ts index fc0a7be3c8..d350e2dc8f 100644 --- a/lib/msal-browser/test/broker/NativeMessageHandler.spec.ts +++ b/lib/msal-browser/test/broker/PlatformAuthExtensionHandler.spec.ts @@ -9,20 +9,31 @@ import { AuthErrorMessage, IPerformanceClient, } from "@azure/msal-common"; -import { NativeMessageHandler } from "../../src/broker/nativeBroker/NativeMessageHandler.js"; +import { PlatformAuthExtensionHandler } from "../../src/broker/nativeBroker/PlatformAuthExtensionHandler.js"; import { BrowserAuthError, BrowserAuthErrorMessage } from "../../src/index.js"; import { NativeExtensionMethod } from "../../src/utils/BrowserConstants.js"; import { NativeAuthError } from "../../src/error/NativeAuthError.js"; import { getDefaultPerformanceClient } from "../utils/TelemetryUtils.js"; import { CryptoOps } from "../../src/crypto/CryptoOps.js"; +import { PlatformAuthRequest } from "../../src/broker/nativeBroker/PlatformAuthRequest.js"; +import { TEST_CONFIG, TEST_URIS } from "../utils/StringConstants.js"; let performanceClient: IPerformanceClient; -describe("NativeMessageHandler Tests", () => { +const TEST_REQUEST: PlatformAuthRequest = { + accountId: "test-account-id", + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + authority: TEST_CONFIG.validAuthority, + redirectUri: TEST_URIS.TEST_REDIR_URI, + scope: "User.Read", + correlationId: "test-correlation-id", + windowTitleSubstring: "", +}; + +describe("PlatformAuthExtensionHandler Tests", () => { let postMessageSpy: jest.SpyInstance; let mcPort: MessagePort; let cryptoInterface: CryptoOps; - globalThis.MessageChannel = require("worker_threads").MessageChannel; // jsdom does not include an implementation for MessageChannel beforeEach(() => { postMessageSpy = jest.spyOn(window, "postMessage"); @@ -62,12 +73,15 @@ describe("NativeMessageHandler Tests", () => { window.addEventListener("message", eventHandler, true); - const wamMessageHandler = await NativeMessageHandler.createProvider( - new Logger({}), - 2000, - performanceClient + const wamMessageHandler = + await PlatformAuthExtensionHandler.createProvider( + new Logger({}), + 2000, + performanceClient + ); + expect(wamMessageHandler).toBeInstanceOf( + PlatformAuthExtensionHandler ); - expect(wamMessageHandler).toBeInstanceOf(NativeMessageHandler); window.removeEventListener("message", eventHandler, true); }); @@ -111,7 +125,7 @@ describe("NativeMessageHandler Tests", () => { } ); - NativeMessageHandler.createProvider( + PlatformAuthExtensionHandler.createProvider( new Logger({}), 2000, performanceClient @@ -148,18 +162,21 @@ describe("NativeMessageHandler Tests", () => { window.addEventListener("message", eventHandler, true); - const wamMessageHandler = await NativeMessageHandler.createProvider( - new Logger({}), - 2000, - performanceClient + const wamMessageHandler = + await PlatformAuthExtensionHandler.createProvider( + new Logger({}), + 2000, + performanceClient + ); + expect(wamMessageHandler).toBeInstanceOf( + PlatformAuthExtensionHandler ); - expect(wamMessageHandler).toBeInstanceOf(NativeMessageHandler); window.removeEventListener("message", eventHandler, true); }); it("Throws if no extension is installed", (done) => { - NativeMessageHandler.createProvider( + PlatformAuthExtensionHandler.createProvider( new Logger({}), 2000, performanceClient @@ -182,7 +199,7 @@ describe("NativeMessageHandler Tests", () => { window.addEventListener("message", eventHandler, true); - NativeMessageHandler.createProvider( + PlatformAuthExtensionHandler.createProvider( new Logger({}), 2000, performanceClient @@ -220,7 +237,7 @@ describe("NativeMessageHandler Tests", () => { } ); - NativeMessageHandler.createProvider( + PlatformAuthExtensionHandler.createProvider( new Logger({}), 2000, performanceClient @@ -234,11 +251,21 @@ describe("NativeMessageHandler Tests", () => { describe("sendMessage", () => { it("Sends message to WAM extension", async () => { + const testWAMResponse = { + access_token: "test-access-token", + id_token: "test-id-token", + client_info: "test-client-info", + account: { + id: "test-account-id", + properties: {}, + userName: "test-user-name", + }, + scope: "read openid", + expires_in: "3600", + }; const testResponse = { status: "Success", - result: { - accessToken: "test-access-token", - }, + result: testWAMResponse, }; const eventHandler = function (event: MessageEvent) { event.stopImmediatePropagation(); @@ -276,16 +303,17 @@ describe("NativeMessageHandler Tests", () => { window.addEventListener("message", eventHandler, true); - const wamMessageHandler = await NativeMessageHandler.createProvider( - new Logger({}), - 2000, - performanceClient + const wamMessageHandler = + await PlatformAuthExtensionHandler.createProvider( + new Logger({}), + 2000, + performanceClient + ); + expect(wamMessageHandler).toBeInstanceOf( + PlatformAuthExtensionHandler ); - expect(wamMessageHandler).toBeInstanceOf(NativeMessageHandler); - const response = await wamMessageHandler.sendMessage({ - method: NativeExtensionMethod.GetToken, - }); + const response = await wamMessageHandler.sendMessage(TEST_REQUEST); expect(response).toEqual(testResponse.result); window.removeEventListener("message", eventHandler, true); @@ -333,22 +361,20 @@ describe("NativeMessageHandler Tests", () => { window.addEventListener("message", eventHandler, true); - NativeMessageHandler.createProvider( + PlatformAuthExtensionHandler.createProvider( new Logger({}), 2000, performanceClient ) .then((wamMessageHandler) => { - wamMessageHandler - .sendMessage({ method: NativeExtensionMethod.GetToken }) - .catch((e) => { - expect(e).toBeInstanceOf(NativeAuthError); - expect(e.errorCode).toEqual(testResponse.code); - expect(e.errorMessage).toEqual( - testResponse.description - ); - done(); - }); + wamMessageHandler.sendMessage(TEST_REQUEST).catch((e) => { + expect(e).toBeInstanceOf(NativeAuthError); + expect(e.errorCode).toEqual(testResponse.code); + expect(e.errorMessage).toEqual( + testResponse.description + ); + done(); + }); }) .finally(() => { window.removeEventListener("message", eventHandler, true); @@ -399,24 +425,20 @@ describe("NativeMessageHandler Tests", () => { window.addEventListener("message", eventHandler, true); - NativeMessageHandler.createProvider( + PlatformAuthExtensionHandler.createProvider( new Logger({}), 2000, performanceClient ) .then((wamMessageHandler) => { - wamMessageHandler - .sendMessage({ method: NativeExtensionMethod.GetToken }) - .catch((e) => { - expect(e).toBeInstanceOf(NativeAuthError); - expect(e.errorCode).toEqual( - testResponse.result.code - ); - expect(e.errorMessage).toEqual( - testResponse.result.description - ); - done(); - }); + wamMessageHandler.sendMessage(TEST_REQUEST).catch((e) => { + expect(e).toBeInstanceOf(NativeAuthError); + expect(e.errorCode).toEqual(testResponse.result.code); + expect(e.errorMessage).toEqual( + testResponse.result.description + ); + done(); + }); }) .finally(() => { window.removeEventListener("message", eventHandler, true); @@ -463,24 +485,22 @@ describe("NativeMessageHandler Tests", () => { window.addEventListener("message", eventHandler, true); - NativeMessageHandler.createProvider( + PlatformAuthExtensionHandler.createProvider( new Logger({}), 2000, performanceClient ) .then((wamMessageHandler) => { - wamMessageHandler - .sendMessage({ method: NativeExtensionMethod.GetToken }) - .catch((e) => { - expect(e).toBeInstanceOf(AuthError); - expect(e.errorCode).toEqual( - AuthErrorMessage.unexpectedError.code - ); - expect(e.errorMessage).toContain( - AuthErrorMessage.unexpectedError.desc - ); - done(); - }); + wamMessageHandler.sendMessage(TEST_REQUEST).catch((e) => { + expect(e).toBeInstanceOf(AuthError); + expect(e.errorCode).toEqual( + AuthErrorMessage.unexpectedError.code + ); + expect(e.errorMessage).toContain( + AuthErrorMessage.unexpectedError.desc + ); + done(); + }); }) .finally(() => { window.removeEventListener("message", eventHandler, true); diff --git a/lib/msal-browser/test/broker/PlatformAuthProvider.spec.ts b/lib/msal-browser/test/broker/PlatformAuthProvider.spec.ts new file mode 100644 index 0000000000..f2c72a693b --- /dev/null +++ b/lib/msal-browser/test/broker/PlatformAuthProvider.spec.ts @@ -0,0 +1,277 @@ +import { + Logger, + IPerformanceClient, + AuthenticationScheme, +} from "@azure/msal-common/browser"; +import * as PlatformAuthProvider from "../../src/broker/nativeBroker/PlatformAuthProvider.js"; +import { getDefaultPerformanceClient } from "../utils/TelemetryUtils.js"; +import { PlatformAuthDOMHandler } from "../../src/broker/nativeBroker/PlatformAuthDOMHandler.js"; +import { + BrowserConfiguration, + buildConfiguration, + Configuration, +} from "../../src/config/Configuration.js"; +import { PlatformAuthExtensionHandler } from "../../src/broker/nativeBroker/PlatformAuthExtensionHandler.js"; +import exp from "constants"; +import { log } from "console"; +import { BrowserAuthError } from "../../src/index.js"; + +describe("PlatformAuthProvider tests", () => { + function stubExtensionProvider() { + return jest + .spyOn(PlatformAuthExtensionHandler, "createProvider") + .mockImplementation(async () => { + return new PlatformAuthExtensionHandler( + logger, + 2000, + performanceClient, + "test-extensionId" + ); + }); + } + + function stubDOMProvider() { + return jest + .spyOn(PlatformAuthDOMHandler, "createProvider") + .mockImplementation(async () => { + return new PlatformAuthDOMHandler( + logger, + performanceClient, + "test-correlation-id" + ); + }); + } + + let performanceClient: IPerformanceClient; + let logger: Logger; + beforeEach(() => { + performanceClient = getDefaultPerformanceClient(); + logger = new Logger({}, "test", "1.0.0"); + }); + afterEach(() => { + window.sessionStorage.clear(); + jest.restoreAllMocks(); + }); + + describe("isPlatformBrokerAvailable tests", () => { + it("should return false if application is not in browser environment", async () => { + const { window } = global; + //@ts-ignore + delete global.window; + const result = await PlatformAuthProvider.isPlatformBrokerAvailable( + {}, + performanceClient, + "test-correlation-id" + ); + expect(result).toBe(false); + global.window = window; // Restore window + }); + + it("should return true if its a browser app and dom handler is available", async () => { + window.sessionStorage.setItem( + "msal.browser.platform.auth.dom", + "true" + ); + const getItemSpy = jest.spyOn( + Object.getPrototypeOf(sessionStorage), + "getItem" + ); + + const domProviderSpy = stubDOMProvider(); + + const result = + await PlatformAuthProvider.isPlatformBrokerAvailable(); + expect(result).toBe(true); + expect(getItemSpy).toHaveBeenCalled(); + expect(domProviderSpy).toHaveBeenCalled(); + }); + + it("should return true if its a browser app and extension handler is available", async () => { + const extensionProviderSpy = stubExtensionProvider(); + + const result = + await PlatformAuthProvider.isPlatformBrokerAvailable(); + expect(result).toBe(true); + expect(extensionProviderSpy).toHaveBeenCalled(); + }); + + it("should return false if no handler is available", async () => { + jest.spyOn( + PlatformAuthDOMHandler, + "createProvider" + ).mockImplementation(async () => { + throw new Error("No handler available"); + }); + + jest.spyOn( + PlatformAuthExtensionHandler, + "createProvider" + ).mockImplementation(async () => { + throw new Error("No handler available"); + }); + + const result = + await PlatformAuthProvider.isPlatformBrokerAvailable(); + expect(result).toBe(false); + }); + }); + + describe("getPlatformAuthProvider tests", () => { + it("should return undefined if no provider is available", async () => { + jest.spyOn( + PlatformAuthDOMHandler, + "createProvider" + ).mockImplementation(async () => { + throw new Error("No handler available"); + }); + + jest.spyOn( + PlatformAuthExtensionHandler, + "createProvider" + ).mockImplementation(async () => { + throw new Error("No handler available"); + }); + + const result = await PlatformAuthProvider.getPlatformAuthProvider( + logger, + performanceClient, + "test-correlation-id" + ); + expect(result).toBe(undefined); + }); + + it("returns dom handler when available", async () => { + window.sessionStorage.setItem( + "msal.browser.platform.auth.dom", + "true" + ); + const getItemSpy = jest.spyOn( + Object.getPrototypeOf(sessionStorage), + "getItem" + ); + + const domProviderSpy = stubDOMProvider(); + + const result = await PlatformAuthProvider.getPlatformAuthProvider( + logger, + performanceClient, + "test-correlation-id" + ); + expect(result).not.toBe(undefined); + expect(result).toBeInstanceOf(PlatformAuthDOMHandler); + expect(getItemSpy).toHaveBeenCalled(); + expect(domProviderSpy).toHaveBeenCalled(); + }); + + it("returns extension handler if dom APIs are not available and extension is available", async () => { + const extensionProviderSpy = stubExtensionProvider(); + + const result = await PlatformAuthProvider.getPlatformAuthProvider( + logger, + performanceClient, + "test-correlation-id" + ); + expect(result).not.toBe(undefined); + expect(result).toBeInstanceOf(PlatformAuthExtensionHandler); + expect(extensionProviderSpy).toHaveBeenCalled(); + }); + }); + + describe("isDomEnabledForPlatformAuth tests", () => { + it("should return false if session storage is not set with DOM API flag", () => { + expect(PlatformAuthProvider.isDomEnabledForPlatformAuth()).toBe( + false + ); + }); + + it("should return true if session storage is set with DOM API flag", () => { + window.sessionStorage.setItem( + "msal.browser.platform.auth.dom", + "true" + ); + expect(PlatformAuthProvider.isDomEnabledForPlatformAuth()).toBe( + true + ); + }); + + it("should return false if session storage errors out", () => { + const { sessionStorage } = global.window; + //@ts-ignore + delete global.window.sessionStorage; + const result = PlatformAuthProvider.isDomEnabledForPlatformAuth(); + expect(result).toBe(false); + global.window.sessionStorage = sessionStorage; // Restore sessionStorage + }); + }); + + describe("isPlatformAuthAllowed", () => { + let config: BrowserConfiguration; + beforeEach(() => { + config = buildConfiguration( + { + auth: { + clientId: "test-client-id", + authority: "https://login.microsoftonline.com/common", + redirectUri: "http://localhost", + }, + system: { + allowPlatformBroker: true, + }, + }, + true + ); + }); + + it("returns false when config is not set to enable paltform broker", () => { + config.system.allowPlatformBroker = false; + const result = PlatformAuthProvider.isPlatformAuthAllowed( + config, + logger, + new PlatformAuthDOMHandler( + logger, + performanceClient, + "test-correlation-id" + ), + AuthenticationScheme.BEARER + ); + expect(result).toBe(false); + }); + + it("returns false when platform auth provider is not initialized", () => { + const result = PlatformAuthProvider.isPlatformAuthAllowed( + config, + logger, + undefined, + AuthenticationScheme.BEARER + ); + expect(result).toBe(false); + }); + it("returns false when authentication scheme is not supported", () => { + const result = PlatformAuthProvider.isPlatformAuthAllowed( + config, + logger, + new PlatformAuthDOMHandler( + logger, + performanceClient, + "test-correlation-id" + ), + "unknown-scheme" as AuthenticationScheme + ); + expect(result).toBe(false); + }); + + it("returns true when platform auth provider is initialized and authentication scheme is supported", () => { + const result = PlatformAuthProvider.isPlatformAuthAllowed( + config, + logger, + new PlatformAuthDOMHandler( + logger, + performanceClient, + "test-correlation-id" + ), + AuthenticationScheme.BEARER + ); + expect(result).toBe(true); + }); + }); +}); diff --git a/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts b/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts index ab97e897b2..805a609ad6 100644 --- a/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts +++ b/lib/msal-browser/test/cache/BrowserCacheManager.spec.ts @@ -16,9 +16,7 @@ import { import { CacheOptions } from "../../src/config/Configuration.js"; import { Constants, - PersistentCacheKeys, CommonAuthorizationCodeRequest as AuthorizationCodeRequest, - ProtocolUtils, Logger, LogLevel, AuthenticationScheme, @@ -35,21 +33,23 @@ import { CacheManager, PerformanceEvent, StubPerformanceClient, + CommonAuthorizationUrlRequest, + ResponseMode, } from "@azure/msal-common"; import { BrowserCacheLocation, - InteractionType, + INTERACTION_TYPE, + StaticCacheKeys, TemporaryCacheKeys, } from "../../src/utils/BrowserConstants.js"; import { CryptoOps } from "../../src/crypto/CryptoOps.js"; import { DatabaseStorage } from "../../src/cache/DatabaseStorage.js"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; -import { BrowserStateObject } from "../../src/utils/BrowserProtocolUtils.js"; import { base64Decode } from "../../src/encode/Base64Decode.js"; -import { getDefaultPerformanceClient } from "../utils/TelemetryUtils.js"; import { BrowserPerformanceClient } from "../../src/telemetry/BrowserPerformanceClient.js"; import { CookieStorage } from "../../src/cache/CookieStorage.js"; import { EventHandler } from "../../src/event/EventHandler.js"; +import { version } from "../../src/packageMetadata.js"; describe("BrowserCacheManager tests", () => { let cacheConfig: Required; @@ -137,6 +137,85 @@ describe("BrowserCacheManager tests", () => { }); }); + describe("initialize", () => { + it("sets MSAL version in localStorage if not already set", async () => { + const browserCacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + { + ...cacheConfig, + cacheLocation: BrowserCacheLocation.LocalStorage, + }, + browserCrypto, + logger, + new StubPerformanceClient(), + new EventHandler() + ); + await browserCacheManager.initialize(TEST_CONFIG.CORRELATION_ID); + expect(window.localStorage.getItem(StaticCacheKeys.VERSION)).toBe( + version + ); + }); + + it("sets MSAL version in localStorage if previous version doesn't match", async () => { + window.localStorage.setItem(StaticCacheKeys.VERSION, "1.0.0"); + const browserCacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + { + ...cacheConfig, + cacheLocation: BrowserCacheLocation.LocalStorage, + }, + browserCrypto, + logger, + new StubPerformanceClient(), + new EventHandler() + ); + await browserCacheManager.initialize(TEST_CONFIG.CORRELATION_ID); + expect(window.localStorage.getItem(StaticCacheKeys.VERSION)).toBe( + version + ); + }); + + it("does not set MSAL version in localStorage if existing version already matches", async () => { + // First make sure the version gets set + const browserCacheManager1 = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + { + ...cacheConfig, + cacheLocation: BrowserCacheLocation.LocalStorage, + }, + browserCrypto, + logger, + new StubPerformanceClient(), + new EventHandler() + ); + await browserCacheManager1.initialize(TEST_CONFIG.CORRELATION_ID); + expect(window.localStorage.getItem(StaticCacheKeys.VERSION)).toBe( + version + ); + + const setSpy = jest.spyOn(Storage.prototype, "setItem"); + const browserCacheManager2 = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + { + ...cacheConfig, + cacheLocation: BrowserCacheLocation.LocalStorage, + }, + browserCrypto, + logger, + new StubPerformanceClient(), + new EventHandler() + ); + await browserCacheManager2.initialize(TEST_CONFIG.CORRELATION_ID); + expect(window.localStorage.getItem(StaticCacheKeys.VERSION)).toBe( + version + ); + expect(setSpy).not.toHaveBeenCalledWith( + StaticCacheKeys.VERSION, + expect.anything() + ); + }); + }); + describe("Interface functions", () => { let browserSessionStorage: BrowserCacheManager; let authority: Authority; @@ -189,8 +268,8 @@ describe("BrowserCacheManager tests", () => { }); afterEach(async () => { - await browserSessionStorage.clear(); - await browserLocalStorage.clear(); + browserSessionStorage.clear(RANDOM_TEST_GUID); + browserLocalStorage.clear(RANDOM_TEST_GUID); }); it("setTemporaryCache", () => { @@ -221,12 +300,588 @@ describe("BrowserCacheManager tests", () => { }); it("setItem", () => { - window.sessionStorage.setItem(msalCacheKey, cacheVal); - window.localStorage.setItem(msalCacheKey2, cacheVal); + browserSessionStorage.setItem( + msalCacheKey, + cacheVal, + RANDOM_TEST_GUID + ); + browserLocalStorage.setItem( + msalCacheKey2, + cacheVal, + RANDOM_TEST_GUID + ); expect(window.sessionStorage.getItem(msalCacheKey)).toBe(cacheVal); expect(window.localStorage.getItem(msalCacheKey2)).toBe(cacheVal); }); + it("setItem removes old access tokens if cache quota is reached", async () => { + const browserCacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + cacheConfig, + browserCrypto, + logger, + new StubPerformanceClient(), + new EventHandler() + ); + // Create a real AccessTokenEntity to be removed + const accessToken1 = CacheHelpers.createAccessTokenEntity( + "homeAccountId", + "environment", + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + "tenantId", + "openid", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const accessToken2 = CacheHelpers.createAccessTokenEntity( + "homeAccountId2", + "environment2", + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + "tenantId2", + "openid2", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const atKey1 = CacheHelpers.generateCredentialKey(accessToken1); + const atKey2 = CacheHelpers.generateCredentialKey(accessToken2); + await browserCacheManager.setAccessTokenCredential( + accessToken1, + RANDOM_TEST_GUID + ); + await browserCacheManager.setAccessTokenCredential( + accessToken2, + RANDOM_TEST_GUID + ); + expect(window.sessionStorage.getItem(atKey1)).toBe( + JSON.stringify(accessToken1) + ); + expect(window.sessionStorage.getItem(atKey2)).toBe( + JSON.stringify(accessToken2) + ); + expect(browserCacheManager.getTokenKeys().accessToken).toEqual([ + atKey1, + atKey2, + ]); + + // Create a new AccessTokenEntity to be removed + const newCacheKey = "test-cache-entry"; + const newCacheVal = "test-cache-value"; + + // Simulate quota exceeded error on first setItem call, then succeed + const setItemSpy = jest + .spyOn(Storage.prototype, "setItem") + .mockImplementationOnce(() => { + const error: any = new DOMException( + "The quota has been exceeded", + "QuotaExceededError" + ); + throw error; + }); + + browserCacheManager.setItem( + newCacheKey, + newCacheVal, + RANDOM_TEST_GUID + ); + + // The access token should have been removed from storage + expect(window.sessionStorage.getItem(atKey1)).toBeNull(); + expect(window.sessionStorage.getItem(atKey2)).toBe( + JSON.stringify(accessToken2) + ); + // The new item should be set + expect(window.sessionStorage.getItem(newCacheKey)).toBe( + newCacheVal + ); + // The token keys should be updated (accessToken array should be empty) + expect(browserCacheManager.getTokenKeys().accessToken).toEqual([ + atKey2, + ]); + + expect(setItemSpy).toHaveBeenCalledTimes(3); + }); + + it("setItem throws error if cache quota is reached and there are no access tokens left to remove", async () => { + const browserCacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + cacheConfig, + browserCrypto, + logger, + new StubPerformanceClient(), + new EventHandler() + ); + // Create a real AccessTokenEntity to be removed + const accessToken1 = CacheHelpers.createAccessTokenEntity( + "homeAccountId", + "environment", + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + "tenantId", + "openid", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const accessToken2 = CacheHelpers.createAccessTokenEntity( + "homeAccountId2", + "environment2", + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + "tenantId2", + "openid2", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const accessToken3 = CacheHelpers.createAccessTokenEntity( + "homeAccountId3", + "environment3", + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + "tenantId3", + "openid3", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const atKey1 = CacheHelpers.generateCredentialKey(accessToken1); + const atKey2 = CacheHelpers.generateCredentialKey(accessToken2); + const atKey3 = CacheHelpers.generateCredentialKey(accessToken3); + await browserCacheManager.setAccessTokenCredential( + accessToken1, + RANDOM_TEST_GUID + ); + await browserCacheManager.setAccessTokenCredential( + accessToken2, + RANDOM_TEST_GUID + ); + await browserCacheManager.setAccessTokenCredential( + accessToken3, + RANDOM_TEST_GUID + ); + expect(window.sessionStorage.getItem(atKey1)).toBe( + JSON.stringify(accessToken1) + ); + expect(window.sessionStorage.getItem(atKey2)).toBe( + JSON.stringify(accessToken2) + ); + expect(window.sessionStorage.getItem(atKey3)).toBe( + JSON.stringify(accessToken3) + ); + expect(browserCacheManager.getTokenKeys().accessToken).toEqual([ + atKey1, + atKey2, + atKey3, + ]); + + // Create a new AccessTokenEntity to be removed + const newCacheKey = "test-cache-entry"; + const newCacheVal = "test-cache-value"; + + const spy = jest + .spyOn(Storage.prototype, "setItem") + .mockImplementation(() => { + const error: any = new DOMException( + "The quota has been exceeded", + "QuotaExceededError" + ); + throw error; + }); + + expect(() => + browserCacheManager.setItem( + newCacheKey, + newCacheVal, + RANDOM_TEST_GUID + ) + ).toThrow(new CacheError(CacheErrorCodes.cacheQuotaExceeded)); + + // The access token should have been removed from storage + expect(window.sessionStorage.getItem(atKey1)).toBeNull(); + expect(window.sessionStorage.getItem(atKey2)).toBeNull(); + expect(window.sessionStorage.getItem(atKey3)).toBeNull(); + expect(window.sessionStorage.getItem(newCacheKey)).toBeNull(); + expect(browserCacheManager.getTokenKeys().accessToken).toHaveLength( + 3 // Failed to update token keys map, so it should still contain all 3 keys + ); + expect(spy).toHaveBeenCalledTimes(4); // First attempt + 3 attempts after each access token removed + }); + + it("setItem throws error if cache quota is reached and 20 access tokens have already been removed", async () => { + const browserCacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + cacheConfig, + browserCrypto, + logger, + new StubPerformanceClient(), + new EventHandler() + ); + + const atKeys = []; + for (let i = 0; i < 25; i++) { + const accessToken = CacheHelpers.createAccessTokenEntity( + `homeAccountId${i}`, + `environment${i}`, + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + `tenantId${i}`, + `openid${i}`, + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const atKey = CacheHelpers.generateCredentialKey(accessToken); + atKeys.push(atKey); + await browserCacheManager.setAccessTokenCredential( + accessToken, + RANDOM_TEST_GUID + ); + expect(window.sessionStorage.getItem(atKey)).toBe( + JSON.stringify(accessToken) + ); + } + expect(browserCacheManager.getTokenKeys().accessToken).toEqual( + atKeys + ); + + // Create a new AccessTokenEntity to be removed + const newCacheKey = "test-cache-entry"; + const newCacheVal = "test-cache-value"; + + const spy = jest + // @ts-ignore + .spyOn(browserCacheManager.browserStorage, "setItem") + .mockImplementation(() => { + const error: any = new DOMException( + "The quota has been exceeded", + "QuotaExceededError" + ); + throw error; + }); + + expect(() => + browserCacheManager.setItem( + newCacheKey, + newCacheVal, + RANDOM_TEST_GUID + ) + ).toThrow(new CacheError(CacheErrorCodes.cacheQuotaExceeded)); + + // The access token should have been removed from storage + for (let i = 0; i < 20; i++) { + expect(window.sessionStorage.getItem(atKeys[i])).toBeNull(); + } + for (let i = 20; i < 25; i++) { + expect(window.sessionStorage.getItem(atKeys[i])).not.toBeNull(); + } + expect(window.sessionStorage.getItem(newCacheKey)).toBeNull(); + expect(browserCacheManager.getTokenKeys().accessToken).toHaveLength( + 25 // Failed to update the token keys map, so it should still contain all 25 keys + ); + expect(spy).toHaveBeenCalledTimes(21); // First attempt + 20 attempts after each access token removed + }); + + it("setUserData removes old access tokens if cache quota is reached", async () => { + const browserCacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + cacheConfig, + browserCrypto, + logger, + new StubPerformanceClient(), + new EventHandler() + ); + // Create a real AccessTokenEntity to be removed + const accessToken1 = CacheHelpers.createAccessTokenEntity( + "homeAccountId", + "environment", + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + "tenantId", + "openid", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const accessToken2 = CacheHelpers.createAccessTokenEntity( + "homeAccountId2", + "environment2", + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + "tenantId2", + "openid2", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const atKey1 = CacheHelpers.generateCredentialKey(accessToken1); + const atKey2 = CacheHelpers.generateCredentialKey(accessToken2); + await browserCacheManager.setAccessTokenCredential( + accessToken1, + RANDOM_TEST_GUID + ); + await browserCacheManager.setAccessTokenCredential( + accessToken2, + RANDOM_TEST_GUID + ); + expect(window.sessionStorage.getItem(atKey1)).toBe( + JSON.stringify(accessToken1) + ); + expect(window.sessionStorage.getItem(atKey2)).toBe( + JSON.stringify(accessToken2) + ); + expect(browserCacheManager.getTokenKeys().accessToken).toEqual([ + atKey1, + atKey2, + ]); + + // Create a new AccessTokenEntity to be removed + const newCacheKey = "test-cache-entry"; + const newCacheVal = "test-cache-value"; + + // Simulate quota exceeded error on first setItem call, then succeed + const setItemSpy = jest + .spyOn(Storage.prototype, "setItem") + .mockImplementationOnce(() => { + const error: any = new DOMException( + "The quota has been exceeded", + "QuotaExceededError" + ); + throw error; + }); + + await browserCacheManager.setUserData( + newCacheKey, + newCacheVal, + RANDOM_TEST_GUID, + Date.now().toString() + ); + + // The access token should have been removed from storage + expect(window.sessionStorage.getItem(atKey1)).toBeNull(); + expect(window.sessionStorage.getItem(atKey2)).toBe( + JSON.stringify(accessToken2) + ); + // The new item should be set + expect(window.sessionStorage.getItem(newCacheKey)).toBe( + newCacheVal + ); + // The token keys should be updated (accessToken array should be empty) + expect(browserCacheManager.getTokenKeys().accessToken).toEqual([ + atKey2, + ]); + + expect(setItemSpy).toHaveBeenCalledTimes(3); + }); + + it("setUserData throws error if cache quota is reached and there are no access tokens left to remove", async () => { + const browserCacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + cacheConfig, + browserCrypto, + logger, + new StubPerformanceClient(), + new EventHandler() + ); + const atKeys = []; + for (let i = 0; i < 25; i++) { + const accessToken = CacheHelpers.createAccessTokenEntity( + `homeAccountId${i}`, + `environment${i}`, + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + `tenantId${i}`, + `openid${i}`, + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const atKey = CacheHelpers.generateCredentialKey(accessToken); + atKeys.push(atKey); + await browserCacheManager.setAccessTokenCredential( + accessToken, + RANDOM_TEST_GUID + ); + expect(window.sessionStorage.getItem(atKey)).toBe( + JSON.stringify(accessToken) + ); + } + expect(browserCacheManager.getTokenKeys().accessToken).toEqual( + atKeys + ); + + // Create a new AccessTokenEntity to be removed + const newCacheKey = "test-cache-entry"; + const newCacheVal = "test-cache-value"; + const spy = jest + .spyOn(Storage.prototype, "setItem") + .mockImplementation(() => { + const error: any = new DOMException( + "The quota has been exceeded", + "QuotaExceededError" + ); + throw error; + }); + await expect(() => + browserCacheManager.setUserData( + newCacheKey, + newCacheVal, + RANDOM_TEST_GUID, + Date.now().toString() + ) + ).rejects.toEqual( + new CacheError(CacheErrorCodes.cacheQuotaExceeded) + ); + + // The access token should have been removed from storage + for (let i = 0; i < 20; i++) { + expect(window.sessionStorage.getItem(atKeys[i])).toBeNull(); + } + for (let i = 20; i < 25; i++) { + expect(window.sessionStorage.getItem(atKeys[i])).not.toBeNull(); + } + expect(window.sessionStorage.getItem(newCacheKey)).toBeNull(); + expect(browserCacheManager.getTokenKeys().accessToken).toHaveLength( + 25 // Failed to update token keys map, so it should still contain all 25 keys + ); + expect(spy).toHaveBeenCalledTimes(21); // First attempt + an attempt after each access token removed + }); + + it("setUserData throws error if cache quota is reached and there are no access tokens left to remove", async () => { + const browserCacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + cacheConfig, + browserCrypto, + logger, + new StubPerformanceClient(), + new EventHandler() + ); + // Create a real AccessTokenEntity to be removed + const accessToken1 = CacheHelpers.createAccessTokenEntity( + "homeAccountId", + "environment", + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + "tenantId", + "openid", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const accessToken2 = CacheHelpers.createAccessTokenEntity( + "homeAccountId2", + "environment2", + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + "tenantId2", + "openid2", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const accessToken3 = CacheHelpers.createAccessTokenEntity( + "homeAccountId3", + "environment3", + TEST_TOKENS.ACCESS_TOKEN, + TEST_CONFIG.MSAL_CLIENT_ID, + "tenantId3", + "openid3", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER + ); + const atKey1 = CacheHelpers.generateCredentialKey(accessToken1); + const atKey2 = CacheHelpers.generateCredentialKey(accessToken2); + const atKey3 = CacheHelpers.generateCredentialKey(accessToken3); + await browserCacheManager.setAccessTokenCredential( + accessToken1, + RANDOM_TEST_GUID + ); + await browserCacheManager.setAccessTokenCredential( + accessToken2, + RANDOM_TEST_GUID + ); + await browserCacheManager.setAccessTokenCredential( + accessToken3, + RANDOM_TEST_GUID + ); + expect(window.sessionStorage.getItem(atKey1)).toBe( + JSON.stringify(accessToken1) + ); + expect(window.sessionStorage.getItem(atKey2)).toBe( + JSON.stringify(accessToken2) + ); + expect(window.sessionStorage.getItem(atKey3)).toBe( + JSON.stringify(accessToken3) + ); + expect(browserCacheManager.getTokenKeys().accessToken).toEqual([ + atKey1, + atKey2, + atKey3, + ]); + + // Create a new AccessTokenEntity to be removed + const newCacheKey = "test-cache-entry"; + const newCacheVal = "test-cache-value"; + const spy = jest + // @ts-ignore + .spyOn(browserCacheManager.browserStorage, "setItem") + .mockImplementation(() => { + const error: any = new DOMException( + "The quota has been exceeded", + "QuotaExceededError" + ); + throw error; + }); + await expect(() => + browserCacheManager.setUserData( + newCacheKey, + newCacheVal, + RANDOM_TEST_GUID, + Date.now().toString() + ) + ).rejects.toEqual( + new CacheError(CacheErrorCodes.cacheQuotaExceeded) + ); + + // The access token should have been removed from storage + expect(window.sessionStorage.getItem(atKey1)).toBeNull(); + expect(window.sessionStorage.getItem(atKey2)).toBeNull(); + expect(window.sessionStorage.getItem(atKey3)).toBeNull(); + expect(window.sessionStorage.getItem(newCacheKey)).toBeNull(); + expect(browserCacheManager.getTokenKeys().accessToken).toHaveLength( + 3 // Failed to update token keys map, so it should still contain all 3 keys + ); + expect(spy).toHaveBeenCalledTimes(4); // First attempt + 3 attempts after each access token removed + }); + it("removeItem()", () => { browserSessionStorage.setTemporaryCache("cacheKey", cacheVal, true); browserLocalStorage.setTemporaryCache("cacheKey", cacheVal, true); @@ -248,16 +903,17 @@ describe("BrowserCacheManager tests", () => { expect(browserLocalStorage.getKeys()).toEqual([ "msal.account.keys", `msal.token.keys.${TEST_CONFIG.MSAL_CLIENT_ID}`, + StaticCacheKeys.VERSION, msalCacheKey, msalCacheKey2, ]); }); - it("clear()", async () => { + it("clear()", () => { browserSessionStorage.setTemporaryCache("cacheKey", cacheVal, true); browserLocalStorage.setTemporaryCache("cacheKey", cacheVal, true); - await browserSessionStorage.clear(); - await browserLocalStorage.clear(); + browserSessionStorage.clear(RANDOM_TEST_GUID); + browserLocalStorage.clear(RANDOM_TEST_GUID); expect(browserSessionStorage.getKeys()).toHaveLength(0); expect(browserLocalStorage.getKeys()).toHaveLength(0); }); @@ -266,8 +922,12 @@ describe("BrowserCacheManager tests", () => { describe("Account", () => { it("getAccount returns null if key not in cache", () => { const key = "not-in-cache"; - expect(browserSessionStorage.getAccount(key)).toBeNull(); - expect(browserLocalStorage.getAccount(key)).toBeNull(); + expect( + browserSessionStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); + expect( + browserLocalStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); }); it("getAccount returns null if value is not JSON", () => { @@ -275,8 +935,12 @@ describe("BrowserCacheManager tests", () => { window.localStorage.setItem(key, "this is not json"); window.sessionStorage.setItem(key, "this is not json"); - expect(browserSessionStorage.getAccount(key)).toBeNull(); - expect(browserLocalStorage.getAccount(key)).toBeNull(); + expect( + browserSessionStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); + expect( + browserLocalStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); }); it("getAccount returns null if value is not account entity", () => { @@ -294,8 +958,12 @@ describe("BrowserCacheManager tests", () => { JSON.stringify(partialAccount) ); - expect(browserSessionStorage.getAccount(key)).toBeNull(); - expect(browserLocalStorage.getAccount(key)).toBeNull(); + expect( + browserSessionStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); + expect( + browserLocalStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); }); it("getAccount returns AccountEntity", async () => { @@ -318,29 +986,33 @@ describe("BrowserCacheManager tests", () => { testAccount, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccount( - testAccount, - TEST_CONFIG.CORRELATION_ID - ); - expect( - browserSessionStorage.getAccount( - testAccount.generateAccountKey() + browserLocalStorage.getAccount( + testAccount.generateAccountKey(), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testAccount); expect( - browserSessionStorage.getAccount( - testAccount.generateAccountKey() + browserLocalStorage.getAccount( + testAccount.generateAccountKey(), + TEST_CONFIG.CORRELATION_ID ) ).toBeInstanceOf(AccountEntity); + + await browserSessionStorage.setAccount( + testAccount, + TEST_CONFIG.CORRELATION_ID + ); expect( - browserLocalStorage.getAccount( - testAccount.generateAccountKey() + browserSessionStorage.getAccount( + testAccount.generateAccountKey(), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testAccount); expect( - browserLocalStorage.getAccount( - testAccount.generateAccountKey() + browserSessionStorage.getAccount( + testAccount.generateAccountKey(), + TEST_CONFIG.CORRELATION_ID ) ).toBeInstanceOf(AccountEntity); }); @@ -350,10 +1022,16 @@ describe("BrowserCacheManager tests", () => { it("getIdTokenCredential returns null if key not in cache", () => { const key = "not-in-cache"; expect( - browserSessionStorage.getIdTokenCredential(key) + browserSessionStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getIdTokenCredential(key) + browserLocalStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -363,10 +1041,16 @@ describe("BrowserCacheManager tests", () => { window.sessionStorage.setItem(key, "this is not json"); expect( - browserSessionStorage.getIdTokenCredential(key) + browserSessionStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getIdTokenCredential(key) + browserLocalStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -386,10 +1070,16 @@ describe("BrowserCacheManager tests", () => { ); expect( - browserSessionStorage.getIdTokenCredential(key) + browserSessionStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getIdTokenCredential(key) + browserLocalStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -406,19 +1096,21 @@ describe("BrowserCacheManager tests", () => { testIdToken, TEST_CONFIG.CORRELATION_ID ); + expect( + browserLocalStorage.getIdTokenCredential( + CacheHelpers.generateCredentialKey(testIdToken), + TEST_CONFIG.CORRELATION_ID + ) + ).toEqual(testIdToken); + await browserSessionStorage.setIdTokenCredential( testIdToken, TEST_CONFIG.CORRELATION_ID ); - expect( browserSessionStorage.getIdTokenCredential( - CacheHelpers.generateCredentialKey(testIdToken) - ) - ).toEqual(testIdToken); - expect( - browserLocalStorage.getIdTokenCredential( - CacheHelpers.generateCredentialKey(testIdToken) + CacheHelpers.generateCredentialKey(testIdToken), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testIdToken); }); @@ -428,10 +1120,16 @@ describe("BrowserCacheManager tests", () => { it("getAccessTokenCredential returns null if key not in cache", () => { const key = "not-in-cache"; expect( - browserSessionStorage.getAccessTokenCredential(key) + browserSessionStorage.getAccessTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getAccessTokenCredential(key) + browserLocalStorage.getAccessTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -441,10 +1139,16 @@ describe("BrowserCacheManager tests", () => { window.sessionStorage.setItem(key, "this is not json"); expect( - browserSessionStorage.getAccessTokenCredential(key) + browserSessionStorage.getAccessTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getAccessTokenCredential(key) + browserLocalStorage.getAccessTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -463,73 +1167,26 @@ describe("BrowserCacheManager tests", () => { JSON.stringify(partialAccessTokenEntity) ); - expect( - browserSessionStorage.getAccessTokenCredential(key) - ).toBeNull(); - expect( - browserLocalStorage.getAccessTokenCredential(key) - ).toBeNull(); - }); - - it("getAccessTokenCredential returns AccessTokenEntity", async () => { - const testAccessToken = - CacheHelpers.createAccessTokenEntity( - "homeAccountId", - "environment", - TEST_TOKENS.ACCESS_TOKEN, - "client-id", - "tenantId", - "openid", - 1000, - 1000, - browserCrypto.base64Decode, - 500, - AuthenticationScheme.BEARER, - "oboAssertion" - ); - - await browserLocalStorage.setAccessTokenCredential( - testAccessToken, - TEST_CONFIG.CORRELATION_ID - ); - await browserSessionStorage.setAccessTokenCredential( - testAccessToken, - TEST_CONFIG.CORRELATION_ID - ); - expect( browserSessionStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAccessToken) + key, + RANDOM_TEST_GUID ) - ).toEqual(testAccessToken); + ).toBeNull(); expect( browserLocalStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAccessToken) - ) - ).toEqual(testAccessToken); - }); - - it("getAccessTokenCredential returns Bearer access token when authentication scheme is set to Bearer and both a Bearer and pop token are in the cache", async () => { - const testAccessTokenWithoutAuthScheme = - CacheHelpers.createAccessTokenEntity( - "homeAccountId", - "environment", - TEST_TOKENS.ACCESS_TOKEN, - "client-id", - "tenantId", - "openid", - 1000, - 1000, - browserCrypto.base64Decode, - 500, - AuthenticationScheme.BEARER, - "oboAssertion" - ); - const testAccessTokenWithAuthScheme = + key, + RANDOM_TEST_GUID + ) + ).toBeNull(); + }); + + it("getAccessTokenCredential returns AccessTokenEntity", async () => { + const testAccessToken = CacheHelpers.createAccessTokenEntity( "homeAccountId", "environment", - TEST_TOKENS.POP_TOKEN, + TEST_TOKENS.ACCESS_TOKEN, "client-id", "tenantId", "openid", @@ -537,18 +1194,31 @@ describe("BrowserCacheManager tests", () => { 1000, browserCrypto.base64Decode, 500, - AuthenticationScheme.POP, + AuthenticationScheme.BEARER, "oboAssertion" ); - // Cache bearer token + await browserLocalStorage.setAccessTokenCredential( - testAccessTokenWithoutAuthScheme, + testAccessToken, TEST_CONFIG.CORRELATION_ID ); + expect( + browserLocalStorage.getAccessTokenCredential( + CacheHelpers.generateCredentialKey(testAccessToken), + TEST_CONFIG.CORRELATION_ID + ) + ).toEqual(testAccessToken); + await browserSessionStorage.setAccessTokenCredential( - testAccessTokenWithoutAuthScheme, + testAccessToken, TEST_CONFIG.CORRELATION_ID ); + expect( + browserSessionStorage.getAccessTokenCredential( + CacheHelpers.generateCredentialKey(testAccessToken), + TEST_CONFIG.CORRELATION_ID + ) + ).toEqual(testAccessToken); }); it("getAccessTokenCredential returns Bearer access token when authentication scheme is set to Bearer and both a Bearer and pop token are in the cache", async () => { @@ -587,47 +1257,50 @@ describe("BrowserCacheManager tests", () => { testAccessTokenWithoutAuthScheme, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccessTokenCredential( - testAccessTokenWithoutAuthScheme, - TEST_CONFIG.CORRELATION_ID - ); - - // Cache pop token await browserLocalStorage.setAccessTokenCredential( testAccessTokenWithAuthScheme, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccessTokenCredential( - testAccessTokenWithAuthScheme, - TEST_CONFIG.CORRELATION_ID - ); - expect( - browserSessionStorage.getAccessTokenCredential( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithoutAuthScheme - ) + ), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testAccessTokenWithoutAuthScheme); expect( - browserSessionStorage.getAccessTokenCredential( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithoutAuthScheme - ) + ), + TEST_CONFIG.CORRELATION_ID )?.credentialType ).toBe(CredentialType.ACCESS_TOKEN); + + await browserSessionStorage.setAccessTokenCredential( + testAccessTokenWithoutAuthScheme, + TEST_CONFIG.CORRELATION_ID + ); + await browserSessionStorage.setAccessTokenCredential( + testAccessTokenWithAuthScheme, + TEST_CONFIG.CORRELATION_ID + ); + expect( - browserLocalStorage.getAccessTokenCredential( + browserSessionStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithoutAuthScheme - ) + ), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testAccessTokenWithoutAuthScheme); expect( - browserLocalStorage.getAccessTokenCredential( + browserSessionStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithoutAuthScheme - ) + ), + TEST_CONFIG.CORRELATION_ID )?.credentialType ).toBe(CredentialType.ACCESS_TOKEN); }); @@ -668,51 +1341,116 @@ describe("BrowserCacheManager tests", () => { testAccessTokenWithoutAuthScheme, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccessTokenCredential( - testAccessTokenWithoutAuthScheme, - TEST_CONFIG.CORRELATION_ID - ); - - // Cache pop token await browserLocalStorage.setAccessTokenCredential( testAccessTokenWithAuthScheme, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccessTokenCredential( - testAccessTokenWithAuthScheme, - TEST_CONFIG.CORRELATION_ID - ); - expect( - browserSessionStorage.getAccessTokenCredential( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithAuthScheme - ) + ), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testAccessTokenWithAuthScheme); expect( - browserSessionStorage.getAccessTokenCredential( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithAuthScheme - ) + ), + TEST_CONFIG.CORRELATION_ID )?.credentialType ).toBe(CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME); + + await browserSessionStorage.setAccessTokenCredential( + testAccessTokenWithoutAuthScheme, + TEST_CONFIG.CORRELATION_ID + ); + await browserSessionStorage.setAccessTokenCredential( + testAccessTokenWithAuthScheme, + TEST_CONFIG.CORRELATION_ID + ); + expect( - browserLocalStorage.getAccessTokenCredential( + browserSessionStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithAuthScheme - ) + ), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testAccessTokenWithAuthScheme); expect( - browserLocalStorage.getAccessTokenCredential( + browserSessionStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithAuthScheme - ) + ), + TEST_CONFIG.CORRELATION_ID )?.credentialType ).toBe(CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME); }); + it("setAccessTokenCredential moves cache key to the end of the token keys array if it already exists", async () => { + const accessToken1 = CacheHelpers.createAccessTokenEntity( + "homeAccountId", + "environment", + TEST_TOKENS.ACCESS_TOKEN, + "client-id", + "tenantId", + "openid", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER, + "oboAssertion" + ); + const atKey1 = + CacheHelpers.generateCredentialKey(accessToken1); + + // Add two tokens, so we can check the order + const accessToken2 = CacheHelpers.createAccessTokenEntity( + "homeAccountId2", + "environment2", + TEST_TOKENS.ACCESS_TOKEN, + "client-id", + "tenantId2", + "openid2", + 1000, + 1000, + browserCrypto.base64Decode, + 500, + AuthenticationScheme.BEARER, + "oboAssertion" + ); + const atKey2 = + CacheHelpers.generateCredentialKey(accessToken2); + + await browserLocalStorage.setAccessTokenCredential( + accessToken1, + TEST_CONFIG.CORRELATION_ID + ); + await browserLocalStorage.setAccessTokenCredential( + accessToken2, + TEST_CONFIG.CORRELATION_ID + ); + + // At this point, order should be [accessTokenKey, anotherAccessTokenKey] + expect( + browserLocalStorage.getTokenKeys().accessToken + ).toEqual([atKey1, atKey2]); + + // Set the first token again, it should move to the end + await browserLocalStorage.setAccessTokenCredential( + accessToken1, + TEST_CONFIG.CORRELATION_ID + ); + + // Now the order should be [anotherAccessTokenKey, accessTokenKey] + expect( + browserLocalStorage.getTokenKeys().accessToken + ).toEqual([atKey2, atKey1]); + }); + it("clearTokensWithClaimsInCache clears all access tokens with claims in tokenKeys", async () => { const testAT1 = CacheHelpers.createAccessTokenEntity( "homeAccountId1", @@ -795,35 +1533,18 @@ describe("BrowserCacheManager tests", () => { testAT1, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccessTokenCredential( - testAT1, - TEST_CONFIG.CORRELATION_ID - ); await browserLocalStorage.setAccessTokenCredential( testAT2, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccessTokenCredential( - testAT2, - TEST_CONFIG.CORRELATION_ID - ); await browserLocalStorage.setAccessTokenCredential( testAT3, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccessTokenCredential( - testAT3, - TEST_CONFIG.CORRELATION_ID - ); await browserLocalStorage.setAccessTokenCredential( testAT4, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccessTokenCredential( - testAT4, - TEST_CONFIG.CORRELATION_ID - ); - expect(browserLocalStorage.getTokenKeys()).toStrictEqual({ idToken: [], accessToken: [ @@ -834,131 +1555,157 @@ describe("BrowserCacheManager tests", () => { ], refreshToken: [], }); - - expect(browserSessionStorage.getTokenKeys()).toStrictEqual({ - idToken: [], - accessToken: [ + expect( + browserLocalStorage.getTokenKeys().accessToken.length + ).toBe(4); + expect( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey(testAT1), + RANDOM_TEST_GUID + ) + ).toEqual(testAT1); + expect( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey(testAT2), + RANDOM_TEST_GUID + ) + ).toEqual(testAT2); + expect( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey(testAT3), + RANDOM_TEST_GUID + ) + ).toEqual(testAT3); + expect( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey(testAT4), - ], - refreshToken: [], - }); + RANDOM_TEST_GUID + ) + ).toEqual(testAT4); - expect( - browserSessionStorage.getTokenKeys().accessToken.length - ).toBe(4); - expect( - browserLocalStorage.getTokenKeys().accessToken.length - ).toBe(4); + browserLocalStorage.clearTokensAndKeysWithClaims( + "test-correlation-id" + ); expect( - browserSessionStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT1) + browserLocalStorage.getAccessTokenCredential( + CacheHelpers.generateCredentialKey(testAT1), + RANDOM_TEST_GUID ) ).toEqual(testAT1); expect( browserLocalStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT1) + CacheHelpers.generateCredentialKey(testAT2), + RANDOM_TEST_GUID ) - ).toEqual(testAT1); - + ).toBeNull(); expect( - browserSessionStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT2) + browserLocalStorage.getAccessTokenCredential( + CacheHelpers.generateCredentialKey(testAT3), + RANDOM_TEST_GUID ) - ).toEqual(testAT2); + ).toEqual(testAT3); expect( browserLocalStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT2) + CacheHelpers.generateCredentialKey(testAT2), + RANDOM_TEST_GUID ) - ).toEqual(testAT2); + ).toBeNull(); + expect(browserLocalStorage.getTokenKeys()).toStrictEqual({ + idToken: [], + accessToken: [ + CacheHelpers.generateCredentialKey(testAT1), + CacheHelpers.generateCredentialKey(testAT3), + ], + refreshToken: [], + }); + expect( + browserLocalStorage.getTokenKeys().accessToken.length + ).toBe(2); + + await browserSessionStorage.setAccessTokenCredential( + testAT1, + TEST_CONFIG.CORRELATION_ID + ); + await browserSessionStorage.setAccessTokenCredential( + testAT2, + TEST_CONFIG.CORRELATION_ID + ); + await browserSessionStorage.setAccessTokenCredential( + testAT3, + TEST_CONFIG.CORRELATION_ID + ); + await browserSessionStorage.setAccessTokenCredential( + testAT4, + TEST_CONFIG.CORRELATION_ID + ); + + expect(browserSessionStorage.getTokenKeys()).toStrictEqual({ + idToken: [], + accessToken: [ + CacheHelpers.generateCredentialKey(testAT1), + CacheHelpers.generateCredentialKey(testAT2), + CacheHelpers.generateCredentialKey(testAT3), + CacheHelpers.generateCredentialKey(testAT4), + ], + refreshToken: [], + }); + expect( + browserSessionStorage.getTokenKeys().accessToken.length + ).toBe(4); expect( browserSessionStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT3) + CacheHelpers.generateCredentialKey(testAT1), + RANDOM_TEST_GUID ) - ).toEqual(testAT3); + ).toEqual(testAT1); expect( - browserLocalStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT3) + browserSessionStorage.getAccessTokenCredential( + CacheHelpers.generateCredentialKey(testAT2), + RANDOM_TEST_GUID ) - ).toEqual(testAT3); - + ).toEqual(testAT2); expect( browserSessionStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT4) + CacheHelpers.generateCredentialKey(testAT3), + RANDOM_TEST_GUID ) - ).toEqual(testAT4); + ).toEqual(testAT3); expect( - browserLocalStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT4) + browserSessionStorage.getAccessTokenCredential( + CacheHelpers.generateCredentialKey(testAT4), + RANDOM_TEST_GUID ) ).toEqual(testAT4); - browserSessionStorage.clearTokensAndKeysWithClaims( - getDefaultPerformanceClient(), - "test-correlation-id" - ); - browserLocalStorage.clearTokensAndKeysWithClaims( - getDefaultPerformanceClient(), "test-correlation-id" ); expect( browserSessionStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT1) - ) - ).toEqual(testAT1); - expect( - browserLocalStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT1) + CacheHelpers.generateCredentialKey(testAT1), + RANDOM_TEST_GUID ) ).toEqual(testAT1); - expect( browserSessionStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT2) - ) - ).toBeNull(); - expect( - browserLocalStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT2) + CacheHelpers.generateCredentialKey(testAT2), + RANDOM_TEST_GUID ) ).toBeNull(); - expect( browserSessionStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT3) - ) - ).toEqual(testAT3); - expect( - browserLocalStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT3) + CacheHelpers.generateCredentialKey(testAT3), + RANDOM_TEST_GUID ) ).toEqual(testAT3); - expect( browserSessionStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT2) - ) - ).toBeNull(); - expect( - browserLocalStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAT2) + CacheHelpers.generateCredentialKey(testAT2), + RANDOM_TEST_GUID ) ).toBeNull(); - - expect(browserLocalStorage.getTokenKeys()).toStrictEqual({ - idToken: [], - accessToken: [ - CacheHelpers.generateCredentialKey(testAT1), - CacheHelpers.generateCredentialKey(testAT3), - ], - refreshToken: [], - }); - expect(browserSessionStorage.getTokenKeys()).toStrictEqual({ idToken: [], accessToken: [ @@ -971,9 +1718,6 @@ describe("BrowserCacheManager tests", () => { expect( browserSessionStorage.getTokenKeys().accessToken.length ).toBe(2); - expect( - browserLocalStorage.getTokenKeys().accessToken.length - ).toBe(2); }); }); @@ -981,10 +1725,16 @@ describe("BrowserCacheManager tests", () => { it("getRefreshTokenCredential returns null if key not in cache", () => { const key = "not-in-cache"; expect( - browserSessionStorage.getRefreshTokenCredential(key) + browserSessionStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getRefreshTokenCredential(key) + browserLocalStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -994,10 +1744,16 @@ describe("BrowserCacheManager tests", () => { window.sessionStorage.setItem(key, "this is not json"); expect( - browserSessionStorage.getRefreshTokenCredential(key) + browserSessionStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getRefreshTokenCredential(key) + browserLocalStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -1017,10 +1773,16 @@ describe("BrowserCacheManager tests", () => { ); expect( - browserSessionStorage.getRefreshTokenCredential(key) + browserSessionStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getRefreshTokenCredential(key) + browserLocalStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -1039,19 +1801,25 @@ describe("BrowserCacheManager tests", () => { testRefreshToken, TEST_CONFIG.CORRELATION_ID ); + expect( + browserLocalStorage.getRefreshTokenCredential( + CacheHelpers.generateCredentialKey( + testRefreshToken + ), + TEST_CONFIG.CORRELATION_ID + ) + ).toEqual(testRefreshToken); + await browserSessionStorage.setRefreshTokenCredential( testRefreshToken, TEST_CONFIG.CORRELATION_ID ); - expect( browserSessionStorage.getRefreshTokenCredential( - CacheHelpers.generateCredentialKey(testRefreshToken) - ) - ).toEqual(testRefreshToken); - expect( - browserLocalStorage.getRefreshTokenCredential( - CacheHelpers.generateCredentialKey(testRefreshToken) + CacheHelpers.generateCredentialKey( + testRefreshToken + ), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testRefreshToken); }); @@ -1105,8 +1873,14 @@ describe("BrowserCacheManager tests", () => { familyId: "1", }; - browserLocalStorage.setAppMetadata(testAppMetadata); - browserSessionStorage.setAppMetadata(testAppMetadata); + browserLocalStorage.setAppMetadata( + testAppMetadata, + RANDOM_TEST_GUID + ); + browserSessionStorage.setAppMetadata( + testAppMetadata, + RANDOM_TEST_GUID + ); expect( browserSessionStorage.getAppMetadata( @@ -1176,8 +1950,16 @@ describe("BrowserCacheManager tests", () => { cacheHits: 2, }; - browserLocalStorage.setServerTelemetry(testKey, testVal); - browserSessionStorage.setServerTelemetry(testKey, testVal); + browserLocalStorage.setServerTelemetry( + testKey, + testVal, + RANDOM_TEST_GUID + ); + browserSessionStorage.setServerTelemetry( + testKey, + testVal, + RANDOM_TEST_GUID + ); expect( browserSessionStorage.getServerTelemetry(testKey) @@ -1285,8 +2067,8 @@ describe("BrowserCacheManager tests", () => { browserSessionStorage.getAuthorityMetadataKeys() ).toEqual(expect.arrayContaining([key])); - await browserSessionStorage.clear(); - await browserLocalStorage.clear(); + browserSessionStorage.clear(RANDOM_TEST_GUID); + browserLocalStorage.clear(RANDOM_TEST_GUID); expect( browserSessionStorage.getAuthorityMetadata(key) ).toBeNull(); @@ -1355,8 +2137,16 @@ describe("BrowserCacheManager tests", () => { throttleTime: 60, }; - browserLocalStorage.setThrottlingCache(testKey, testVal); - browserSessionStorage.setThrottlingCache(testKey, testVal); + browserLocalStorage.setThrottlingCache( + testKey, + testVal, + RANDOM_TEST_GUID + ); + browserSessionStorage.setThrottlingCache( + testKey, + testVal, + RANDOM_TEST_GUID + ); expect( browserSessionStorage.getThrottlingCache(testKey) @@ -1371,7 +2161,7 @@ describe("BrowserCacheManager tests", () => { describe("saveCacheRecord", () => { it("saveCacheRecord re-throws and captures telemetry", (done) => { const cacheError = new CacheError( - CacheErrorCodes.cacheQuotaExceededErrorCode + CacheErrorCodes.cacheQuotaExceeded ); const testAppConfig = { auth: { @@ -1425,7 +2215,7 @@ describe("BrowserCacheManager tests", () => { ); expect(event.success).toBeFalsy(); expect(event.errorCode).toEqual( - CacheErrorCodes.cacheQuotaExceededErrorCode + CacheErrorCodes.cacheQuotaExceeded ); expect(event.cacheIdCount).toEqual(0); expect(event.cacheRtCount).toEqual(0); @@ -1465,6 +2255,90 @@ describe("BrowserCacheManager tests", () => { ); }); }); + + describe("interactionInProgress", () => { + it("handles new format", () => { + const perfClient = new BrowserPerformanceClient({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + }); + const cacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + cacheConfig, + browserCrypto, + logger, + perfClient, + new EventHandler() + ); + + cacheManager.setInteractionInProgress(true); + expect( + cacheManager.getInteractionInProgress()?.clientId + ).toEqual(TEST_CONFIG.MSAL_CLIENT_ID); + expect( + cacheManager.getInteractionInProgress()?.type + ).toEqual(INTERACTION_TYPE.SIGNIN); + }); + + it("handles old format", () => { + const perfClient = new BrowserPerformanceClient({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + }); + const cacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + cacheConfig, + browserCrypto, + logger, + perfClient, + new EventHandler() + ); + + cacheManager.setTemporaryCache( + `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, + TEST_CONFIG.MSAL_CLIENT_ID + ); + expect(cacheManager.getInteractionInProgress()).toBeNull(); + }); + + it("handles old format and removes temporary artifacts", () => { + const perfClient = new BrowserPerformanceClient({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + }, + }); + const cacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + cacheConfig, + browserCrypto, + logger, + perfClient, + new EventHandler() + ); + + cacheManager.setTemporaryCache( + `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, + TEST_CONFIG.MSAL_CLIENT_ID + ); + // @ts-ignore + const requestParamKey = cacheManager.generateCacheKey( + TemporaryCacheKeys.REQUEST_PARAMS + ); + const requestParamPayload = JSON.stringify({ + correlationId: "test-correlation-id", + }); + cacheManager.setTemporaryCache( + requestParamKey, + requestParamPayload + ); + expect(cacheManager.getInteractionInProgress()).toBeNull(); + expect( + cacheManager.getTemporaryCache(requestParamKey) + ).toBeNull(); + }); + }); }); }); @@ -1521,8 +2395,8 @@ describe("BrowserCacheManager tests", () => { }); afterEach(async () => { - await browserSessionStorage.clear(); - await browserLocalStorage.clear(); + browserSessionStorage.clear(RANDOM_TEST_GUID); + browserLocalStorage.clear(RANDOM_TEST_GUID); }); it("setTemporaryCache", () => { @@ -1567,11 +2441,11 @@ describe("BrowserCacheManager tests", () => { ).toBeNull(); }); - it("clear()", async () => { + it("clear()", () => { browserSessionStorage.setTemporaryCache("cacheKey", cacheVal, true); browserLocalStorage.setTemporaryCache("cacheKey", cacheVal, true); - await browserSessionStorage.clear(); - await browserLocalStorage.clear(); + browserSessionStorage.clear(RANDOM_TEST_GUID); + browserLocalStorage.clear(RANDOM_TEST_GUID); expect(browserSessionStorage.getKeys()).toHaveLength(0); expect(browserLocalStorage.getKeys()).toHaveLength(0); }); @@ -1580,8 +2454,12 @@ describe("BrowserCacheManager tests", () => { describe("Account", () => { it("getAccount returns null if key not in cache", () => { const key = "not-in-cache"; - expect(browserSessionStorage.getAccount(key)).toBeNull(); - expect(browserLocalStorage.getAccount(key)).toBeNull(); + expect( + browserSessionStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); + expect( + browserLocalStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); }); it("getAccount returns null if value is not JSON", () => { @@ -1589,8 +2467,12 @@ describe("BrowserCacheManager tests", () => { window.localStorage.setItem(key, "this is not json"); window.sessionStorage.setItem(key, "this is not json"); - expect(browserSessionStorage.getAccount(key)).toBeNull(); - expect(browserLocalStorage.getAccount(key)).toBeNull(); + expect( + browserSessionStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); + expect( + browserLocalStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); }); it("getAccount returns null if value is not account entity", () => { @@ -1608,8 +2490,12 @@ describe("BrowserCacheManager tests", () => { JSON.stringify(partialAccount) ); - expect(browserSessionStorage.getAccount(key)).toBeNull(); - expect(browserLocalStorage.getAccount(key)).toBeNull(); + expect( + browserSessionStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); + expect( + browserLocalStorage.getAccount(key, RANDOM_TEST_GUID) + ).toBeNull(); }); it("getAccount returns AccountEntity", async () => { @@ -1632,6 +2518,19 @@ describe("BrowserCacheManager tests", () => { testAccount, TEST_CONFIG.CORRELATION_ID ); + expect( + browserLocalStorage.getAccount( + testAccount.generateAccountKey(), + TEST_CONFIG.CORRELATION_ID + ) + ).toEqual(testAccount); + expect( + browserLocalStorage.getAccount( + testAccount.generateAccountKey(), + TEST_CONFIG.CORRELATION_ID + ) + ).toBeInstanceOf(AccountEntity); + await browserSessionStorage.setAccount( testAccount, TEST_CONFIG.CORRELATION_ID @@ -1639,22 +2538,14 @@ describe("BrowserCacheManager tests", () => { expect( browserSessionStorage.getAccount( - testAccount.generateAccountKey() + testAccount.generateAccountKey(), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testAccount); expect( browserSessionStorage.getAccount( - testAccount.generateAccountKey() - ) - ).toBeInstanceOf(AccountEntity); - expect( - browserLocalStorage.getAccount( - testAccount.generateAccountKey() - ) - ).toEqual(testAccount); - expect( - browserLocalStorage.getAccount( - testAccount.generateAccountKey() + testAccount.generateAccountKey(), + TEST_CONFIG.CORRELATION_ID ) ).toBeInstanceOf(AccountEntity); }); @@ -1664,10 +2555,16 @@ describe("BrowserCacheManager tests", () => { it("getIdTokenCredential returns null if key not in cache", () => { const key = "not-in-cache"; expect( - browserSessionStorage.getIdTokenCredential(key) + browserSessionStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getIdTokenCredential(key) + browserLocalStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -1677,10 +2574,16 @@ describe("BrowserCacheManager tests", () => { window.sessionStorage.setItem(key, "this is not json"); expect( - browserSessionStorage.getIdTokenCredential(key) + browserSessionStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getIdTokenCredential(key) + browserLocalStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -1700,10 +2603,16 @@ describe("BrowserCacheManager tests", () => { ); expect( - browserSessionStorage.getIdTokenCredential(key) + browserSessionStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getIdTokenCredential(key) + browserLocalStorage.getIdTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -1720,19 +2629,21 @@ describe("BrowserCacheManager tests", () => { testIdToken, TEST_CONFIG.CORRELATION_ID ); + expect( + browserLocalStorage.getIdTokenCredential( + CacheHelpers.generateCredentialKey(testIdToken), + TEST_CONFIG.CORRELATION_ID + ) + ).toEqual(testIdToken); + await browserSessionStorage.setIdTokenCredential( testIdToken, TEST_CONFIG.CORRELATION_ID ); - expect( browserSessionStorage.getIdTokenCredential( - CacheHelpers.generateCredentialKey(testIdToken) - ) - ).toEqual(testIdToken); - expect( - browserLocalStorage.getIdTokenCredential( - CacheHelpers.generateCredentialKey(testIdToken) + CacheHelpers.generateCredentialKey(testIdToken), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testIdToken); }); @@ -1742,10 +2653,16 @@ describe("BrowserCacheManager tests", () => { it("getAccessTokenCredential returns null if key not in cache", () => { const key = "not-in-cache"; expect( - browserSessionStorage.getAccessTokenCredential(key) + browserSessionStorage.getAccessTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getAccessTokenCredential(key) + browserLocalStorage.getAccessTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -1755,10 +2672,16 @@ describe("BrowserCacheManager tests", () => { window.sessionStorage.setItem(key, "this is not json"); expect( - browserSessionStorage.getAccessTokenCredential(key) + browserSessionStorage.getAccessTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getAccessTokenCredential(key) + browserLocalStorage.getAccessTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -1778,10 +2701,16 @@ describe("BrowserCacheManager tests", () => { ); expect( - browserSessionStorage.getAccessTokenCredential(key) + browserSessionStorage.getAccessTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getAccessTokenCredential(key) + browserLocalStorage.getAccessTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -1806,19 +2735,21 @@ describe("BrowserCacheManager tests", () => { testAccessToken, TEST_CONFIG.CORRELATION_ID ); + expect( + browserLocalStorage.getAccessTokenCredential( + CacheHelpers.generateCredentialKey(testAccessToken), + TEST_CONFIG.CORRELATION_ID + ) + ).toEqual(testAccessToken); + await browserSessionStorage.setAccessTokenCredential( testAccessToken, TEST_CONFIG.CORRELATION_ID ); - expect( browserSessionStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAccessToken) - ) - ).toEqual(testAccessToken); - expect( - browserLocalStorage.getAccessTokenCredential( - CacheHelpers.generateCredentialKey(testAccessToken) + CacheHelpers.generateCredentialKey(testAccessToken), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testAccessToken); }); @@ -1859,47 +2790,50 @@ describe("BrowserCacheManager tests", () => { testAccessTokenWithoutAuthScheme, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccessTokenCredential( - testAccessTokenWithoutAuthScheme, - TEST_CONFIG.CORRELATION_ID - ); - - // Cache pop token await browserLocalStorage.setAccessTokenCredential( testAccessTokenWithAuthScheme, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccessTokenCredential( - testAccessTokenWithAuthScheme, - TEST_CONFIG.CORRELATION_ID - ); - expect( - browserSessionStorage.getAccessTokenCredential( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithoutAuthScheme - ) + ), + TEST_CONFIG.CORRELATION_ID ) ).toEqual(testAccessTokenWithoutAuthScheme); expect( - browserSessionStorage.getAccessTokenCredential( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithoutAuthScheme - ) + ), + TEST_CONFIG.CORRELATION_ID )?.credentialType ).toBe(CredentialType.ACCESS_TOKEN); + + await browserSessionStorage.setAccessTokenCredential( + testAccessTokenWithoutAuthScheme, + TEST_CONFIG.CORRELATION_ID + ); + await browserSessionStorage.setAccessTokenCredential( + testAccessTokenWithAuthScheme, + TEST_CONFIG.CORRELATION_ID + ); + expect( - browserLocalStorage.getAccessTokenCredential( + browserSessionStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithoutAuthScheme - ) + ), + RANDOM_TEST_GUID ) ).toEqual(testAccessTokenWithoutAuthScheme); expect( - browserLocalStorage.getAccessTokenCredential( + browserSessionStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithoutAuthScheme - ) + ), + RANDOM_TEST_GUID )?.credentialType ).toBe(CredentialType.ACCESS_TOKEN); }); @@ -1936,51 +2870,53 @@ describe("BrowserCacheManager tests", () => { "oboAssertion" ); // Cache bearer token - await browserLocalStorage.setAccessTokenCredential( - testAccessTokenWithoutAuthScheme, - TEST_CONFIG.CORRELATION_ID - ); - await browserSessionStorage.setAccessTokenCredential( + await browserLocalStorage.setAccessTokenCredential( testAccessTokenWithoutAuthScheme, TEST_CONFIG.CORRELATION_ID ); - - // Cache pop token await browserLocalStorage.setAccessTokenCredential( testAccessTokenWithAuthScheme, TEST_CONFIG.CORRELATION_ID ); - await browserSessionStorage.setAccessTokenCredential( - testAccessTokenWithAuthScheme, - TEST_CONFIG.CORRELATION_ID - ); - expect( - browserSessionStorage.getAccessTokenCredential( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithAuthScheme - ) + ), + RANDOM_TEST_GUID ) ).toEqual(testAccessTokenWithAuthScheme); expect( - browserSessionStorage.getAccessTokenCredential( + browserLocalStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithAuthScheme - ) + ), + RANDOM_TEST_GUID )?.credentialType ).toBe(CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME); + + await browserSessionStorage.setAccessTokenCredential( + testAccessTokenWithoutAuthScheme, + TEST_CONFIG.CORRELATION_ID + ); + await browserSessionStorage.setAccessTokenCredential( + testAccessTokenWithAuthScheme, + TEST_CONFIG.CORRELATION_ID + ); expect( - browserLocalStorage.getAccessTokenCredential( + browserSessionStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithAuthScheme - ) + ), + RANDOM_TEST_GUID ) ).toEqual(testAccessTokenWithAuthScheme); expect( - browserLocalStorage.getAccessTokenCredential( + browserSessionStorage.getAccessTokenCredential( CacheHelpers.generateCredentialKey( testAccessTokenWithAuthScheme - ) + ), + RANDOM_TEST_GUID )?.credentialType ).toBe(CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME); }); @@ -1990,10 +2926,16 @@ describe("BrowserCacheManager tests", () => { it("getRefreshTokenCredential returns null if key not in cache", () => { const key = "not-in-cache"; expect( - browserSessionStorage.getRefreshTokenCredential(key) + browserSessionStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getRefreshTokenCredential(key) + browserLocalStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -2003,10 +2945,16 @@ describe("BrowserCacheManager tests", () => { window.sessionStorage.setItem(key, "this is not json"); expect( - browserSessionStorage.getRefreshTokenCredential(key) + browserSessionStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getRefreshTokenCredential(key) + browserLocalStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -2026,10 +2974,16 @@ describe("BrowserCacheManager tests", () => { ); expect( - browserSessionStorage.getRefreshTokenCredential(key) + browserSessionStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); expect( - browserLocalStorage.getRefreshTokenCredential(key) + browserLocalStorage.getRefreshTokenCredential( + key, + RANDOM_TEST_GUID + ) ).toBeNull(); }); @@ -2048,19 +3002,25 @@ describe("BrowserCacheManager tests", () => { testRefreshToken, TEST_CONFIG.CORRELATION_ID ); + expect( + browserLocalStorage.getRefreshTokenCredential( + CacheHelpers.generateCredentialKey( + testRefreshToken + ), + RANDOM_TEST_GUID + ) + ).toEqual(testRefreshToken); + await browserSessionStorage.setRefreshTokenCredential( testRefreshToken, TEST_CONFIG.CORRELATION_ID ); - expect( browserSessionStorage.getRefreshTokenCredential( - CacheHelpers.generateCredentialKey(testRefreshToken) - ) - ).toEqual(testRefreshToken); - expect( - browserLocalStorage.getRefreshTokenCredential( - CacheHelpers.generateCredentialKey(testRefreshToken) + CacheHelpers.generateCredentialKey( + testRefreshToken + ), + RANDOM_TEST_GUID ) ).toEqual(testRefreshToken); }); @@ -2114,8 +3074,14 @@ describe("BrowserCacheManager tests", () => { familyId: "1", }; - browserLocalStorage.setAppMetadata(testAppMetadata); - browserSessionStorage.setAppMetadata(testAppMetadata); + browserLocalStorage.setAppMetadata( + testAppMetadata, + RANDOM_TEST_GUID + ); + browserSessionStorage.setAppMetadata( + testAppMetadata, + RANDOM_TEST_GUID + ); expect( browserSessionStorage.getAppMetadata( @@ -2185,8 +3151,16 @@ describe("BrowserCacheManager tests", () => { cacheHits: 2, }; - browserLocalStorage.setServerTelemetry(testKey, testVal); - browserSessionStorage.setServerTelemetry(testKey, testVal); + browserLocalStorage.setServerTelemetry( + testKey, + testVal, + RANDOM_TEST_GUID + ); + browserSessionStorage.setServerTelemetry( + testKey, + testVal, + RANDOM_TEST_GUID + ); expect( browserSessionStorage.getServerTelemetry(testKey) @@ -2278,7 +3252,7 @@ describe("BrowserCacheManager tests", () => { ).toEqual(expect.arrayContaining([key])); }); - it("clear() removes AuthorityMetadataEntity from in-memory storage", async () => { + it("clear() removes AuthorityMetadataEntity from in-memory storage", () => { browserSessionStorage.setAuthorityMetadata(key, testObj); browserLocalStorage.setAuthorityMetadata(key, testObj); @@ -2295,8 +3269,8 @@ describe("BrowserCacheManager tests", () => { browserSessionStorage.getAuthorityMetadataKeys() ).toEqual(expect.arrayContaining([key])); - await browserSessionStorage.clear(); - await browserLocalStorage.clear(); + browserSessionStorage.clear(RANDOM_TEST_GUID); + browserLocalStorage.clear(RANDOM_TEST_GUID); expect( browserSessionStorage.getAuthorityMetadata(key) ).toBeNull(); @@ -2363,8 +3337,16 @@ describe("BrowserCacheManager tests", () => { const testKey = "throttling"; const testVal = { throttleTime: 60 }; - browserLocalStorage.setThrottlingCache(testKey, testVal); - browserSessionStorage.setThrottlingCache(testKey, testVal); + browserLocalStorage.setThrottlingCache( + testKey, + testVal, + RANDOM_TEST_GUID + ); + browserSessionStorage.setThrottlingCache( + testKey, + testVal, + RANDOM_TEST_GUID + ); expect( browserSessionStorage.getThrottlingCache(testKey) @@ -2427,9 +3409,9 @@ describe("BrowserCacheManager tests", () => { msalCacheKey = browserSessionStorage.generateCacheKey("cacheKey"); }); - afterEach(async () => { - await browserSessionStorage.clear(); - await browserLocalStorage.clear(); + afterEach(() => { + browserSessionStorage.clear(RANDOM_TEST_GUID); + browserLocalStorage.clear(RANDOM_TEST_GUID); }); it("setTempCache()", () => { @@ -2538,11 +3520,11 @@ describe("BrowserCacheManager tests", () => { expect(clearCookieSpy).toHaveBeenCalledTimes(3); }); - it("clear()", async () => { + it("clear()", () => { // sessionStorage browserSessionStorage.setTemporaryCache(msalCacheKey, cacheVal); expect(document.cookie).toContain(`${msalCacheKey}=${cacheVal}`); - await browserSessionStorage.clear(); + browserSessionStorage.clear(RANDOM_TEST_GUID); expect(browserSessionStorage.getKeys()).toHaveLength(0); expect(document.cookie).not.toContain( `${msalCacheKey}=${cacheVal}` @@ -2550,7 +3532,7 @@ describe("BrowserCacheManager tests", () => { // localStorage browserLocalStorage.setTemporaryCache(msalCacheKey, cacheVal); expect(document.cookie).toContain(`${msalCacheKey}=${cacheVal}`); - await browserLocalStorage.clear(); + browserLocalStorage.clear(RANDOM_TEST_GUID); expect(browserLocalStorage.getKeys()).toHaveLength(0); expect(document.cookie).not.toContain( `${msalCacheKey}=${cacheVal}` @@ -2558,7 +3540,7 @@ describe("BrowserCacheManager tests", () => { // browser memory browserMemoryStorage.setTemporaryCache(msalCacheKey, cacheVal); expect(document.cookie).toContain(`${msalCacheKey}=${cacheVal}`); - await browserMemoryStorage.clear(); + browserMemoryStorage.clear(RANDOM_TEST_GUID); expect(browserMemoryStorage.getKeys()).toHaveLength(0); expect(document.cookie).not.toContain( `${msalCacheKey}=${cacheVal}` @@ -2660,12 +3642,12 @@ describe("BrowserCacheManager tests", () => { expect(clearCookieSpy).toHaveBeenCalledTimes(3); }); - it("clear() with item that contains ==", async () => { + it("clear() with item that contains ==", () => { msalCacheKey = `${Constants.CACHE_PREFIX}.${TEST_STATE_VALUES.ENCODED_LIB_STATE}`; // sessionStorage browserSessionStorage.setTemporaryCache(msalCacheKey, cacheVal); expect(document.cookie).toContain(`${msalCacheKey}=${cacheVal}`); - await browserSessionStorage.clear(); + browserSessionStorage.clear(RANDOM_TEST_GUID); expect(browserSessionStorage.getKeys()).toHaveLength(0); expect(document.cookie).not.toContain( `${msalCacheKey}=${cacheVal}` @@ -2673,7 +3655,7 @@ describe("BrowserCacheManager tests", () => { // localStorage browserLocalStorage.setTemporaryCache(msalCacheKey, cacheVal); expect(document.cookie).toContain(`${msalCacheKey}=${cacheVal}`); - await browserLocalStorage.clear(); + browserLocalStorage.clear(RANDOM_TEST_GUID); expect(browserLocalStorage.getKeys()).toHaveLength(0); expect(document.cookie).not.toContain( `${msalCacheKey}=${cacheVal}` @@ -2681,7 +3663,7 @@ describe("BrowserCacheManager tests", () => { // browser memory browserMemoryStorage.setTemporaryCache(msalCacheKey, cacheVal); expect(document.cookie).toContain(`${msalCacheKey}=${cacheVal}`); - await browserMemoryStorage.clear(); + browserMemoryStorage.clear(RANDOM_TEST_GUID); expect(browserMemoryStorage.getKeys()).toHaveLength(0); expect(document.cookie).not.toContain( `${msalCacheKey}=${cacheVal}` @@ -2690,75 +3672,7 @@ describe("BrowserCacheManager tests", () => { }); describe("Helpers", () => { - it("generateAuthorityKey() creates a valid cache key for authority strings", () => { - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - logger, - new StubPerformanceClient(), - new EventHandler() - ); - const authorityKey = browserStorage.generateAuthorityKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - expect(authorityKey).toBe( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.AUTHORITY}.${RANDOM_TEST_GUID}` - ); - }); - - it("generateNonceKey() create a valid cache key for nonce strings", () => { - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - logger, - new StubPerformanceClient(), - new EventHandler() - ); - const nonceKey = browserStorage.generateNonceKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - expect(nonceKey).toBe( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.NONCE_IDTOKEN}.${RANDOM_TEST_GUID}` - ); - }); - - it("updateCacheEntries() correctly updates the authority, state and nonce in the cache", () => { - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - logger, - new StubPerformanceClient(), - new EventHandler() - ); - const testNonce = "testNonce"; - const stateString = TEST_STATE_VALUES.TEST_STATE_REDIRECT; - ProtocolUtils.parseRequestState(browserCrypto, stateString) - .libraryState.id; - browserStorage.updateCacheEntries( - stateString, - testNonce, - `${Constants.DEFAULT_AUTHORITY}/`, - "", - null - ); - - const stateKey = browserStorage.generateStateKey(stateString); - const nonceKey = browserStorage.generateNonceKey(stateString); - const authorityKey = - browserStorage.generateAuthorityKey(stateString); - - expect(window.sessionStorage[`${stateKey}`]).toBe(stateString); - expect(window.sessionStorage[`${nonceKey}`]).toBe(testNonce); - expect(window.sessionStorage[`${authorityKey}`]).toBe( - `${Constants.DEFAULT_AUTHORITY}/` - ); - }); - it("resetTempCacheItems() resets all temporary cache items with the given state", () => { - const stateString = TEST_STATE_VALUES.TEST_STATE_REDIRECT; const browserStorage = new BrowserCacheManager( TEST_CONFIG.MSAL_CLIENT_ID, cacheConfig, @@ -2767,51 +3681,28 @@ describe("BrowserCacheManager tests", () => { new StubPerformanceClient(), new EventHandler() ); - browserStorage.updateCacheEntries( - stateString, - "nonce", - `${TEST_URIS.DEFAULT_INSTANCE}/`, - "", - null - ); + const requestParamsKey = `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_PARAMS}`; window.sessionStorage.setItem( - TemporaryCacheKeys.REQUEST_PARAMS, + requestParamsKey, "TestRequestParams" ); + const originUriKey = `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.ORIGIN_URI}`; window.sessionStorage.setItem( - TemporaryCacheKeys.ORIGIN_URI, + originUriKey, TEST_URIS.TEST_REDIR_URI ); - browserStorage.resetRequestCache(stateString); - const nonceKey = browserStorage.generateNonceKey(stateString); - const authorityKey = - browserStorage.generateAuthorityKey(stateString); - expect( - window.sessionStorage[ - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${nonceKey}` - ] - ).toBeUndefined(); - expect( - window.sessionStorage[ - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${authorityKey}` - ] - ).toBeUndefined(); - expect( - window.sessionStorage[ - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}` - ] - ).toBeUndefined(); - expect( - window.sessionStorage[ - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_PARAMS}` - ] - ).toBeUndefined(); - expect( - window.sessionStorage[ - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.ORIGIN_URI}` - ] - ).toBeUndefined(); + expect(window.sessionStorage[requestParamsKey]).toBe( + "TestRequestParams" + ); + expect(window.sessionStorage[originUriKey]).toBe( + TEST_URIS.TEST_REDIR_URI + ); + + browserStorage.resetRequestCache(); + + expect(window.sessionStorage[requestParamsKey]).toBeUndefined(); + expect(window.sessionStorage[originUriKey]).toBeUndefined(); }); it("Successfully retrieves and decodes response from cache", async () => { @@ -2823,27 +3714,26 @@ describe("BrowserCacheManager tests", () => { new StubPerformanceClient(), new EventHandler() ); - const tokenRequest: AuthorizationCodeRequest = { + const tokenRequest: CommonAuthorizationUrlRequest = { redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}`, scopes: [Constants.OPENID_SCOPE, Constants.PROFILE_SCOPE], - code: "thisIsAnAuthCode", - codeVerifier: TEST_CONFIG.TEST_VERIFIER, authority: `${Constants.DEFAULT_AUTHORITY}/`, correlationId: `${RANDOM_TEST_GUID}`, authenticationScheme: AuthenticationScheme.BEARER, + responseMode: ResponseMode.FRAGMENT, + state: TEST_CONFIG.STATE, + nonce: RANDOM_TEST_GUID, }; - browserStorage.setTemporaryCache( - TemporaryCacheKeys.REQUEST_PARAMS, - browserCrypto.base64Encode(JSON.stringify(tokenRequest)), - true + browserStorage.cacheAuthorizeRequest( + tokenRequest, + TEST_CONFIG.TEST_VERIFIER ); - const cachedRequest = - browserStorage.getCachedRequest(RANDOM_TEST_GUID); + const [cachedRequest, codeVerifier] = + browserStorage.getCachedRequest(); expect(cachedRequest).toEqual(tokenRequest); - - // expect(() => browserStorage.getCachedRequest(RANDOM_TEST_GUID, cryptoObj)).to.throw(BrowserAuthErrorMessage.tokenRequestCacheError.desc); + expect(codeVerifier).toEqual(TEST_CONFIG.TEST_VERIFIER); }); it("Throws error if request cannot be retrieved from cache", async () => { @@ -2855,11 +3745,8 @@ describe("BrowserCacheManager tests", () => { new StubPerformanceClient(), new EventHandler() ); - // browserStorage.setItem(TemporaryCacheKeys.REQUEST_PARAMS, cryptoObj.base64Encode(JSON.stringify(tokenRequest))); - expect(() => - browserStorage.getCachedRequest(RANDOM_TEST_GUID) - ).toThrowError( + expect(() => browserStorage.getCachedRequest()).toThrowError( BrowserAuthErrorMessage.noTokenRequestCacheError.desc ); }); @@ -2894,266 +3781,9 @@ describe("BrowserCacheManager tests", () => { stringifiedRequest.substring(0, stringifiedRequest.length / 2), true ); - expect(() => - browserStorage.getCachedRequest(RANDOM_TEST_GUID) - ).toThrowError( + expect(() => browserStorage.getCachedRequest()).toThrowError( BrowserAuthErrorMessage.unableToParseTokenRequestCacheError.desc ); }); - - it("Uses authority from cache if not present in cached request", async () => { - let dbStorage = {}; - jest.spyOn(DatabaseStorage.prototype, "open").mockImplementation( - async (): Promise => { - dbStorage = {}; - } - ); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - logger, - new StubPerformanceClient(), - new EventHandler() - ); - // Set up cache - const authorityKey = browserStorage.generateAuthorityKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - const alternateAuthority = `${TEST_URIS.ALTERNATE_INSTANCE}/common/`; - window.sessionStorage.setItem(authorityKey, alternateAuthority); - - const cachedRequest: AuthorizationCodeRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - code: "thisIsACode", - codeVerifier: TEST_CONFIG.TEST_VERIFIER, - correlationId: RANDOM_TEST_GUID, - scopes: [TEST_CONFIG.MSAL_CLIENT_ID], - authority: "", - authenticationScheme: AuthenticationScheme.BEARER, - }; - const stringifiedRequest = browserCrypto.base64Encode( - JSON.stringify(cachedRequest) - ); - browserStorage.setTemporaryCache( - TemporaryCacheKeys.REQUEST_PARAMS, - stringifiedRequest, - true - ); - - // Perform test - const tokenRequest = browserStorage.getCachedRequest( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - expect(tokenRequest.authority).toBe(alternateAuthority); - }); - - it("cleanRequestByInteractionType() returns early if state is not present", () => { - let dbStorage = {}; - jest.spyOn(DatabaseStorage.prototype, "open").mockImplementation( - async (): Promise => { - dbStorage = {}; - } - ); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - logger, - new StubPerformanceClient(), - new EventHandler() - ); - - const cacheKey = "cacheKey"; - const cacheValue = "cacheValue"; - browserStorage.setTemporaryCache(cacheKey, cacheValue, true); - browserStorage.cleanRequestByInteractionType( - InteractionType.Redirect - ); - expect(browserStorage.getTemporaryCache(cacheKey, true)).toBe( - cacheValue - ); - browserStorage.clear(); - }); - - it("cleanRequestByInteractionType() cleans cache", () => { - let dbStorage = {}; - jest.spyOn(DatabaseStorage.prototype, "open").mockImplementation( - async (): Promise => { - dbStorage = {}; - } - ); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - logger, - new StubPerformanceClient(), - new EventHandler() - ); - - const browserState: BrowserStateObject = { - interactionType: InteractionType.Redirect, - }; - - jest.spyOn(CryptoOps.prototype, "createNewGuid").mockReturnValue( - RANDOM_TEST_GUID - ); - const state = ProtocolUtils.setRequestState( - browserCrypto, - undefined, - browserState - ); - const cacheKey = `cacheKey.${state}`; - const cacheValue = "cacheValue"; - browserStorage.setTemporaryCache(cacheKey, cacheValue, true); - browserStorage.setTemporaryCache( - `${TemporaryCacheKeys.REQUEST_STATE}.${RANDOM_TEST_GUID}`, - state, - true - ); - browserStorage.cleanRequestByInteractionType( - InteractionType.Redirect - ); - expect(browserStorage.getKeys()).toHaveLength(0); - }); - it("cleanRequestByInteractionType() interaction status even no request is in progress", () => { - let dbStorage = {}; - jest.spyOn(DatabaseStorage.prototype, "open").mockImplementation( - async (): Promise => { - dbStorage = {}; - } - ); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - { - ...cacheConfig, - storeAuthStateInCookie: true, - }, - browserCrypto, - logger, - new StubPerformanceClient(), - new EventHandler() - ); - - browserStorage.setInteractionInProgress(true); - browserStorage.cleanRequestByInteractionType( - InteractionType.Redirect - ); - expect(browserStorage.getInteractionInProgress()).toBeFalsy(); - }); - - it("addTokenKey adds credential to key map and removeTokenKey removes the given credential from the key map", () => { - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - { - ...cacheConfig, - }, - browserCrypto, - logger, - new StubPerformanceClient(), - new EventHandler() - ); - - expect(browserStorage.getTokenKeys()).toStrictEqual({ - idToken: [], - accessToken: [], - refreshToken: [], - }); - - browserStorage.addTokenKey("idToken1", CredentialType.ID_TOKEN); - browserStorage.addTokenKey("idToken2", CredentialType.ID_TOKEN); - expect(browserStorage.getTokenKeys()).toStrictEqual({ - idToken: ["idToken1", "idToken2"], - accessToken: [], - refreshToken: [], - }); - - browserStorage.addTokenKey( - "accessToken1", - CredentialType.ACCESS_TOKEN - ); - browserStorage.addTokenKey( - "accessToken2", - CredentialType.ACCESS_TOKEN - ); - expect(browserStorage.getTokenKeys()).toStrictEqual({ - idToken: ["idToken1", "idToken2"], - accessToken: ["accessToken1", "accessToken2"], - refreshToken: [], - }); - - browserStorage.addTokenKey( - "refreshToken1", - CredentialType.REFRESH_TOKEN - ); - browserStorage.addTokenKey( - "refreshToken2", - CredentialType.REFRESH_TOKEN - ); - expect(browserStorage.getTokenKeys()).toStrictEqual({ - idToken: ["idToken1", "idToken2"], - accessToken: ["accessToken1", "accessToken2"], - refreshToken: ["refreshToken1", "refreshToken2"], - }); - - browserStorage.removeTokenKey("idToken1", CredentialType.ID_TOKEN); - expect(browserStorage.getTokenKeys()).toStrictEqual({ - idToken: ["idToken2"], - accessToken: ["accessToken1", "accessToken2"], - refreshToken: ["refreshToken1", "refreshToken2"], - }); - - browserStorage.removeTokenKey( - "accessToken2", - CredentialType.ACCESS_TOKEN - ); - expect(browserStorage.getTokenKeys()).toStrictEqual({ - idToken: ["idToken2"], - accessToken: ["accessToken1"], - refreshToken: ["refreshToken1", "refreshToken2"], - }); - - browserStorage.removeTokenKey( - "refreshToken1", - CredentialType.REFRESH_TOKEN - ); - expect(browserStorage.getTokenKeys()).toStrictEqual({ - idToken: ["idToken2"], - accessToken: ["accessToken1"], - refreshToken: ["refreshToken2"], - }); - - // Attempting to remove keys which exist as a different credential type results in a no-op - browserStorage.removeTokenKey( - "idToken2", - CredentialType.ACCESS_TOKEN - ); - browserStorage.removeTokenKey( - "idToken2", - CredentialType.REFRESH_TOKEN - ); - browserStorage.removeTokenKey( - "accessToken1", - CredentialType.ID_TOKEN - ); - browserStorage.removeTokenKey( - "accessToken1", - CredentialType.REFRESH_TOKEN - ); - browserStorage.removeTokenKey( - "refreshToken2", - CredentialType.ID_TOKEN - ); - browserStorage.removeTokenKey( - "refreshToken2", - CredentialType.ACCESS_TOKEN - ); - expect(browserStorage.getTokenKeys()).toStrictEqual({ - idToken: ["idToken2"], - accessToken: ["accessToken1"], - refreshToken: ["refreshToken2"], - }); - }); }); }); diff --git a/lib/msal-browser/test/cache/LocalStorage.spec.ts b/lib/msal-browser/test/cache/LocalStorage.spec.ts index a86743b7dd..ae2d144015 100644 --- a/lib/msal-browser/test/cache/LocalStorage.spec.ts +++ b/lib/msal-browser/test/cache/LocalStorage.spec.ts @@ -25,22 +25,26 @@ describe("LocalStorage tests", () => { await localStorageInstance.setUserData( idTokenKey, idTokenVal, - TEST_CONFIG.CORRELATION_ID + TEST_CONFIG.CORRELATION_ID, + Date.now().toString() ); await localStorageInstance.setUserData( accessTokenKey, accessTokenVal, - TEST_CONFIG.CORRELATION_ID + TEST_CONFIG.CORRELATION_ID, + Date.now().toString() ); await localStorageInstance.setUserData( refreshTokenKey, refreshTokenVal, - TEST_CONFIG.CORRELATION_ID + TEST_CONFIG.CORRELATION_ID, + Date.now().toString() ); await localStorageInstance.setUserData( accountKey, accountVal, - TEST_CONFIG.CORRELATION_ID + TEST_CONFIG.CORRELATION_ID, + Date.now().toString() ); localStorage.setItem( @@ -157,7 +161,8 @@ describe("LocalStorage tests", () => { await localStorageInstance.setUserData( "testKey", "testVal", - TEST_CONFIG.CORRELATION_ID + TEST_CONFIG.CORRELATION_ID, + Date.now().toString() ); expect(localStorage.getItem("testKey")).toBeTruthy(); // Encrypted expect(localStorageInstance.getUserData("testKey")).toBe("testVal"); // From in-memory @@ -205,7 +210,8 @@ describe("LocalStorage tests", () => { await localStorageInstance.setUserData( "testKey", "testVal", - TEST_CONFIG.CORRELATION_ID + TEST_CONFIG.CORRELATION_ID, + Date.now().toString() ); const encrypted = localStorage.getItem("testKey") || ""; @@ -238,7 +244,8 @@ describe("LocalStorage tests", () => { await localStorageInstance1.setUserData( "testKey", "testVal", - TEST_CONFIG.CORRELATION_ID + TEST_CONFIG.CORRELATION_ID, + Date.now().toString() ); expect(localStorageInstance1.getUserData("testKey")).toBe("testVal"); diff --git a/lib/msal-browser/test/cache/TestStorageManager.ts b/lib/msal-browser/test/cache/TestStorageManager.ts index f3fceedaf3..b9e7c33588 100644 --- a/lib/msal-browser/test/cache/TestStorageManager.ts +++ b/lib/msal-browser/test/cache/TestStorageManager.ts @@ -13,10 +13,10 @@ import { ServerTelemetryEntity, ThrottlingEntity, AuthorityMetadataEntity, - ValidCredentialType, TokenKeys, CacheHelpers, } from "@azure/msal-common"; +import { RANDOM_TEST_GUID } from "../utils/StringConstants.js"; const ACCOUNT_KEYS = "ACCOUNT_KEYS"; const TOKEN_KEYS = "TOKEN_KEYS"; @@ -53,8 +53,8 @@ export class TestStorageManager extends CacheManager { } } - async removeAccount(key: string): Promise { - await super.removeAccount(key); + removeAccount(key: string): void { + super.removeAccount(key, RANDOM_TEST_GUID); this.removeAccountKeyFromMap(key); } diff --git a/lib/msal-browser/test/cache/TokenCache.spec.ts b/lib/msal-browser/test/cache/TokenCache.spec.ts index 7ebbbbaed8..c6749391e4 100644 --- a/lib/msal-browser/test/cache/TokenCache.spec.ts +++ b/lib/msal-browser/test/cache/TokenCache.spec.ts @@ -29,6 +29,7 @@ import { import { BrowserCacheLocation } from "../../src/utils/BrowserConstants.js"; import { ID_TOKEN_CLAIMS, + RANDOM_TEST_GUID, TEST_CONFIG, TEST_DATA_CLIENT_INFO, TEST_TOKENS, @@ -36,9 +37,7 @@ import { TEST_URIS, } from "../utils/StringConstants.js"; import { - BrowserAuthError, BrowserAuthErrorCodes, - BrowserAuthErrorMessage, PublicClientApplication, SilentRequest, } from "../../src/index.js"; @@ -103,15 +102,8 @@ describe("TokenCache tests", () => { let testIdToken: string; let testIdTokenClaims: TokenClaims; let testHomeAccountId: string; - let idTokenEntity: IdTokenEntity; - let idTokenKey: string; let testAccessToken: string; - let accessTokenEntity: AccessTokenEntity; - let accessTokenKey: string; - let scopeString: string; let testRefreshToken: string; - let refreshTokenEntity: RefreshTokenEntity; - let refreshTokenKey: string; beforeEach(() => { tokenCache = new TokenCache( @@ -136,49 +128,19 @@ describe("TokenCache tests", () => { testIdTokenClaims ); - idTokenEntity = CacheHelpers.createIdTokenEntity( - testHomeAccountId, - testEnvironment, - TEST_TOKENS.IDTOKEN_V2, - configuration.auth.clientId, - ID_TOKEN_CLAIMS.tid - ); - idTokenKey = CacheHelpers.generateCredentialKey(idTokenEntity); - - scopeString = new ScopeSet( - TEST_CONFIG.DEFAULT_SCOPES - ).printScopes(); - (testAccessToken = TEST_TOKENS.ACCESS_TOKEN), - (accessTokenEntity = CacheHelpers.createAccessTokenEntity( - testHomeAccountId, - testEnvironment, - testAccessToken, - configuration.auth.clientId, - TEST_CONFIG.TENANT, - scopeString, - TEST_TOKEN_LIFETIMES.TEST_ACCESS_TOKEN_EXP, - TEST_TOKEN_LIFETIMES.TEST_ACCESS_TOKEN_EXP, - cryptoObj.base64Decode - )); - accessTokenKey = - CacheHelpers.generateCredentialKey(accessTokenEntity); - + testAccessToken = TEST_TOKENS.ACCESS_TOKEN; testRefreshToken = TEST_TOKENS.REFRESH_TOKEN; - refreshTokenEntity = CacheHelpers.createRefreshTokenEntity( - testHomeAccountId, - testEnvironment, - testRefreshToken, - configuration.auth.clientId - ); - refreshTokenKey = - CacheHelpers.generateCredentialKey(refreshTokenEntity); }); afterEach(() => { - browserStorage.clear(); + browserStorage.clear(RANDOM_TEST_GUID); }); it("loads id token with a request account", async () => { + const setSpy = jest.spyOn( + BrowserCacheManager.prototype, + "setIdTokenCredential" + ); const requestHomeAccountId = TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID; const request: SilentRequest = { @@ -201,23 +163,18 @@ describe("TokenCache tests", () => { options ); - const testIdTokenEntity = CacheHelpers.createIdTokenEntity( - requestHomeAccountId, - testEnvironment, - TEST_TOKENS.IDTOKEN_V2, - configuration.auth.clientId, - TEST_CONFIG.TENANT - ); - const testIdTokenKey = - CacheHelpers.generateCredentialKey(testIdTokenEntity); - - expect(result.idToken).toEqual(TEST_TOKENS.IDTOKEN_V2); - expect(browserStorage.getIdTokenCredential(testIdTokenKey)).toEqual( - testIdTokenEntity + expect(result.idToken).toEqual(testIdToken); + expect(setSpy).toHaveBeenCalledWith( + expect.objectContaining({ secret: testIdToken }), + expect.anything() ); }); it("loads id token with request authority and client info provided in options", async () => { + const setSpy = jest.spyOn( + BrowserCacheManager.prototype, + "setIdTokenCredential" + ); const request: SilentRequest = { scopes: TEST_CONFIG.DEFAULT_SCOPES, authority: `${TEST_URIS.DEFAULT_INSTANCE}${TEST_CONFIG.TENANT}`, @@ -235,13 +192,18 @@ describe("TokenCache tests", () => { options ); - expect(result.idToken).toEqual(TEST_TOKENS.IDTOKEN_V2); - expect(browserStorage.getIdTokenCredential(idTokenKey)).toEqual( - idTokenEntity + expect(result.idToken).toEqual(testIdToken); + expect(setSpy).toHaveBeenCalledWith( + expect.objectContaining({ secret: testIdToken }), + expect.anything() ); }); it("sets account when id token is loaded", async () => { + const setSpy = jest.spyOn( + BrowserCacheManager.prototype, + "setIdTokenCredential" + ); const request: SilentRequest = { scopes: TEST_CONFIG.DEFAULT_SCOPES, authority: `${TEST_URIS.DEFAULT_INSTANCE}${TEST_CONFIG.TENANT}`, @@ -266,17 +228,23 @@ describe("TokenCache tests", () => { options ); - expect(result.idToken).toEqual(TEST_TOKENS.IDTOKEN_V2); + expect(result.idToken).toEqual(testIdToken); expect(result.account).toEqual(testAccountInfo); - expect(browserStorage.getIdTokenCredential(idTokenKey)).toEqual( - idTokenEntity + expect(setSpy).toHaveBeenCalledWith( + expect.objectContaining({ secret: testIdToken }), + expect.anything() ); expect( - browserStorage.getAccount(testAccountKey)?.homeAccountId + browserStorage.getAccount(testAccountKey, RANDOM_TEST_GUID) + ?.homeAccountId ).toEqual(testAccountInfo.homeAccountId); }); it("loads id token with request authority and client info provided in response", async () => { + const setSpy = jest.spyOn( + BrowserCacheManager.prototype, + "setIdTokenCredential" + ); const request: SilentRequest = { scopes: TEST_CONFIG.DEFAULT_SCOPES, authority: `${TEST_URIS.DEFAULT_INSTANCE}${TEST_CONFIG.TENANT}`, @@ -292,9 +260,10 @@ describe("TokenCache tests", () => { options ); - expect(result.idToken).toEqual(TEST_TOKENS.IDTOKEN_V2); - expect(browserStorage.getIdTokenCredential(idTokenKey)).toEqual( - idTokenEntity + expect(result.idToken).toEqual(testIdToken); + expect(setSpy).toHaveBeenCalledWith( + expect.objectContaining({ secret: testIdToken }), + expect.anything() ); }); @@ -342,6 +311,14 @@ describe("TokenCache tests", () => { }); it("skips storing access token if server response provided does not have expires_in", async () => { + const idSpy = jest.spyOn( + BrowserCacheManager.prototype, + "setIdTokenCredential" + ); + const accessSpy = jest.spyOn( + BrowserCacheManager.prototype, + "setAccessTokenCredential" + ); const request: SilentRequest = { scopes: TEST_CONFIG.DEFAULT_SCOPES, account: { @@ -365,16 +342,19 @@ describe("TokenCache tests", () => { ); expect(result.idToken).toEqual(TEST_TOKENS.IDTOKEN_V2); - expect(browserStorage.getIdTokenCredential(idTokenKey)).toEqual( - idTokenEntity + expect(idSpy).toHaveBeenCalledWith( + expect.objectContaining({ secret: testIdToken }), + expect.anything() ); expect(result.accessToken).toEqual(""); - expect( - browserStorage.getAccessTokenCredential(accessTokenKey) - ).toEqual(null); + expect(accessSpy).not.toHaveBeenCalled(); }); it("loads access tokens from server response and token options", async () => { + const accessSpy = jest.spyOn( + BrowserCacheManager.prototype, + "setAccessTokenCredential" + ); const request: SilentRequest = { scopes: TEST_CONFIG.DEFAULT_SCOPES, account: { @@ -400,13 +380,11 @@ describe("TokenCache tests", () => { options ); - expect(parseInt(accessTokenEntity.expiresOn)).toBeGreaterThan( - TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN + expect(result.accessToken).toEqual(testAccessToken); + expect(accessSpy).toHaveBeenCalledWith( + expect.objectContaining({ secret: testAccessToken }), + expect.anything() ); - expect(result.accessToken).toEqual(accessTokenEntity.secret); - expect( - browserStorage.getAccessTokenCredential(accessTokenKey) - ).toEqual(accessTokenEntity); }); it("throws error if in non-browser environment", (done) => { @@ -441,6 +419,10 @@ describe("TokenCache tests", () => { }); it("loads refresh token with request authority and client info provided in response", async () => { + const refreshSpy = jest.spyOn( + BrowserCacheManager.prototype, + "setRefreshTokenCredential" + ); const request: SilentRequest = { scopes: TEST_CONFIG.DEFAULT_SCOPES, authority: `${TEST_URIS.DEFAULT_INSTANCE}${TEST_CONFIG.TENANT}`, @@ -453,12 +435,17 @@ describe("TokenCache tests", () => { await tokenCache.loadExternalTokens(request, response, options); - expect( - browserStorage.getRefreshTokenCredential(refreshTokenKey) - ).toEqual(refreshTokenEntity); + expect(refreshSpy).toHaveBeenCalledWith( + expect.objectContaining({ secret: testRefreshToken }), + expect.anything() + ); }); it("loads refresh token with request authority and client info provided in options", async () => { + const refreshSpy = jest.spyOn( + BrowserCacheManager.prototype, + "setRefreshTokenCredential" + ); const request: SilentRequest = { scopes: TEST_CONFIG.DEFAULT_SCOPES, authority: `${TEST_URIS.DEFAULT_INSTANCE}${TEST_CONFIG.TENANT}`, @@ -489,12 +476,21 @@ describe("TokenCache tests", () => { ).toEqual(result.account); // Validate tokens can be retrieved - expect( - browserStorage.getRefreshTokenCredential(refreshTokenKey) - ).toEqual(refreshTokenEntity); + expect(refreshSpy).toHaveBeenCalledWith( + expect.objectContaining({ secret: testRefreshToken }), + expect.anything() + ); }); it("loads refresh token with request authority and information from id_token", async () => { + const idSpy = jest.spyOn( + BrowserCacheManager.prototype, + "setIdTokenCredential" + ); + const refreshSpy = jest.spyOn( + BrowserCacheManager.prototype, + "setRefreshTokenCredential" + ); const request: SilentRequest = { scopes: TEST_CONFIG.DEFAULT_SCOPES, authority: `${TEST_URIS.DEFAULT_INSTANCE}${TEST_CONFIG.TENANT}`, @@ -519,32 +515,15 @@ describe("TokenCache tests", () => { testIdTokenClaims ); - idTokenEntity = CacheHelpers.createIdTokenEntity( - testHomeAccountId, - testEnvironment, - TEST_TOKENS.IDTOKEN_V2, - configuration.auth.clientId, - ID_TOKEN_CLAIMS.tid + expect(result.idToken).toEqual(TEST_TOKENS.IDTOKEN_V2); + expect(idSpy).toHaveBeenCalledWith( + expect.objectContaining({ secret: testIdToken }), + expect.anything() ); - - refreshTokenEntity = CacheHelpers.createRefreshTokenEntity( - testHomeAccountId, - testEnvironment, - testRefreshToken, - configuration.auth.clientId + expect(refreshSpy).toHaveBeenCalledWith( + expect.objectContaining({ secret: testRefreshToken }), + expect.anything() ); - - expect(result.idToken).toEqual(TEST_TOKENS.IDTOKEN_V2); - expect( - browserStorage.getIdTokenCredential( - CacheHelpers.generateCredentialKey(idTokenEntity) - ) - ).toEqual(idTokenEntity); - expect( - browserStorage.getRefreshTokenCredential( - CacheHelpers.generateCredentialKey(refreshTokenEntity) - ) - ).toEqual(refreshTokenEntity); }); }); }); diff --git a/lib/msal-browser/test/crypto/BrowserCrypto.spec.ts b/lib/msal-browser/test/crypto/BrowserCrypto.spec.ts new file mode 100644 index 0000000000..d8a7003f91 --- /dev/null +++ b/lib/msal-browser/test/crypto/BrowserCrypto.spec.ts @@ -0,0 +1,155 @@ +import { + decryptEarResponse, + generateEarKey, +} from "../../src/crypto/BrowserCrypto.js"; +import { base64Decode } from "../../src/encode/Base64Decode.js"; +import { base64Encode, urlEncodeArr } from "../../src/encode/Base64Encode.js"; +import { BrowserAuthError, BrowserAuthErrorCodes } from "../../src/index.js"; +import { + generateValidEarJWE, + TEST_TOKEN_RESPONSE, + validEarJWE, + validEarJWK, +} from "../utils/StringConstants.js"; + +describe("BrowserCrypto Tests", () => { + describe("generateEarKey", () => { + it("Returns Base64 encoded string of ear_jwk", async () => { + const key = await window.crypto.subtle.generateKey( + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"] + ); + const rawKey = await window.crypto.subtle.exportKey("raw", key); + const keyStr = urlEncodeArr(new Uint8Array(rawKey)); + + const genKeySpy = jest + .spyOn(window.crypto.subtle, "generateKey") + .mockResolvedValue(key); + const exportKeySpy = jest + .spyOn(window.crypto.subtle, "exportKey") + .mockResolvedValue(rawKey); + const encodedJwk = await generateEarKey(); + expect(genKeySpy).toHaveBeenCalledWith( + { name: "AES-GCM", length: 256 }, + true, + ["encrypt", "decrypt"] + ); + expect(exportKeySpy).toHaveBeenCalledWith("raw", key); + + const decodedJwk = base64Decode(encodedJwk); + const jwk = JSON.parse(decodedJwk); + + expect(jwk.alg).toEqual("dir"); + expect(jwk.kty).toEqual("oct"); + expect(jwk.k).toEqual(keyStr); + }); + }); + + describe("decryptEarResponse", () => { + it("Throws if ear_jwe has fewer than 5 parts", (done) => { + decryptEarResponse(validEarJWK, "header.iv.ciphertext.tag").catch( + (e) => { + expect(e).toBeInstanceOf(BrowserAuthError); + expect(e.errorCode).toBe( + BrowserAuthErrorCodes.failedToDecryptEarResponse + ); + expect(e.subError).toBe("jwe_length"); + done(); + } + ); + }); + + it("Throws if ear_jwe has more than 5 parts", (done) => { + decryptEarResponse( + validEarJWK, + "header..iv..ciphertext..tag" + ).catch((e) => { + expect(e).toBeInstanceOf(BrowserAuthError); + expect(e.errorCode).toBe( + BrowserAuthErrorCodes.failedToDecryptEarResponse + ); + expect(e.subError).toBe("jwe_length"); + done(); + }); + }); + + it("Throws if earJwk does not have a 'k' property", (done) => { + const encodedJwk = base64Encode( + JSON.stringify({ alg: "dir", kty: "oct", key: "testKey" }) + ); + decryptEarResponse(encodedJwk, validEarJWE).catch((e) => { + expect(e).toBeInstanceOf(BrowserAuthError); + expect(e.errorCode).toBe( + BrowserAuthErrorCodes.failedToDecryptEarResponse + ); + expect(e.subError).toBe("import_key"); + done(); + }); + }); + + it("Throws if earJwk cannot be B64 decoded", (done) => { + decryptEarResponse( + JSON.stringify({ alg: "dir", kty: "oct", k: "testKey" }), + validEarJWE + ).catch((e) => { + expect(e).toBeInstanceOf(BrowserAuthError); + expect(e.errorCode).toBe( + BrowserAuthErrorCodes.failedToDecryptEarResponse + ); + expect(e.subError).toBe("import_key"); + done(); + }); + }); + + it("Throws if earJwk is not a JSON object", async () => { + decryptEarResponse("notJSON", validEarJWE).catch((e) => { + expect(e).toBeInstanceOf(BrowserAuthError); + expect(e.errorCode).toBe( + BrowserAuthErrorCodes.failedToDecryptEarResponse + ); + expect(e.subError).toBe("import_key"); + }); + }); + + it("Throws if earJwk 'k' property is not a raw encryption key", (done) => { + decryptEarResponse( + base64Encode( + JSON.stringify({ alg: "dir", kty: "oct", k: "testKey" }) + ), + validEarJWE + ).catch((e) => { + expect(e).toBeInstanceOf(BrowserAuthError); + expect(e.errorCode).toBe( + BrowserAuthErrorCodes.failedToDecryptEarResponse + ); + expect(e.subError).toBe("import_key"); + done(); + }); + }); + + it("Throws if ear_jwe cannot be decrypted with the provided key", (done) => { + generateEarKey().then((jwk: string) => { + decryptEarResponse(jwk, validEarJWE).catch((e) => { + expect(e).toBeInstanceOf(BrowserAuthError); + expect(e.errorCode).toBe( + BrowserAuthErrorCodes.failedToDecryptEarResponse + ); + expect(e.subError).toBe("decrypt"); + done(); + }); + }); + }); + + it("Successfully decrypts ear_jwe with given earJwk", async () => { + const jwe = await generateValidEarJWE( + JSON.stringify(TEST_TOKEN_RESPONSE.body), + validEarJWK + ); + const decryptedString = await decryptEarResponse(validEarJWK, jwe); + expect(JSON.parse(decryptedString)).toEqual( + TEST_TOKEN_RESPONSE.body + ); + }); + }); +}); diff --git a/lib/msal-browser/test/crypto/CryptoOps.spec.ts b/lib/msal-browser/test/crypto/CryptoOps.spec.ts index e9a19117de..6f400ed6b5 100644 --- a/lib/msal-browser/test/crypto/CryptoOps.spec.ts +++ b/lib/msal-browser/test/crypto/CryptoOps.spec.ts @@ -301,10 +301,9 @@ describe("CryptoOps.ts Unit Tests", () => { resourceRequestUri: TEST_URIS.TEST_AUTH_ENDPT_WITH_PARAMS, } as BaseAuthRequest); const key = mockDatabase["TestDB.keys"][pkThumbprint]; - const keyDeleted = await cryptoObj.removeTokenBindingKey(pkThumbprint); + await cryptoObj.removeTokenBindingKey(pkThumbprint); expect(key).not.toBe(undefined); expect(mockDatabase["TestDB.keys"][pkThumbprint]).toBe(undefined); - expect(keyDeleted).toBe(true); }, 30000); it("signJwt() throws signingKeyNotFoundInStorage error if signing keypair is not found in storage", async () => { diff --git a/lib/msal-browser/test/custom_auth/CustomAuthPublicClientApplication.spec.ts b/lib/msal-browser/test/custom_auth/CustomAuthPublicClientApplication.spec.ts new file mode 100644 index 0000000000..31382b0559 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/CustomAuthPublicClientApplication.spec.ts @@ -0,0 +1,184 @@ +import { ICustomAuthStandardController } from "../../src/custom_auth/controller/ICustomAuthStandardController.js"; +import { InvalidConfigurationError } from "../../src/custom_auth/core/error/InvalidConfigurationError.js"; +import { CustomAuthPublicClientApplication } from "../../src/custom_auth/CustomAuthPublicClientApplication.js"; +import { customAuthConfig } from "./test_resources/CustomAuthConfig.js"; +import { SignUpResult } from "../../src/custom_auth/sign_up/auth_flow/result/SignUpResult.js"; +import { CustomAuthError } from "../../src/custom_auth/core/error/CustomAuthError.js"; +import { ResetPasswordStartResult } from "../../src/custom_auth/reset_password/auth_flow/result/ResetPasswordStartResult.js"; +import { GetAccountResult } from "../../src/custom_auth/get_account/auth_flow/result/GetAccountResult.js"; +import { CustomAuthStandardController } from "../../src/custom_auth/controller/CustomAuthStandardController.js"; + +describe("CustomAuthPublicClientApplication", () => { + let mockController: jest.Mocked; + + beforeEach(() => { + mockController = { + signIn: jest.fn(), + signUp: jest.fn(), + resetPassword: jest.fn(), + getCurrentAccount: jest.fn(), + } as unknown as jest.Mocked; + }); + + describe("constructor and config validation", () => { + it("should throw an error if the config is null", async () => { + await expect( + CustomAuthPublicClientApplication.create(null as any) + ).rejects.toThrow(InvalidConfigurationError); + }); + + it("should throw an error if the authority is missing", async () => { + const invalidConfig = { auth: {}, customAuth: {} } as any; + + await expect( + CustomAuthPublicClientApplication.create(invalidConfig) + ).rejects.toThrow(InvalidConfigurationError); + }); + + it("should throw an error if challenge type is invalid", async () => { + const invalidConfig = { + auth: { authority: customAuthConfig.auth.authority }, + customAuth: { + challengeTypes: ["invalid-challenge-type", "oob"], + }, + }; + + await expect( + CustomAuthPublicClientApplication.create(invalidConfig as any) + ).rejects.toThrow(InvalidConfigurationError); + }); + + it("should create an instance if the config is valid", async () => { + const app = await CustomAuthPublicClientApplication.create( + customAuthConfig + ); + + expect(app).toBeInstanceOf(CustomAuthPublicClientApplication); + + const controller = (app as CustomAuthPublicClientApplication)[ + "customAuthController" + ] as CustomAuthStandardController; + controller["eventHandler"]["broadcastChannel"]?.close(); + }); + }); + + describe("signIn", () => { + it("should call the customAuthController signIn method with correct inputs", async () => { + const mockSignInInputs = { + username: "testuser", + password: "testpassword", + }; + + const mockSignInResult = { accessToken: "test-token" }; + + mockController.signIn.mockResolvedValueOnce( + mockSignInResult as any + ); + + const app = await CustomAuthPublicClientApplication.create( + customAuthConfig + ); + + (app as any)["customAuthController"] = mockController; + + const result = await app.signIn(mockSignInInputs); + + expect(mockController.signIn).toHaveBeenCalledWith( + mockSignInInputs + ); + expect(result).toEqual(mockSignInResult); + }); + }); + + describe("signUp", () => { + it("should call the customAuthController signUp method with correct inputs", async () => { + const mockSignUpInputs = { + username: "testuser", + password: "testpassword", + }; + + const mockSignUpResult = SignUpResult.createWithError( + new CustomAuthError("test-error") + ); + + mockController.signUp.mockResolvedValueOnce( + mockSignUpResult as any + ); + + const app = await CustomAuthPublicClientApplication.create( + customAuthConfig + ); + + (app as any)["customAuthController"] = mockController; + + const result = await app.signUp(mockSignUpInputs); + + expect(mockController.signUp).toHaveBeenCalledWith( + mockSignUpInputs + ); + expect(result).toEqual(mockSignUpResult); + }); + }); + + describe("resetPassword", () => { + it("should call the customAuthController resetPassword method with correct inputs", async () => { + const mockResetPasswordInputs = { + username: "testuser", + }; + + const mockResetPasswordResult = + ResetPasswordStartResult.createWithError( + new CustomAuthError("test-error") + ); + + mockController.resetPassword.mockResolvedValueOnce( + mockResetPasswordResult as any + ); + + const app = await CustomAuthPublicClientApplication.create( + customAuthConfig + ); + + (app as any)["customAuthController"] = mockController; + + const result = await app.resetPassword(mockResetPasswordInputs); + + expect(mockController.resetPassword).toHaveBeenCalledWith( + mockResetPasswordInputs + ); + expect(result).toEqual(mockResetPasswordResult); + }); + }); + + describe("getCurrentAccount", () => { + it("should call the customAuthController getCurrentAccount method with correct inputs", async () => { + const mockGetCurrentAccountInputs = { + correlationId: "test-id", + }; + + const mockGetCurrentAccountResult = + GetAccountResult.createWithError( + new CustomAuthError("test-error") + ); + + mockController.getCurrentAccount.mockReturnValue( + mockGetCurrentAccountResult as any + ); + + const app = await CustomAuthPublicClientApplication.create( + customAuthConfig + ); + + (app as any)["customAuthController"] = mockController; + + const result = await app.getCurrentAccount( + mockGetCurrentAccountInputs + ); + + expect(mockController.getCurrentAccount).toHaveBeenCalledWith( + mockGetCurrentAccountInputs + ); + expect(result).toEqual(mockGetCurrentAccountResult); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/controller/CustomAuthStandardController.spec.ts b/lib/msal-browser/test/custom_auth/controller/CustomAuthStandardController.spec.ts new file mode 100644 index 0000000000..fc43d93a07 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/controller/CustomAuthStandardController.spec.ts @@ -0,0 +1,411 @@ +import { CustomAuthStandardController } from "../../../src/custom_auth/controller/CustomAuthStandardController.js"; +import { + ResetPasswordInputs, + SignInInputs, + SignUpInputs, +} from "../../../src/custom_auth/CustomAuthActionInputs.js"; +import { CustomAuthOperatingContext } from "../../../src/custom_auth/operating_context/CustomAuthOperatingContext.js"; +import { customAuthConfig } from "../test_resources/CustomAuthConfig.js"; +import { SignInError } from "../../../src/custom_auth/sign_in/auth_flow/error_type/SignInError.js"; +import { SignInResult } from "../../../src/custom_auth/sign_in/auth_flow/result/SignInResult.js"; +import { CustomAuthAccountData } from "../../../src/custom_auth/get_account/auth_flow/CustomAuthAccountData.js"; +import { SignUpError } from "../../../src/custom_auth/sign_up/auth_flow/error_type/SignUpError.js"; +import { ChallengeType } from "../../../src/custom_auth/CustomAuthConstants.js"; +import { + CustomAuthApiError, + RedirectError, +} from "../../../src/custom_auth/core/error/CustomAuthApiError.js"; +import { SignUpResult } from "../../../src/custom_auth/sign_up/auth_flow/result/SignUpResult.js"; +import * as CustomAuthApiErrorCode from "../../../src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorCodes.js"; +import * as CustomAuthApiSuberror from "../../../src/custom_auth/core/network_client/custom_auth_api/types/ApiSuberrors.js"; +import { ResetPasswordError } from "../../../src/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.js"; +import { ResetPasswordCodeRequiredState } from "../../../src/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.js"; +import { ResetPasswordStartResult } from "../../../src/custom_auth/reset_password/auth_flow/result/ResetPasswordStartResult.js"; +import { TestServerTokenResponse } from "../test_resources/TestConstants.js"; + +jest.mock( + "../../../src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.js", + () => { + let signInApiClient = { + initiate: jest.fn(), + requestChallenge: jest.fn(), + requestTokensWithPassword: jest.fn(), + requestTokensWithOTP: jest.fn(), + }; + let signUpApiClient = { + start: jest.fn(), + requestChallenge: jest.fn(), + continue: jest.fn(), + continueWithPassword: jest.fn(), + continueWithAttributes: jest.fn(), + }; + let resetPasswordApiClient = { + start: jest.fn(), + requestChallenge: jest.fn(), + continueWithCode: jest.fn(), + submitNewPassword: jest.fn(), + pollCompletion: jest.fn(), + }; + + // Set up the prototype or instance methods/properties + const CustomAuthApiClient = jest.fn().mockImplementation(() => ({ + signInApi: signInApiClient, + signUpApi: signUpApiClient, + resetPasswordApi: resetPasswordApiClient, + })); + + return { + CustomAuthApiClient, + signInApiClient, + signUpApiClient, + resetPasswordApiClient, + }; + } +); + +describe("CustomAuthStandardController", () => { + let controller: CustomAuthStandardController; + const { signInApiClient, signUpApiClient, resetPasswordApiClient } = + jest.requireMock( + "../../../src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.js" + ); + + beforeEach(() => { + const context = new CustomAuthOperatingContext(customAuthConfig); + controller = new CustomAuthStandardController(context); + + global.fetch = jest.fn(); // Mock the fetch API + }); + + afterEach(() => { + // controller.closeEventChannel(); + jest.clearAllMocks(); // Clear mocks between tests + if ( + controller && + controller["eventHandler"] && + controller["eventHandler"]["broadcastChannel"] + ) { + controller["eventHandler"]["broadcastChannel"].close(); + } + }); + + describe("signIn", () => { + it("should return error result if provided username is invalid", async () => { + const signInInputs: SignInInputs = { + correlationId: "correlation-id", + username: "", + }; + + const result = await controller.signIn(signInInputs); + + expect(result.error).toBeDefined(); + expect(result.error).toBeInstanceOf(SignInError); + + expect(result.error?.isInvalidUsername()).toBe(true); + }); + + it("should return code required result if the challenge type is oob", async () => { + signInApiClient.initiate.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + signInApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.OOB, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + code_length: 6, + challenge_channel: "email", + target_challenge_label: "email", + }); + + const signInInputs: SignInInputs = { + correlationId: "correlation-id", + username: "test@test.com", + }; + + const result = await controller.signIn(signInInputs); + + expect(result).toBeInstanceOf(SignInResult); + expect(result.error).toBeUndefined(); + expect(result.isCodeRequired()).toBe(true); + }); + + it("should return password required result if the challenge type is password", async () => { + signInApiClient.initiate.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + signInApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.PASSWORD, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + }); + + const signInInputs: SignInInputs = { + correlationId: "correlation-id", + username: "test@test.com", + }; + + const result = await controller.signIn(signInInputs); + + expect(result).toBeInstanceOf(SignInResult); + expect(result.error).toBeUndefined(); + expect(result.isPasswordRequired()).toBe(true); + }); + + it("should return correct completed result if the challenge type is password and password is provided", async () => { + signInApiClient.initiate.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + signInApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.PASSWORD, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + }); + signInApiClient.requestTokensWithPassword.mockResolvedValue( + TestServerTokenResponse + ); + + const signInInputs: SignInInputs = { + correlationId: "correlation-id", + username: "test@test.com", + password: "test-password", + }; + + const result = await controller.signIn(signInInputs); + + expect(result).toBeInstanceOf(SignInResult); + expect(result.error).toBeUndefined(); + expect(result.isCompleted()).toBe(true); + expect(result.data).toBeDefined(); + expect(result.data).toBeInstanceOf(CustomAuthAccountData); + + // Sign out for the other tests. + await result.data?.signOut(); + }); + + it("should return failed result if the challenge type is redirect", async () => { + signInApiClient.initiate.mockRejectedValue(new RedirectError()); + + const signInInputs: SignInInputs = { + correlationId: "correlation-id", + username: "test@test.com", + password: "test-password", + }; + + const result = await controller.signIn(signInInputs); + + expect(result).toBeInstanceOf(SignInResult); + expect(result.error).toBeDefined(); + expect(result.error?.errorData).toBeDefined(); + expect(result.error?.isRedirectRequired()).toEqual(true); + expect(result.isFailed()).toBe(true); + }); + }); + + describe("signUp", () => { + it("should return error result if provided username is empty", async () => { + const signUpInputs: SignUpInputs = { + correlationId: "correlation-id", + username: "", + }; + + const result = await controller.signUp(signUpInputs); + + expect(result.error).toBeDefined(); + expect(result.error).toBeInstanceOf(SignUpError); + + expect(result.error?.isInvalidUsername()).toBe(true); + }); + + it("should return result with code required state if the challenge type is oob", async () => { + signUpApiClient.start.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + signUpApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.OOB, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + code_length: 6, + challenge_channel: "email", + challenge_target_label: "email", + }); + + const signUpInputs: SignUpInputs = { + correlationId: "correlation-id", + username: "test@test.com", + }; + + const result = await controller.signUp(signUpInputs); + + expect(result).toBeInstanceOf(SignUpResult); + expect(result.error).toBeUndefined(); + expect(result.isCodeRequired()).toBe(true); + }); + + it("should return result with password required state if the challenge type is password", async () => { + signUpApiClient.start.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + signUpApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.PASSWORD, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + }); + + const signUpInputs: SignUpInputs = { + correlationId: "correlation-id", + username: "test@test.com", + }; + + const result = await controller.signUp(signUpInputs); + + expect(result).toBeInstanceOf(SignUpResult); + expect(result.error).toBeUndefined(); + expect(result.isPasswordRequired()).toBe(true); + }); + + it("should return failed result if the start endpoint returns redirect challenge type", async () => { + signUpApiClient.start.mockRejectedValue(new RedirectError()); + + const signUpInputs: SignUpInputs = { + correlationId: "correlation-id", + username: "test@test.com", + }; + + const result = await controller.signUp(signUpInputs); + + expect(result).toBeInstanceOf(SignUpResult); + expect(result.error).toBeDefined(); + expect(result.error?.errorData).toBeDefined(); + expect(result.error?.isRedirectRequired()).toEqual(true); + expect(result.isFailed()).toBe(true); + }); + + it("should return failed result if the challenge endpoint returns redirect challenge type", async () => { + signUpApiClient.start.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + signUpApiClient.requestChallenge.mockRejectedValue( + new RedirectError() + ); + + const signUpInputs: SignUpInputs = { + correlationId: "correlation-id", + username: "test@test.com", + }; + + const result = await controller.signUp(signUpInputs); + + expect(result).toBeInstanceOf(SignUpResult); + expect(result.error).toBeDefined(); + expect(result.error?.errorData).toBeDefined(); + expect(result.error?.isRedirectRequired()).toEqual(true); + expect(result.isFailed()).toBe(true); + }); + + it("should return failed result if the password is too weak", async () => { + signUpApiClient.start.mockRejectedValue( + new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_GRANT, + "Password is too weak", + "correlation-id", + [], + CustomAuthApiSuberror.PASSWORD_TOO_WEAK + ) + ); + + const signUpInputs: SignUpInputs = { + correlationId: "correlation-id", + username: "test@test.com", + }; + + const result = await controller.signUp(signUpInputs); + + expect(result).toBeInstanceOf(SignUpResult); + expect(result.error).toBeDefined(); + expect(result.error?.errorData).toBeDefined(); + expect(result.error?.isInvalidPassword()).toEqual(true); + expect(result.isFailed()).toBe(true); + }); + }); + + describe("resetPassword", () => { + it("should return error result if provided username is invalid", async () => { + // Empty username + let inputs: ResetPasswordInputs = { + correlationId: "correlation-id", + username: "", + }; + + let result = await controller.resetPassword(inputs); + + expect(result.error).toBeDefined(); + expect(result.error).toBeInstanceOf(ResetPasswordError); + + expect(result.error?.isInvalidUsername()).toBe(true); + }); + + it("should return code required result successfully", async () => { + resetPasswordApiClient.start.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + resetPasswordApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.OOB, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + code_length: 8, + challenge_channel: "email", + target_challenge_label: "email", + }); + + const inputs: ResetPasswordInputs = { + correlationId: "correlation-id", + username: "test@test.com", + }; + + const result = await controller.resetPassword(inputs); + + expect(result.error).toBeUndefined(); + expect(result.state).toBeInstanceOf(ResetPasswordCodeRequiredState); + expect(result.isCodeRequired()).toBe(true); + }); + + it("should return redirect error if the return challenge is redirect", async () => { + resetPasswordApiClient.start.mockRejectedValue(new RedirectError()); + + const inputs: ResetPasswordInputs = { + correlationId: "correlation-id", + username: "test@test.com", + }; + + const result = await controller.resetPassword(inputs); + + expect(result).toBeInstanceOf(ResetPasswordStartResult); + expect(result.error).toBeDefined(); + expect(result.error?.errorData).toBeDefined(); + expect(result.error?.isRedirectRequired()).toEqual(true); + expect(result.isFailed()).toBe(true); + }); + + it("should return failed result if the user is not found", async () => { + resetPasswordApiClient.start.mockRejectedValue( + new CustomAuthApiError( + CustomAuthApiErrorCode.USER_NOT_FOUND, + "User not found" + ) + ); + + const inputs: ResetPasswordInputs = { + correlationId: "correlation-id", + username: "test@test.com", + }; + + const result = await controller.resetPassword(inputs); + + expect(result).toBeInstanceOf(ResetPasswordStartResult); + expect(result.error).toBeDefined(); + expect(result.error?.errorData).toBeDefined(); + expect(result.error?.isUserNotFound()).toEqual(true); + expect(result.isFailed()).toBe(true); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/core/CustomAuthAuthority.spec.ts b/lib/msal-browser/test/custom_auth/core/CustomAuthAuthority.spec.ts new file mode 100644 index 0000000000..fe6ed036be --- /dev/null +++ b/lib/msal-browser/test/custom_auth/core/CustomAuthAuthority.spec.ts @@ -0,0 +1,178 @@ +import { StubbedNetworkModule } from "@azure/msal-common/browser"; +import { buildConfiguration } from "../../../src/config/Configuration.js"; +import { CustomAuthAuthority } from "../../../src/custom_auth/core/CustomAuthAuthority.js"; +import { customAuthConfig } from "../test_resources/CustomAuthConfig.js"; +import { + getDefaultBrowserCacheManager, + getDefaultLogger, +} from "../test_resources/TestModules.js"; + +describe("CustomAuthAuthority", () => { + const authorityUrl = customAuthConfig.auth.authority; + const customAuthProxyDomain = customAuthConfig.customAuth.authApiProxyUrl; + const authorityHostname = + authorityUrl && authorityUrl.startsWith("https") + ? authorityUrl.split("/")[2] + : authorityUrl; + const clientId = customAuthConfig.auth.clientId; + const authorityMetadataEntityKey = `authority-metadata-${clientId}-${authorityHostname}`; + + const config = buildConfiguration({ auth: { clientId: clientId } }, true); + const logger = getDefaultLogger(); + const browserStorage = getDefaultBrowserCacheManager( + clientId, + logger, + undefined, + undefined, + undefined, + config.cache + ); + + describe("constructor", () => { + it("should correctly parse and store the authority URL", () => { + const customAuthAuthority = new CustomAuthAuthority( + authorityUrl ?? "", + config, + StubbedNetworkModule, + browserStorage, + logger + ); + expect(customAuthAuthority.canonicalAuthority).toBe( + "https://spasamples.ciamlogin.com/spasamples.onmicrosoft.com/" + ); + }); + + it("should correctly store the customAuthProxyDomain when provided", () => { + const customAuthAuthority = new CustomAuthAuthority( + authorityUrl ?? "", + config, + StubbedNetworkModule, + browserStorage, + logger, + customAuthProxyDomain + ); + expect(customAuthAuthority["customAuthProxyDomain"]).toBe( + customAuthProxyDomain + ); + }); + + it("should correctly store the customAuthProxyDomain when provided", () => { + const customAuthAuthority = new CustomAuthAuthority( + "https://login.microsoftonline.com/", + config, + StubbedNetworkModule, + browserStorage, + logger, + customAuthProxyDomain + ); + expect(customAuthAuthority["customAuthProxyDomain"]).toBe( + customAuthProxyDomain + ); + }); + + it("should save authority metadata entity into cache", () => { + const customAuthAuthority = new CustomAuthAuthority( + authorityUrl ?? "", + config, + StubbedNetworkModule, + browserStorage, + logger + ); + expect(customAuthAuthority.canonicalAuthority).toBe( + "https://spasamples.ciamlogin.com/spasamples.onmicrosoft.com/" + ); + + const authorityHostname = + customAuthAuthority.canonicalAuthorityUrlComponents + .HostNameAndPort; + + expect( + browserStorage + .getAuthorityMetadataKeys() + .includes(authorityMetadataEntityKey) + ).toBe(true); + expect( + browserStorage.getAuthorityMetadata(authorityMetadataEntityKey) + ).toMatchObject({ + aliases: [authorityHostname], + preferred_cache: authorityHostname, + }); + }); + }); + + describe("tenant getter", () => { + it("should extract the tenant from the authority URL hostname", () => { + const customAuthAuthority = new CustomAuthAuthority( + authorityUrl ?? "", + config, + StubbedNetworkModule, + browserStorage, + logger + ); + expect(customAuthAuthority.tenant).toBe( + "spasamples.onmicrosoft.com" + ); + }); + }); + + describe("getCustomAuthDomain", () => { + it("should return the customAuthProxyDomain when provided", () => { + const customAuthAuthority = new CustomAuthAuthority( + authorityUrl ?? "", + config, + StubbedNetworkModule, + browserStorage, + logger, + customAuthProxyDomain + ); + expect(customAuthAuthority.getCustomAuthApiDomain()).toBe( + customAuthProxyDomain + ); + }); + + it("should generate the auth API domain based on the authority URL when customAuthProxyDomain is not provided", () => { + const customAuthAuthority = new CustomAuthAuthority( + authorityUrl ?? "", + config, + StubbedNetworkModule, + browserStorage, + logger + ); + expect(customAuthAuthority.getCustomAuthApiDomain()).toBe( + "https://spasamples.ciamlogin.com/spasamples.onmicrosoft.com/" + ); + }); + }); + + describe("getPreferredCache", () => { + it("should return the host of authority as preferred cache", () => { + const customAuthAuthority = new CustomAuthAuthority( + authorityUrl ?? "", + config, + StubbedNetworkModule, + browserStorage, + logger, + customAuthProxyDomain + ); + expect(customAuthAuthority.getPreferredCache()).toBe( + "spasamples.ciamlogin.com" + ); + }); + }); + + describe("tokenEndpoint", () => { + it("should return the correct token endpoint", () => { + const customAuthAuthority = new CustomAuthAuthority( + authorityUrl ?? "", + config, + StubbedNetworkModule, + browserStorage, + logger, + customAuthProxyDomain + ); + expect(customAuthAuthority.tokenEndpoint).toBe( + "https://myspafunctiont1.azurewebsites.net/api/ReverseProxy/oauth2/v2.0/token" + ); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.spec.ts b/lib/msal-browser/test/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.spec.ts new file mode 100644 index 0000000000..8bcc6da6d4 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.spec.ts @@ -0,0 +1,28 @@ +import { CustomAuthApiClient } from "../../../../../src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.js"; +import { FetchHttpClient } from "../../../../../src/custom_auth/core/network_client/http_client/FetchHttpClient.js"; +import { getDefaultLogger } from "../../../test_resources/TestModules.js"; + +describe("CustomAuthApiClient", () => { + let customAuthApiClient: CustomAuthApiClient; + + beforeEach(() => { + const logger = getDefaultLogger(); + customAuthApiClient = new CustomAuthApiClient( + "https://test.com", + "client_id", + new FetchHttpClient(logger) + ); + }); + + it("should initialize signInApiClient correctly", () => { + expect(customAuthApiClient.signInApi).toBeDefined(); + }); + + it("should initialize signUpApiClient correctly", () => { + expect(customAuthApiClient.signUpApi).toBeDefined(); + }); + + it("should initialize resetPasswordApiClient correctly", () => { + expect(customAuthApiClient.resetPasswordApi).toBeDefined(); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/core/network_client/http_client/FetchClient.spec.ts b/lib/msal-browser/test/custom_auth/core/network_client/http_client/FetchClient.spec.ts new file mode 100644 index 0000000000..696714f557 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/core/network_client/http_client/FetchClient.spec.ts @@ -0,0 +1,59 @@ +import { FetchHttpClient } from "../../../../../src/custom_auth/core/network_client/http_client/FetchHttpClient.js"; +import { getDefaultLogger } from "../../../test_resources/TestModules.js"; + +class MockResponse { + public readonly status: number; + public readonly headers: Headers; + private readonly body: any; + + constructor(body: any, init: ResponseInit = {}) { + this.status = init.status || 200; + this.headers = new Headers(init.headers); + this.body = body; + } + + async json() { + return JSON.parse(this.body); + } +} + +describe("FetchHttpClient", () => { + let httpClient: FetchHttpClient; + let mockFetch: jest.Mock; + const logger = getDefaultLogger(); + + beforeEach(() => { + // Create a mock for the global fetch + mockFetch = jest.fn(); + global.fetch = mockFetch; + httpClient = new FetchHttpClient(logger); + }); + + afterEach(() => { + jest.resetAllMocks(); + }); + + describe("sendAsync", () => { + it("should call fetch with correct parameters", async () => { + const url = "https://api.example.com"; + const options: RequestInit = { + method: "GET", + headers: { "Content-Type": "application/json" }, + }; + const mockResponse = new MockResponse(null, { status: 200 }); + mockFetch.mockResolvedValue(mockResponse); + const response = await httpClient.sendAsync(url, options); + expect(mockFetch).toHaveBeenCalledWith(url, options); + expect(response).toBe(mockResponse); + }); + + it("should propagate fetch errors", async () => { + const url = "https://api.example.com"; + const error = new Error("Network error"); + mockFetch.mockRejectedValue(error); + await expect(httpClient.sendAsync(url, {})).rejects.toThrow( + "Network error" + ); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/core/utils/ArgumentValidator.spec.ts b/lib/msal-browser/test/custom_auth/core/utils/ArgumentValidator.spec.ts new file mode 100644 index 0000000000..d4a0c2529f --- /dev/null +++ b/lib/msal-browser/test/custom_auth/core/utils/ArgumentValidator.spec.ts @@ -0,0 +1,85 @@ +import { InvalidArgumentError } from "../../../../src/custom_auth/core/error/InvalidArgumentError.js"; +import { + ensureArgumentIsNotEmptyString, + ensureArgumentIsNotNullOrUndefined, +} from "../../../../src/custom_auth/core/utils/ArgumentValidator.js"; + +describe("ArgumentValidator", () => { + describe("ensureArgumentIsNotEmptyString", () => { + it("should not throw an error if the string is non-empty", () => { + expect(() => { + ensureArgumentIsNotEmptyString("testArg", "validString"); + }).not.toThrow(); + }); + + it("should throw InvalidArgumentError if the string is empty", () => { + expect(() => { + ensureArgumentIsNotEmptyString("testArg", ""); + }).toThrow(InvalidArgumentError); + }); + + it("should throw InvalidArgumentError if the string is only whitespace", () => { + expect(() => { + ensureArgumentIsNotEmptyString("testArg", " "); + }).toThrow(InvalidArgumentError); + }); + + it("should pass correlationId to the error when the string is invalid", () => { + const correlationId = "12345"; + try { + ensureArgumentIsNotEmptyString("testArg", "", correlationId); + } catch (error) { + if (error instanceof InvalidArgumentError) { + expect(error.correlationId).toBe(correlationId); + } else { + throw error; + } + } + }); + }); + + describe("ensureArgumentIsNotNullOrUndefined", () => { + it("should not throw an error if the argument is not null or undefined", () => { + expect(() => { + ensureArgumentIsNotNullOrUndefined("testArg", "validValue"); + }).not.toThrow(); + + expect(() => { + ensureArgumentIsNotNullOrUndefined("testArg", 42); + }).not.toThrow(); + + expect(() => { + ensureArgumentIsNotNullOrUndefined("testArg", {}); + }).not.toThrow(); + }); + + it("should throw InvalidArgumentError if the argument is null", () => { + expect(() => { + ensureArgumentIsNotNullOrUndefined("testArg", null); + }).toThrow(InvalidArgumentError); + }); + + it("should throw InvalidArgumentError if the argument is undefined", () => { + expect(() => { + ensureArgumentIsNotNullOrUndefined("testArg", undefined); + }).toThrow(InvalidArgumentError); + }); + + it("should pass correlationId to the error when the argument is invalid", () => { + const correlationId = "12345"; + try { + ensureArgumentIsNotNullOrUndefined( + "testArg", + null, + correlationId + ); + } catch (error) { + if (error instanceof InvalidArgumentError) { + expect(error.correlationId).toBe(correlationId); + } else { + throw error; + } + } + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/core/utils/UrlUtils.spec.ts b/lib/msal-browser/test/custom_auth/core/utils/UrlUtils.spec.ts new file mode 100644 index 0000000000..abf1d657fa --- /dev/null +++ b/lib/msal-browser/test/custom_auth/core/utils/UrlUtils.spec.ts @@ -0,0 +1,73 @@ +import { ParsedUrlError } from "../../../../src/custom_auth/core/error/ParsedUrlError.js"; +import { + buildUrl, + parseUrl, +} from "../../../../src/custom_auth/core/utils/UrlUtils.js"; + +describe("UrlUtils", () => { + describe("parseUrl", () => { + it("should return a valid URL object for a correct URL", () => { + const url = "https://example.com"; + const result = parseUrl(url); + expect(result).toBeInstanceOf(URL); + expect(result.origin).toBe(url); + }); + + it("should throw ParsedUrlError for an invalid URL", () => { + const url = "invalid-url"; + expect(() => parseUrl(url)).toThrow( + new ParsedUrlError( + "invalid_url", + `The URL "${url}" is invalid: TypeError: Invalid URL: invalid-url` + ) + ); + }); + }); + + describe("buildUrl", () => { + test.each([ + [ + "baseUrl does not end with a slash and path does not start with a slash", + "https://example.com", + "path/to/resource", + "https://example.com/path/to/resource", + ], + [ + "baseUrl ends with a slash and path does not start with a slash", + "https://example.com/", + "path/to/resource", + "https://example.com/path/to/resource", + ], + [ + "baseUrl does not end with a slash and path starts with a slash", + "https://example.com", + "/path/to/resource", + "https://example.com/path/to/resource", + ], + [ + "baseUrl ends with a slash and path starts with a slash", + "https://example.com/", + "/path/to/resource", + "https://example.com/path/to/resource", + ], + [ + "URL with query parameters", + "https://example.com", + "path?query=1", + "https://example.com/path?query=1", + ], + [ + "baseUrl contains a subpath", + "https://example.com/sub", + "path/to/resource", + "https://example.com/sub/path/to/resource", + ], + ])( + "should correctly construct a URL when %s", + (name, baseUrl, path, expected) => { + const result = buildUrl(baseUrl, path); + expect(result.toString()).toBe(expected); + } + ); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/get_account/auth_flow/CustomAuthAccountData.spec.ts b/lib/msal-browser/test/custom_auth/get_account/auth_flow/CustomAuthAccountData.spec.ts new file mode 100644 index 0000000000..8c76269a3b --- /dev/null +++ b/lib/msal-browser/test/custom_auth/get_account/auth_flow/CustomAuthAccountData.spec.ts @@ -0,0 +1,267 @@ +import { CustomAuthBrowserConfiguration } from "../../../../src/custom_auth/configuration/CustomAuthConfiguration.js"; +import { CustomAuthSilentCacheClient } from "../../../../src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { CustomAuthAccountData } from "../../../../src/custom_auth/get_account/auth_flow/CustomAuthAccountData.js"; +import { SignOutResult } from "../../../../src/custom_auth/get_account/auth_flow/result/SignOutResult.js"; +import { SignOutError } from "../../../../src/custom_auth/get_account/auth_flow/error_type/GetAccountError.js"; +import { MsalCustomAuthError } from "../../../../src/custom_auth/core/error/MsalCustomAuthError.js"; +import { + AccountInfo, + IdTokenClaims, + InteractionRequiredAuthError, + InteractionRequiredAuthErrorCodes, + Logger, +} from "@azure/msal-common/browser"; +import { AuthenticationResult } from "../../../../src/response/AuthenticationResult.js"; + +describe("CustomAuthAccountData", () => { + let mockAccount: AccountInfo; + let mockConfig: CustomAuthBrowserConfiguration; + let mockCacheClient: CustomAuthSilentCacheClient; + let mockLogger: Logger; + const correlationId = "test-correlation-id"; + let mockAuthenticationResult: AuthenticationResult; + + beforeEach(() => { + mockAccount = { + homeAccountId: "test-home-account-id", + name: "Test User", + username: "test.user@example.com", + environment: "test-environment", + localAccountId: "test-local-account-id", + tenantId: "test-tenant-id", + idToken: "test-id-token", + idTokenClaims: { + name: "Test User", + }, + }; + + mockAuthenticationResult = { + authority: "test-authority", + uniqueId: "test-unique-id", + tenantId: "test-tenant-id", + scopes: ["test-scope"], + account: mockAccount, + idToken: "test-id-token", + idTokenClaims: mockAccount.idTokenClaims as IdTokenClaims, + accessToken: "test-access-token", + fromCache: true, + expiresOn: new Date(), + tokenType: "Bearer", + correlationId: correlationId, + } as AuthenticationResult; + + mockConfig = { + auth: { + authority: "test-authority", + }, + } as CustomAuthBrowserConfiguration; // Mock as needed + mockCacheClient = { + acquireToken: jest.fn(), + getCurrentAccount: jest.fn(), + logout: jest.fn(), + } as unknown as CustomAuthSilentCacheClient; + mockLogger = { + info: jest.fn(), + verbose: jest.fn(), + error: jest.fn(), + errorPii: jest.fn(), + } as unknown as Logger; + }); + + afterEach(() => { + jest.clearAllMocks(); // Clear mocks between tests + }); + + describe("signOut", () => { + it("should sign out the user successfully", async () => { + (mockCacheClient.getCurrentAccount as jest.Mock).mockReturnValue( + mockAccount + ); + + const accountData = new CustomAuthAccountData( + mockAccount, + mockConfig, + mockCacheClient, + mockLogger, + correlationId + ); + const result = await accountData.signOut(); + + expect(mockCacheClient.logout).toHaveBeenCalledWith({ + correlationId: correlationId, + account: mockAccount, + }); + expect(result).toBeInstanceOf(SignOutResult); + expect(mockLogger.verbose).toHaveBeenCalledWith( + "Signing out user", + "test-correlation-id" + ); + expect(mockLogger.verbose).toHaveBeenCalledWith( + "User signed out", + "test-correlation-id" + ); + }); + + it("should handle errors during sign out", async () => { + const error = new Error("Sign out error"); + (mockCacheClient.getCurrentAccount as jest.Mock).mockReturnValue( + mockAccount + ); + (mockCacheClient.logout as jest.Mock).mockRejectedValue(error); + + const accountData = new CustomAuthAccountData( + mockAccount, + mockConfig, + mockCacheClient, + mockLogger, + correlationId + ); + const result = await accountData.signOut(); + + expect(mockLogger.errorPii).toHaveBeenCalledWith( + `An error occurred during sign out: ${error}`, + "test-correlation-id" + ); + expect(result).toBeInstanceOf(SignOutResult); + expect(result.error).toBeDefined(); + }); + + it("should handle no cached account", async () => { + (mockCacheClient.getCurrentAccount as jest.Mock).mockReturnValue( + null + ); + const accountData = new CustomAuthAccountData( + mockAccount, + mockConfig, + mockCacheClient, + mockLogger, + correlationId + ); + const result = await accountData.signOut(); + expect(result).toBeInstanceOf(SignOutResult); + expect(result.error).toBeInstanceOf(SignOutError); + expect(result.error?.isUserNotSignedIn()).toBe(true); + }); + }); + + describe("getAccount", () => { + it("should return the account information", () => { + const accountData = new CustomAuthAccountData( + mockAccount, + mockConfig, + mockCacheClient, + mockLogger, + correlationId + ); + const account = accountData.getAccount(); + expect(account).toEqual(mockAccount); + }); + }); + + describe("getIdToken", () => { + it("should return the id token", () => { + const accountData = new CustomAuthAccountData( + mockAccount, + mockConfig, + mockCacheClient, + mockLogger, + correlationId + ); + const idToken = accountData.getIdToken(); + expect(idToken).toEqual(mockAccount.idToken); + }); + }); + + describe("getClaims", () => { + it("should return the token claims", () => { + const accountData = new CustomAuthAccountData( + mockAccount, + mockConfig, + mockCacheClient, + mockLogger, + correlationId + ); + const claims = accountData.getClaims(); + expect(claims).toEqual(mockAccount.idTokenClaims); + }); + }); + + describe("getAccessToken", () => { + it("should return succeed GetAccessTokenState.Completed with cached tokens", async () => { + (mockCacheClient.getCurrentAccount as jest.Mock).mockReturnValue( + mockAccount + ); + jest.spyOn( + CustomAuthAccountData.prototype as any, + "createCommonSilentFlowRequest" + ).mockReturnValue({}); + (mockCacheClient.acquireToken as jest.Mock).mockResolvedValue( + mockAuthenticationResult + ); + const accountData = new CustomAuthAccountData( + mockAccount, + mockConfig, + mockCacheClient, + mockLogger, + correlationId + ); + + const response = await accountData.getAccessToken({ + forceRefresh: false, + }); + + expect(response).toBeDefined(); + expect(response.isCompleted()).toBe(true); + expect(response.data?.account).toEqual(mockAccount); + expect(response.data?.idToken).toEqual( + mockAuthenticationResult.idToken + ); + }); + + it("should return GetAccessTokenError if there is an error when aquire tokens", async () => { + (mockCacheClient.getCurrentAccount as jest.Mock).mockReturnValue( + mockAccount + ); + const errorCode = + InteractionRequiredAuthErrorCodes.refreshTokenExpired; + const errorMessage = "Refresh token has expired."; + const subError = + "Refresh token has expired, can not use it to get a new access token."; + const mockRefreshTokenExpiredError = + new InteractionRequiredAuthError( + errorCode, + errorMessage, + subError + ); + (mockCacheClient.acquireToken as jest.Mock).mockRejectedValue( + mockRefreshTokenExpiredError + ); + + const accountData = new CustomAuthAccountData( + mockAccount, + mockConfig, + mockCacheClient, + mockLogger, + correlationId + ); + + const response = await accountData.getAccessToken({ + forceRefresh: false, + }); + + expect(response).toBeDefined(); + expect(response.isFailed()).toBe(true); + expect(response.error?.errorData).toEqual( + mockRefreshTokenExpiredError + ); + expect(response.error?.errorData).toBeInstanceOf( + MsalCustomAuthError + ); + + const msalError = response.error?.errorData as MsalCustomAuthError; + expect(msalError.error).toEqual(errorCode); + expect(msalError.errorDescription).toEqual(errorMessage); + expect(msalError.subError).toEqual(subError); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/get_account/auth_flow/error_type/GetAccountError.spec.ts b/lib/msal-browser/test/custom_auth/get_account/auth_flow/error_type/GetAccountError.spec.ts new file mode 100644 index 0000000000..0e2b376333 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/get_account/auth_flow/error_type/GetAccountError.spec.ts @@ -0,0 +1,34 @@ +import { NoCachedAccountFoundError } from "../../../../../src/custom_auth/core/error/NoCachedAccountFoundError.js"; +import { + GetAccountError, + SignOutError, +} from "../../../../../src/custom_auth/get_account/auth_flow/error_type/GetAccountError.js"; +import { UnexpectedError } from "../../../../../src/custom_auth/index.js"; + +describe("GetAccountError", () => { + it("should return true for isCurrentAccountNotFound when error is NoSignedInAccountFound", () => { + const error = new GetAccountError(new NoCachedAccountFoundError()); + expect(error.isCurrentAccountNotFound()).toBe(true); + }); + + it("should return false for isCurrentAccountNotFound when error is not NoSignedInAccountFound", () => { + const error = new GetAccountError( + new UnexpectedError("unknown_error", "Unknown error") + ); + expect(error.isCurrentAccountNotFound()).toBe(false); + }); +}); + +describe("SignOutError", () => { + it("should return true for isUserNotSignedIn when error is NoCachedAccountFoundError", () => { + const error = new SignOutError(new NoCachedAccountFoundError()); + expect(error.isUserNotSignedIn()).toBe(true); + }); + + it("should return false for isUserNotSignedIn when error is not NoCachedAccountFoundError", () => { + const error = new SignOutError( + new UnexpectedError("unknown_error", "Unknown error") + ); + expect(error.isUserNotSignedIn()).toBe(false); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.spec.ts b/lib/msal-browser/test/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.spec.ts new file mode 100644 index 0000000000..2b9acc5e3c --- /dev/null +++ b/lib/msal-browser/test/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.spec.ts @@ -0,0 +1,448 @@ +import { CustomAuthSilentCacheClient } from "../../../../src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { customAuthConfig } from "../../test_resources/CustomAuthConfig.js"; +import { CustomAuthAuthority } from "../../../../src/custom_auth/core/CustomAuthAuthority.js"; +import { + AccessTokenEntity, + AccountEntity, + AuthenticationScheme, + CacheHelpers, + CommonSilentFlowRequest, + createInteractionRequiredAuthError, + ICrypto, + INetworkModule, + InteractionRequiredAuthErrorCodes, + Logger, + RefreshTokenEntity, + TimeUtils, +} from "@azure/msal-common/browser"; +import { + TestTokenResponse, + TestAccounDetails, + TestServerTokenResponse, + TestHomeAccountId, + TestTenantId, + RenewedTokens, +} from "../../test_resources/TestConstants.js"; +import { DefaultScopes } from "../../../../src/custom_auth/CustomAuthConstants.js"; +import { BrowserCacheManager } from "../../../../src/cache/BrowserCacheManager.js"; +import { BrowserConfiguration } from "../../../../src/config/Configuration.js"; +import { INavigationClient } from "../../../../src/navigation/INavigationClient.js"; +import { RANDOM_TEST_GUID } from "../../../utils/StringConstants.js"; +import { + getDefaultCrypto, + getDefaultEventHandler, + getDefaultPerformanceClient, +} from "../../test_resources/TestModules.js"; + +describe("CustomAuthSilentCacheClient", () => { + let client: CustomAuthSilentCacheClient; + let mockBrowserConfig: BrowserConfiguration; + let mockCacheManager: BrowserCacheManager; + let mockCrypto: ICrypto; + let mockNetworkModule: INetworkModule; + + const mockNavigationClient = { + navigateExternal: jest.fn(), + } as unknown as jest.Mocked; + + beforeEach(() => { + const serverResponse = { + status: 200, + body: { + token_type: "Bearer", + scope: TestServerTokenResponse.scope, + expires_in: 3600, + ext_expires_in: 3600, + correlation_id: "test-correlation-id", + access_token: RenewedTokens.ACCESS_TOKEN, + refresh_token: RenewedTokens.REFRESH_TOKEN, + id_token: TestTokenResponse.ID_TOKEN, + client_info: TestTokenResponse.CLIENT_INFO, + }, + }; + + mockNetworkModule = { + sendGetRequestAsync: jest.fn(), + sendPostRequestAsync: jest.fn().mockResolvedValue(serverResponse), + } as unknown as jest.Mocked; + + mockBrowserConfig = { + auth: { + clientId: customAuthConfig.auth.clientId, + authority: customAuthConfig.auth.authority, + postLogoutRedirectUri: "http://example.com", + }, + system: { + loggerOptions: { + loggerCallback: jest.fn(), + piiLoggingEnabled: false, + logLevel: 2, + }, + networkClient: mockNetworkModule, + tokenRenewalOffsetSeconds: 300, + }, + cache: { + claimsBasedCachingEnabled: false, + }, + telemetry: {}, + } as unknown as jest.Mocked; + + const mockLogger = { + clone: jest.fn(), + info: jest.fn(), + verbose: jest.fn(), + warning: jest.fn(), + trace: jest.fn(), + tracePii: jest.fn(), + error: jest.fn(), + verbosePii: jest.fn(), + errorPii: jest.fn(), + infoPii: jest.fn(), + } as unknown as jest.Mocked; + + mockLogger.clone.mockReturnValue(mockLogger); + + const mockEventHandler = getDefaultEventHandler(); + const mockPerformanceClient = getDefaultPerformanceClient(); + const mockedApiClient = {} as unknown as jest.Mocked; + mockCrypto = getDefaultCrypto( + customAuthConfig.auth.clientId, + mockLogger, + mockPerformanceClient + ); + + mockCacheManager = new BrowserCacheManager( + customAuthConfig.auth.clientId, + mockBrowserConfig.cache, + mockCrypto, + mockLogger, + mockPerformanceClient, + mockEventHandler + ); + + const authority = new CustomAuthAuthority( + customAuthConfig.auth.authority ?? "", + mockBrowserConfig, + mockNetworkModule, + mockCacheManager, + mockLogger, + customAuthConfig.customAuth.authApiProxyUrl + ); + + client = new CustomAuthSilentCacheClient( + mockBrowserConfig, + mockCacheManager, + mockCrypto, + mockLogger, + mockEventHandler, + mockNavigationClient, + mockPerformanceClient, + mockedApiClient, + authority + ); + }); + + afterEach(() => { + jest.clearAllMocks(); // Clear mocks between tests + }); + + describe("getAccessToken", () => { + let accountEntityToCache: AccountEntity; + let accessTokenEntityToCache: AccessTokenEntity; + let refreshTokenEntityToCache: RefreshTokenEntity; + + const defaultScopes = [...DefaultScopes]; + const commonSilentFlowRequest = { + authority: customAuthConfig.auth.authority, + correlationId: "test-correlation-id", + scopes: defaultScopes, + account: TestAccounDetails, + forceRefresh: false, + storeInCache: { + idToken: true, + accessToken: true, + refreshToken: true, + }, + } as CommonSilentFlowRequest; + + beforeEach(() => { + accountEntityToCache = + AccountEntity.createFromAccountInfo(TestAccounDetails); + accessTokenEntityToCache = createAccessTokenEntity(mockCrypto); + refreshTokenEntityToCache = createRefreshTokenEntity(); + + jest.spyOn(AccountEntity, "generateHomeAccountId").mockReturnValue( + TestHomeAccountId + ); + }); + + afterEach(() => { + mockCacheManager.clear("test-correlation-id"); + }); + + it("should get cached access token successfully and return.", async () => { + await saveTokensIntoCache( + "test-correlation-id", + mockCacheManager, + accountEntityToCache, + accessTokenEntityToCache, + refreshTokenEntityToCache + ); + + const result = await client.acquireToken(commonSilentFlowRequest); + + expect(result).toBeDefined(); + expect(result.accessToken).toBe(accessTokenEntityToCache.secret); + const cachedAccessTokenScopes = + accessTokenEntityToCache.target.split(" "); + expect(result.scopes).toEqual(cachedAccessTokenScopes); + }); + + it("should refresh access token (with valid cached refresh token) when cached access token is invalid.", async () => { + accessTokenEntityToCache.cachedAt = new Date(Date.now() - 1000) + .getTime() + .toString(); + await saveTokensIntoCache( + "test-correlation-id", + mockCacheManager, + accountEntityToCache, + accessTokenEntityToCache, + refreshTokenEntityToCache + ); + + const result = await client.acquireToken(commonSilentFlowRequest); + + expect(result).toBeDefined(); + expect(result.accessToken).toBe(RenewedTokens.ACCESS_TOKEN); + + const refreshTokenKey = mockCacheManager + .getTokenKeys() + .refreshToken.filter((key) => + key.includes(TestHomeAccountId) + )[0]; + const refreshToken = mockCacheManager.getRefreshTokenCredential( + refreshTokenKey, + RANDOM_TEST_GUID + ); + expect(refreshToken?.secret).toEqual("renewed-refresh-token"); + }); + + it("should renew token when no cached access token found (by giving unmatched scopes)", async () => { + // result in error when fetching access token because given scopes should be subset of cached access token scopes + const unmatchedScope = ["Mail.Read"]; + await saveTokensIntoCache( + "test-correlation-id", + mockCacheManager, + accountEntityToCache, + accessTokenEntityToCache, + refreshTokenEntityToCache + ); + + commonSilentFlowRequest.scopes = unmatchedScope; + + const result = await client.acquireToken(commonSilentFlowRequest); + + expect(result).toBeDefined(); + expect(result.accessToken).toBe(RenewedTokens.ACCESS_TOKEN); + + const refreshTokenKey = mockCacheManager + .getTokenKeys() + .refreshToken.filter((key) => + key.includes(TestHomeAccountId) + )[0]; + const refreshToken = mockCacheManager.getRefreshTokenCredential( + refreshTokenKey, + RANDOM_TEST_GUID + ); + expect(refreshToken?.secret).toEqual("renewed-refresh-token"); + }); + + it("should skip cache lookup and refresh access token when refreshForced is true", async () => { + await saveTokensIntoCache( + "test-correlation-id", + mockCacheManager, + accountEntityToCache, + accessTokenEntityToCache, + refreshTokenEntityToCache + ); + + commonSilentFlowRequest.forceRefresh = true; + + const result = await client.acquireToken(commonSilentFlowRequest); + + expect(result).toBeDefined(); + expect(result.accessToken).toBe(RenewedTokens.ACCESS_TOKEN); + + const refreshTokenKey = mockCacheManager + .getTokenKeys() + .refreshToken.filter((key) => + key.includes(TestHomeAccountId) + )[0]; + const refreshToken = mockCacheManager.getRefreshTokenCredential( + refreshTokenKey, + RANDOM_TEST_GUID + ); + expect(refreshToken?.secret).toEqual("renewed-refresh-token"); + }); + + it("should throw error when refresh token is not found", async () => { + await saveTokensIntoCache( + "test-correlation-id", + mockCacheManager, + accountEntityToCache, + accessTokenEntityToCache + ); + + const mockNoTokensFoundError = createInteractionRequiredAuthError( + InteractionRequiredAuthErrorCodes.noTokensFound + ); + + commonSilentFlowRequest.forceRefresh = true; + + expect( + client.acquireToken(commonSilentFlowRequest) + ).rejects.toThrow(mockNoTokensFoundError); + }); + + it("should throw error when refresh token is expired", async () => { + refreshTokenEntityToCache.expiresOn = + TimeUtils.nowSeconds().toString(); + await saveTokensIntoCache( + "test-correlation-id", + mockCacheManager, + accountEntityToCache, + accessTokenEntityToCache, + refreshTokenEntityToCache + ); + + const mockRefreshTokenExpiredError = + createInteractionRequiredAuthError( + InteractionRequiredAuthErrorCodes.refreshTokenExpired + ); + + commonSilentFlowRequest.forceRefresh = true; + + expect( + client.acquireToken(commonSilentFlowRequest) + ).rejects.toThrow(mockRefreshTokenExpiredError); + }); + }); + + describe("getCurrentAccount", () => { + it("should return account from cache", () => { + jest.spyOn(mockCacheManager, "getAllAccounts").mockReturnValue([ + { + homeAccountId: "test-home-account-id", + environment: "test-environment", + tenantId: "test-tenant-id", + username: "test-username", + localAccountId: "test-local-account-id", + }, + { + homeAccountId: "test-home-account-id-2", + environment: "test-environment-2", + tenantId: "test-tenant-id-2", + username: "test-username-2", + localAccountId: "test-local-account-id-2", + }, + ]); + + const account = client.getCurrentAccount("test-corrlation-id"); + + expect(account).toBeDefined(); + expect(account?.homeAccountId).toBe("test-home-account-id"); + expect(account?.tenantId).toBe("test-tenant-id"); + expect(account?.username).toBe("test-username"); + expect(account?.localAccountId).toBe("test-local-account-id"); + expect(account?.environment).toBe("test-environment"); + }); + + it("should return null if no account found", () => { + jest.spyOn(mockCacheManager, "getAllAccounts").mockReturnValue([]); + + const account = client.getCurrentAccount("test-corrlation-id"); + + expect(account).toBe(null); + }); + }); + + describe("logout", () => { + it("should logout successfully", async () => { + jest.spyOn(mockCacheManager, "getActiveAccount").mockReturnValue({ + homeAccountId: "test-home-account-id-2", + environment: "test-environment-2", + tenantId: "test-tenant-id-2", + username: "test-username-2", + localAccountId: "test-local-account-id-2", + }); + + jest.spyOn(mockCacheManager, "removeAccount"); + + await client.logout({ + account: { + homeAccountId: "test-home-account-id", + environment: "test-environment", + tenantId: "test-tenant-id", + username: "test-username", + localAccountId: "test-local-account-id", + }, + }); + + expect(mockCacheManager.removeAccount).toHaveBeenCalled(); + expect(mockNavigationClient.navigateExternal).toHaveBeenCalled(); + }); + }); +}); + +async function saveTokensIntoCache( + correlationId: string, + cacheManager: BrowserCacheManager, + accountEntity?: AccountEntity, + accessTokenEntity?: AccessTokenEntity, + refreshTokenEntity?: RefreshTokenEntity +): Promise { + accountEntity + ? await cacheManager.setAccount(accountEntity, correlationId) + : null; + accessTokenEntity + ? await cacheManager.setAccessTokenCredential( + accessTokenEntity, + correlationId + ) + : null; + refreshTokenEntity + ? await cacheManager.setRefreshTokenCredential( + refreshTokenEntity, + correlationId + ) + : null; +} + +function createAccessTokenEntity(browserCrypto: ICrypto): AccessTokenEntity { + const expiresOn = new Date( + Date.now() + TestServerTokenResponse.expires_in * 1000 + ).getTime(); + + return CacheHelpers.createAccessTokenEntity( + TestHomeAccountId, + TestAccounDetails.environment, + TestTokenResponse.ACCESS_TOKEN, + customAuthConfig.auth.clientId, + TestTenantId, + TestServerTokenResponse.scope, + expiresOn, + expiresOn + 0, + browserCrypto.base64Decode, + undefined, + TestServerTokenResponse.token_type as AuthenticationScheme + ); +} + +function createRefreshTokenEntity(): RefreshTokenEntity { + return CacheHelpers.createRefreshTokenEntity( + TestHomeAccountId, + TestAccounDetails.environment, + TestServerTokenResponse.refresh_token, + customAuthConfig.auth.clientId + ); +} diff --git a/lib/msal-browser/test/custom_auth/integration_tests/GetAccount.spec.ts b/lib/msal-browser/test/custom_auth/integration_tests/GetAccount.spec.ts new file mode 100644 index 0000000000..d25374f7eb --- /dev/null +++ b/lib/msal-browser/test/custom_auth/integration_tests/GetAccount.spec.ts @@ -0,0 +1,181 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthPublicClientApplication } from "../../../src/custom_auth/CustomAuthPublicClientApplication.js"; +import { ICustomAuthPublicClientApplication } from "../../../src/custom_auth/ICustomAuthPublicClientApplication.js"; +import { customAuthConfig } from "../test_resources/CustomAuthConfig.js"; +import { CustomAuthAccountData } from "../../../src/custom_auth/get_account/auth_flow/CustomAuthAccountData.js"; +import { + TestHomeAccountId, + TestTenantId, + TestTokenResponse, + TestUsername, +} from "../test_resources/TestConstants.js"; +import { CustomAuthStandardController } from "../../../src/custom_auth/controller/CustomAuthStandardController.js"; + +describe("GetAccount", () => { + let app: CustomAuthPublicClientApplication; + + beforeEach(async () => { + app = (await CustomAuthPublicClientApplication.create( + customAuthConfig + )) as CustomAuthPublicClientApplication; + + global.fetch = jest.fn(); // Mock the fetch API + }); + + afterEach(() => { + const controller = app[ + "customAuthController" + ] as CustomAuthStandardController; + if ( + controller && + controller["eventHandler"] && + controller["eventHandler"]["broadcastChannel"] + ) { + controller["eventHandler"]["broadcastChannel"].close(); + } + + jest.clearAllMocks(); // Clear mocks between tests + }); + + describe("GetAccount", () => { + it("should return correct account data after the sign-in is successful", async () => { + await signIn(app); + + const accountData = app.getCurrentAccount({ + correlationId: "test-correlation-id", + }); + + expect(accountData).toBeDefined(); + expect(accountData.error).toBeUndefined(); + expect(accountData.isCompleted()).toBe(true); + expect(accountData.data).toBeDefined(); + expect(accountData.data).toBeInstanceOf(CustomAuthAccountData); + expect(accountData.data?.getAccount()).toBeDefined(); + + const accountInfo = accountData.data?.getAccount(); + + expect(accountInfo?.homeAccountId).toStrictEqual(TestHomeAccountId); + expect(accountInfo?.tenantId).toStrictEqual(TestTenantId); + expect(accountInfo?.username).toStrictEqual(TestUsername); + + await accountData.data?.signOut(); + }); + + it("should return error data if the account is not found", async () => { + const accountData = app.getCurrentAccount({ + correlationId: "test-correlation-id", + }); + + expect(accountData).toBeDefined(); + expect(accountData.error).toBeDefined(); + expect(accountData.error?.errorData).toBeDefined(); + expect(accountData.error?.isCurrentAccountNotFound()).toBe(true); + expect(accountData.isFailed()).toBe(true); + expect(accountData.data).toBeUndefined(); + }); + }); + + describe("SignOut", () => { + it("should sign the user out after the sign-in is successful", async () => { + await signIn(app); + + const result = app.getCurrentAccount({ + correlationId: "test-correlation-id", + }); + + const accountData = result.data; + + expect(accountData).toBeDefined(); + + const signOutResult = await accountData?.signOut(); + + expect(signOutResult).toBeDefined(); + expect(signOutResult?.error).toBeUndefined(); + expect(signOutResult?.isCompleted()).toBe(true); + + const accountResultAfterSignOut = app.getCurrentAccount({ + correlationId: "test-correlation-id", + }); + + expect(accountResultAfterSignOut).toBeDefined(); + expect(accountResultAfterSignOut.error).toBeDefined(); + expect( + accountResultAfterSignOut.error?.isCurrentAccountNotFound() + ).toBe(true); + }); + + it("should return error data if try to sign out an user who is not signed in", async () => { + await signIn(app); + + const result = app.getCurrentAccount({ + correlationId: "test-correlation-id", + }); + + await result.data?.signOut(); + + const accountData = result.data; + const signOutResult = await accountData?.signOut(); + + expect(signOutResult).toBeDefined(); + expect(signOutResult?.error).toBeDefined(); + expect(signOutResult?.isFailed()).toBe(true); + expect(signOutResult?.error?.isUserNotSignedIn()).toBe(true); + }); + }); +}); + +async function signIn(app: ICustomAuthPublicClientApplication): Promise { + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-1", + challenge_type: "oob password redirect", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-2", + challenge_type: "password", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + correlation_id: "test-correlation-id", + token_type: "Bearer", + scopes: "test-scope", + expires_in: 3600, + id_token: TestTokenResponse.ID_TOKEN, + access_token: TestTokenResponse.ACCESS_TOKEN, + refresh_token: TestTokenResponse.REFRESH_TOKEN, + client_info: TestTokenResponse.CLIENT_INFO, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const signInInputs = { + username: "abc@test.com", + password: "test-pwd", + correlationId: "test-correlation-id", + }; + + await app.signIn(signInInputs); +} diff --git a/lib/msal-browser/test/custom_auth/integration_tests/ResetPassword.spec.ts b/lib/msal-browser/test/custom_auth/integration_tests/ResetPassword.spec.ts new file mode 100644 index 0000000000..42d13fb37d --- /dev/null +++ b/lib/msal-browser/test/custom_auth/integration_tests/ResetPassword.spec.ts @@ -0,0 +1,247 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthAccountData } from "../../../src/custom_auth/get_account/auth_flow/CustomAuthAccountData.js"; +import { CustomAuthPublicClientApplication } from "../../../src/custom_auth/CustomAuthPublicClientApplication.js"; +import { ResetPasswordStartResult } from "../../../src/custom_auth/reset_password/auth_flow/result/ResetPasswordStartResult.js"; +import { ResetPasswordSubmitCodeResult } from "../../../src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitCodeResult.js"; +import { ResetPasswordSubmitPasswordResult } from "../../../src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitPasswordResult.js"; +import { customAuthConfig } from "../test_resources/CustomAuthConfig.js"; +import { SignInResult } from "../../../src/custom_auth/sign_in/auth_flow/result/SignInResult.js"; +import { CustomAuthStandardController } from "../../../src/custom_auth/controller/CustomAuthStandardController.js"; +import { ResetPasswordCodeRequiredState } from "../../../src/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.js"; +import { ResetPasswordPasswordRequiredState } from "../../../src/custom_auth/reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.js"; +import { ResetPasswordCompletedState } from "../../../src/custom_auth/reset_password/auth_flow/state/ResetPasswordCompletedState.js"; +import { TestServerTokenResponse } from "../test_resources/TestConstants.js"; + +describe("Reset password", () => { + let app: CustomAuthPublicClientApplication; + const correlationId = "test-correlation-id"; + + beforeEach(async () => { + app = (await CustomAuthPublicClientApplication.create( + customAuthConfig + )) as CustomAuthPublicClientApplication; + + global.fetch = jest.fn(); // Mock the fetch API + }); + + afterEach(() => { + const controller = app[ + "customAuthController" + ] as CustomAuthStandardController; + if ( + controller && + controller["eventHandler"] && + controller["eventHandler"]["broadcastChannel"] + ) { + controller["eventHandler"]["broadcastChannel"].close(); + } + + jest.clearAllMocks(); // Clear mocks between tests + }); + + it("should reset password successfully if the new password is valid", async () => { + (fetch as jest.Mock) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-1", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-2", + challenge_type: "oob", + binding_method: "prompt", + challenge_channel: "email", + challenge_target_label: "s****n@o*********m", + code_length: 8, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-3", + expires_in: 600, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-4", + poll_interval: 1, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + status: "in_progress", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + status: "in_progress", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-5", + status: "succeeded", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return TestServerTokenResponse; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const resetPasswordInputs = { + username: "test@test.com", + correlationId: correlationId, + }; + + const startResult = await app.resetPassword(resetPasswordInputs); + + expect(startResult).toBeInstanceOf(ResetPasswordStartResult); + expect(startResult.error).toBeUndefined(); + expect(startResult.isCodeRequired()).toBe(true); + + const submitCodeResult = await ( + startResult.state as ResetPasswordCodeRequiredState + ).submitCode("12345678"); + + expect(submitCodeResult).toBeInstanceOf(ResetPasswordSubmitCodeResult); + expect(submitCodeResult.error).toBeUndefined(); + expect(submitCodeResult.isPasswordRequired()).toBe(true); + + const submitPasswordResult = await ( + submitCodeResult.state as ResetPasswordPasswordRequiredState + ).submitNewPassword("valid-password"); + + expect(submitPasswordResult).toBeInstanceOf( + ResetPasswordSubmitPasswordResult + ); + expect(submitPasswordResult.error).toBeUndefined(); + expect(submitPasswordResult.isCompleted()).toBe(true); + + const signInResult = await ( + submitPasswordResult.state as ResetPasswordCompletedState + ).signIn(); + + expect(signInResult).toBeInstanceOf(SignInResult); + expect(signInResult.error).toBeUndefined(); + expect(signInResult.isCompleted()).toBe(true); + expect(signInResult.data).toBeDefined(); + expect(signInResult.data).toBeInstanceOf(CustomAuthAccountData); + expect(signInResult.data?.getAccount()?.idToken).toStrictEqual( + TestServerTokenResponse.id_token + ); + + // Sign out the user for clean up the state for the other tests. + signInResult.data?.signOut(); + }); + + it("should reset password failed if the redirect challenge returned", async () => { + (fetch as jest.Mock) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-1", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + challenge_type: "redirect", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const resetPasswordInputs = { + username: "test@test.com", + correlationId: correlationId, + }; + + const startResult = await app.resetPassword(resetPasswordInputs); + + expect(startResult).toBeInstanceOf(ResetPasswordStartResult); + expect(startResult.error).toBeDefined(); + expect(startResult.isFailed()).toBe(true); + expect(startResult.error?.isRedirectRequired()).toBe(true); + }); + + it("should reset password failed if the given user is not found", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 400, + json: async () => { + return { + error: "user_not_found", + error_description: + "The user account could not be found. Please check the username and try again.", + error_codes: [1003037], + timestamp: "yyyy-mm-dd 10:15:00Z", + trace_id: "test-trace-id", + correlation_id: correlationId, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: false, + }); + + const resetPasswordInputs = { + username: "test@test.com", + correlationId: correlationId, + }; + + const startResult = await app.resetPassword(resetPasswordInputs); + + expect(startResult).toBeInstanceOf(ResetPasswordStartResult); + expect(startResult.error).toBeDefined(); + expect(startResult.isFailed()).toBe(true); + expect(startResult.error?.isUserNotFound()).toBe(true); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/integration_tests/SignIn.spec.ts b/lib/msal-browser/test/custom_auth/integration_tests/SignIn.spec.ts new file mode 100644 index 0000000000..9c7a8ba587 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/integration_tests/SignIn.spec.ts @@ -0,0 +1,395 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthPublicClientApplication } from "../../../src/custom_auth/CustomAuthPublicClientApplication.js"; +import { SignInResult } from "../../../src/custom_auth/sign_in/auth_flow/result/SignInResult.js"; +import { SignInSubmitCodeResult } from "../../../src/custom_auth/sign_in/auth_flow/result/SignInSubmitCodeResult.js"; +import { SignInSubmitPasswordResult } from "../../../src/custom_auth/sign_in/auth_flow/result/SignInSubmitPasswordResult.js"; +import { customAuthConfig } from "../test_resources/CustomAuthConfig.js"; +import { CustomAuthAccountData } from "../../../src/custom_auth/get_account/auth_flow/CustomAuthAccountData.js"; +import { CustomAuthStandardController } from "../../../src/custom_auth/controller/CustomAuthStandardController.js"; +import { SignInCodeRequiredState } from "../../../src/custom_auth/sign_in/auth_flow/state/SignInCodeRequiredState.js"; +import { SignInPasswordRequiredState } from "../../../src/custom_auth/sign_in/auth_flow/state/SignInPasswordRequiredState.js"; +import { TestServerTokenResponse } from "../test_resources/TestConstants.js"; + +describe("Sign in", () => { + let app: CustomAuthPublicClientApplication; + const correlationId = "test-correlation-id"; + + beforeEach(async () => { + app = (await CustomAuthPublicClientApplication.create( + customAuthConfig + )) as CustomAuthPublicClientApplication; + + global.fetch = jest.fn(); // Mock the fetch API + }); + + afterEach(() => { + const controller = app[ + "customAuthController" + ] as CustomAuthStandardController; + if ( + controller && + controller["eventHandler"] && + controller["eventHandler"]["broadcastChannel"] + ) { + controller["eventHandler"]["broadcastChannel"].close(); + } + + jest.clearAllMocks(); // Clear mocks between tests + }); + + it("should sign in successfully if the challenge type is password and password is provided initially", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-1", + challenge_type: "oob password redirect", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-2", + challenge_type: "password", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return TestServerTokenResponse; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const signInInputs = { + username: "test@test.com", + password: "password", + correlationId: correlationId, + }; + + const result = await app.signIn(signInInputs); + + expect(result).toBeInstanceOf(SignInResult); + expect(result.error).toBeUndefined(); + expect(result.isCompleted()).toBe(true); + expect(result.data).toBeDefined(); + expect(result.data).toBeInstanceOf(CustomAuthAccountData); + + // Sign out the user for clean up the state for the other tests. + result.data?.signOut(); + }); + + it("should sign in successfully if the challenge type is oob", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + correlation_id: correlationId, + continuation_token: "test-continuation-token-1", + challenge_type: "oob password redirect", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + correlation_id: correlationId, + continuation_token: "test-continuation-token-2", + challenge_type: "oob", + code_length: 8, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return TestServerTokenResponse; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const signInInputs = { + username: "test@test.com", + correlationId: correlationId, + }; + + const signInResult = await app.signIn(signInInputs); + + expect(signInResult).toBeInstanceOf(SignInResult); + expect(signInResult.error).toBeUndefined(); + expect(signInResult.isCodeRequired()).toBe(true); + + const state = signInResult.state as SignInCodeRequiredState; + const submitCodeResult = await state.submitCode("12345678"); + + expect(submitCodeResult).toBeDefined(); + expect(submitCodeResult).toBeInstanceOf(SignInSubmitCodeResult); + expect(submitCodeResult.data).toBeInstanceOf(CustomAuthAccountData); + + // Sign out the user for clean up the state for the other tests. + submitCodeResult.data?.signOut(); + }); + + it("should sign in successfully if the challenge type is password", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + correlation_id: correlationId, + continuation_token: "test-continuation-token-1", + challenge_type: "oob password redirect", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + correlation_id: correlationId, + continuation_token: "test-continuation-token-2", + challenge_type: "password", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return TestServerTokenResponse; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const signInInputs = { + username: "test@test.com", + correlationId: correlationId, + }; + + const signInResult = await app.signIn(signInInputs); + + expect(signInResult).toBeInstanceOf(SignInResult); + expect(signInResult.error).toBeUndefined(); + expect(signInResult.isPasswordRequired()).toBe(true); + + const state = signInResult.state as SignInPasswordRequiredState; + + const submitCodeResult = await state.submitPassword("valid-password"); + + expect(submitCodeResult).toBeDefined(); + expect(submitCodeResult).toBeInstanceOf(SignInSubmitPasswordResult); + expect(submitCodeResult.data).toBeInstanceOf(CustomAuthAccountData); + + // Sign out the user for clean up the state for the other tests. + submitCodeResult.data?.signOut(); + }); + + it("should sign in failed with error if the challenge type is redirect", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + correlation_id: correlationId, + continuation_token: "test-continuation-token-1", + challenge_type: "oob password redirect", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + correlation_id: correlationId, + challenge_type: "redirect", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const signInInputs = { + username: "test@test.com", + correlationId: correlationId, + }; + + const signInResult = await app.signIn(signInInputs); + + expect(signInResult).toBeInstanceOf(SignInResult); + expect(signInResult.error).toBeDefined(); + expect(signInResult.isFailed()).toBe(true); + expect(signInResult.error?.isRedirectRequired()).toBe(true); + }); + + it("should sign in failed with error if the given user is not found", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 400, + json: async () => { + return { + error: "user_not_found", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: false, + }); + + const signInInputs = { + username: "test@test.com", + correlationId: correlationId, + }; + + const signInResult = await app.signIn(signInInputs); + + expect(signInResult).toBeInstanceOf(SignInResult); + expect(signInResult.error).toBeDefined(); + expect(signInResult.isFailed()).toBe(true); + expect(signInResult.error?.isUserNotFound()).toBe(true); + }); + + it("should sign in failed if the challenge type is password but given password is incorrect", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + correlation_id: correlationId, + continuation_token: "test-continuation-token-1", + challenge_type: "oob password redirect", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + correlation_id: correlationId, + continuation_token: "test-continuation-token-2", + challenge_type: "password", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 400, + json: async () => { + return { + error: "invalid_grant", + error_description: + "AADSTS901007: Error validating credentials due to invalid username or password.", + error_codes: [50126], + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: false, + }); + + const signInInputs = { + username: "test@test.com", + correlationId: correlationId, + password: "invalid-password", + }; + + const signInResult = await app.signIn(signInInputs); + + expect(signInResult).toBeInstanceOf(SignInResult); + expect(signInResult.error).toBeDefined(); + expect(signInResult.isFailed()).toBe(true); + expect(signInResult.error?.isPasswordIncorrect()).toBe(true); + }); + + it("should sign in failed if the challenge type is oob but given code is incorrect", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + correlation_id: correlationId, + continuation_token: "test-continuation-token-1", + challenge_type: "oob password redirect", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + correlation_id: correlationId, + continuation_token: "test-continuation-token-2", + challenge_type: "oob", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 400, + json: async () => { + return { + error: "invalid_grant", + error_description: + "AADSTS901007: Error validating credentials due to invalid username or password.", + error_codes: [], + suberror: "invalid_oob_value", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: false, + }); + + const signInInputs = { + username: "test@test.com", + correlationId: correlationId, + }; + + const signInResult = await app.signIn(signInInputs); + + expect(signInResult).toBeInstanceOf(SignInResult); + expect(signInResult.error).toBeUndefined(); + expect(signInResult.isCodeRequired()).toBe(true); + + const state = signInResult.state as SignInCodeRequiredState; + + const submitCodeResult = await state.submitCode("invalid-code"); + + expect(submitCodeResult).toBeDefined(); + expect(submitCodeResult).toBeInstanceOf(SignInSubmitCodeResult); + expect(submitCodeResult.error).toBeDefined(); + expect(submitCodeResult.error?.isInvalidCode()).toBe(true); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/integration_tests/SignUp.spec.ts b/lib/msal-browser/test/custom_auth/integration_tests/SignUp.spec.ts new file mode 100644 index 0000000000..465457122b --- /dev/null +++ b/lib/msal-browser/test/custom_auth/integration_tests/SignUp.spec.ts @@ -0,0 +1,648 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CustomAuthAccountData } from "../../../src/custom_auth/get_account/auth_flow/CustomAuthAccountData.js"; +import { CustomAuthPublicClientApplication } from "../../../src/custom_auth/CustomAuthPublicClientApplication.js"; +import { SignUpSubmitCodeResult } from "../../../src/custom_auth/sign_up/auth_flow/result/SignUpSubmitCodeResult.js"; +import { SignUpSubmitPasswordResult } from "../../../src/custom_auth/sign_up/auth_flow/result/SignUpSubmitPasswordResult.js"; +import { customAuthConfig } from "../test_resources/CustomAuthConfig.js"; +import { SignInResult } from "../../../src/custom_auth/sign_in/auth_flow/result/SignInResult.js"; +import { SignUpInputs } from "../../../src/custom_auth/CustomAuthActionInputs.js"; +import { UserAccountAttributes } from "../../../src/custom_auth/UserAccountAttributes.js"; +import { SignUpResult } from "../../../src/custom_auth/sign_up/auth_flow/result/SignUpResult.js"; +import { SignUpSubmitAttributesResult } from "../../../src/custom_auth/sign_up/auth_flow/result/SignUpSubmitAttributesResult.js"; +import { CustomAuthStandardController } from "../../../src/custom_auth/controller/CustomAuthStandardController.js"; +import { SignUpCodeRequiredState } from "../../../src/custom_auth/sign_up/auth_flow/state/SignUpCodeRequiredState.js"; +import { SignUpCompletedState } from "../../../src/custom_auth/sign_up/auth_flow/state/SignUpCompletedState.js"; +import { SignUpPasswordRequiredState } from "../../../src/custom_auth/sign_up/auth_flow/state/SignUpPasswordRequiredState.js"; +import { SignUpAttributesRequiredState } from "../../../src/custom_auth/sign_up/auth_flow/state/SignUpAttributesRequiredState.js"; +import { TestServerTokenResponse } from "../test_resources/TestConstants.js"; + +describe("Sign up", () => { + let app: CustomAuthPublicClientApplication; + const correlationId = "test-correlation-id"; + + beforeEach(async () => { + app = (await CustomAuthPublicClientApplication.create( + customAuthConfig + )) as CustomAuthPublicClientApplication; + + global.fetch = jest.fn(); // Mock the fetch API + }); + + afterEach(() => { + const controller = app[ + "customAuthController" + ] as CustomAuthStandardController; + if ( + controller && + controller["eventHandler"] && + controller["eventHandler"]["broadcastChannel"] + ) { + controller["eventHandler"]["broadcastChannel"].close(); + } + + jest.clearAllMocks(); // Clear mocks between tests + }); + + it("should sign up successfully if no password is provided when starting the password reset", async () => { + (fetch as jest.Mock) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-1", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-2", + challenge_type: "oob", + binding_method: "prompt", + challenge_channel: "email", + challenge_target_label: "s****n@o*********m", + code_length: 8, + interval: 300, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 400, + json: async () => { + return { + continuation_token: "test-continuation-token-4", + error: "credential_required", + error_description: "Credential required.", + error_codes: [55103], + timestamp: "yy-mm-dd 02:37:33Z", + trace_id: "test-trace-id", + correlation_id: correlationId, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: false, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-5", + challenge_type: "password", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-6", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return TestServerTokenResponse; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const attributes: UserAccountAttributes = { + city: "test-city", + }; + + const signUpInputs: SignUpInputs = { + username: "test@test.com", + correlationId: correlationId, + attributes: attributes, + }; + + const startResult = await app.signUp(signUpInputs); + + expect(startResult).toBeInstanceOf(SignUpResult); + expect(startResult.error).toBeUndefined(); + expect(startResult.isCodeRequired()).toBe(true); + + const submitCodeResult = await ( + startResult.state as SignUpCodeRequiredState + ).submitCode("12345678"); + + expect(submitCodeResult).toBeInstanceOf(SignUpSubmitCodeResult); + expect(submitCodeResult.error).toBeUndefined(); + expect(submitCodeResult.isPasswordRequired()).toBe(true); + + const submitPasswordResult = await ( + submitCodeResult.state as SignUpPasswordRequiredState + ).submitPassword("valid-password"); + + expect(submitPasswordResult).toBeInstanceOf(SignUpSubmitPasswordResult); + expect(submitPasswordResult.error).toBeUndefined(); + expect(submitPasswordResult.isCompleted()).toBe(true); + + const signInResult = await ( + submitPasswordResult.state as SignUpCompletedState + ).signIn(); + + expect(signInResult).toBeInstanceOf(SignInResult); + expect(signInResult.error).toBeUndefined(); + expect(signInResult.isCompleted()).toBe(true); + expect(signInResult.data).toBeDefined(); + expect(signInResult.data).toBeInstanceOf(CustomAuthAccountData); + expect(signInResult.data?.getAccount()?.idToken).toStrictEqual( + TestServerTokenResponse.id_token + ); + + // Sign out the user for clean up the state for the other tests. + signInResult.data?.signOut(); + }); + + it("should sign up successfully if attributes are required after starting the password reset", async () => { + (fetch as jest.Mock) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-1", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-2", + challenge_type: "oob", + binding_method: "prompt", + challenge_channel: "email", + challenge_target_label: "s****n@o*********m", + code_length: 8, + interval: 300, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 400, + json: async () => { + return { + error: "attributes_required", + error_description: "User attributes required", + error_codes: [55106], + timestamp: "yy-mm-dd 02:37:33Z", + trace_id: "test-trace-id", + correlation_id: correlationId, + continuation_token: "test-continuation-token-3", + required_attributes: [ + { + name: "displayName", + type: "string", + required: true, + options: { + regex: ".*@.**$", + }, + }, + { + name: "extension_2588abcdwhtfeehjjeeqwertc_age", + type: "string", + required: true, + }, + { + name: "postalCode", + type: "string", + required: true, + options: { + regex: "^[1-9][0-9]*$", + }, + }, + ], + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: false, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-4", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return TestServerTokenResponse; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const attributes: UserAccountAttributes = { + city: "test-city", + }; + + const signUpInputs: SignUpInputs = { + username: "test@test.com", + correlationId: correlationId, + attributes: attributes, + }; + + const startResult = await app.signUp(signUpInputs); + + expect(startResult).toBeInstanceOf(SignUpResult); + expect(startResult.error).toBeUndefined(); + expect(startResult.isCodeRequired()).toBe(true); + + const submitCodeResult = await ( + startResult.state as SignUpCodeRequiredState + ).submitCode("12345678"); + + expect(submitCodeResult).toBeInstanceOf(SignUpSubmitCodeResult); + expect(submitCodeResult.error).toBeUndefined(); + expect(submitCodeResult.isAttributesRequired()).toBe(true); + expect( + ( + submitCodeResult.state as SignUpAttributesRequiredState + )?.getRequiredAttributes().length + ).toBe(3); + + const requiredAttributes: UserAccountAttributes = { + displayName: "test-display-name", + }; + const submitAttributesResult = await ( + submitCodeResult.state as SignUpAttributesRequiredState + ).submitAttributes(requiredAttributes); + + expect(submitAttributesResult).toBeInstanceOf( + SignUpSubmitAttributesResult + ); + expect(submitAttributesResult.error).toBeUndefined(); + expect(submitAttributesResult.isCompleted()).toBe(true); + + const signInResult = await ( + submitAttributesResult.state as SignUpCompletedState + ).signIn(); + + expect(signInResult).toBeInstanceOf(SignInResult); + expect(signInResult.error).toBeUndefined(); + expect(signInResult.isCompleted()).toBe(true); + expect(signInResult.data).toBeDefined(); + expect(signInResult.data).toBeInstanceOf(CustomAuthAccountData); + expect(signInResult.data?.getAccount()?.idToken).toStrictEqual( + TestServerTokenResponse.id_token + ); + + // Sign out the user for clean up the state for the other tests. + signInResult.data?.signOut(); + }); + + it("should sign up successfully if password and attributes are required after starting the password reset", async () => { + (fetch as jest.Mock) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-1", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-2", + challenge_type: "oob", + binding_method: "prompt", + challenge_channel: "email", + challenge_target_label: "s****n@o*********m", + code_length: 8, + interval: 300, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 400, + json: async () => { + return { + continuation_token: "test-continuation-token-3", + error: "credential_required", + error_description: "Credential required.", + error_codes: [55103], + timestamp: "yy-mm-dd 02:37:33Z", + trace_id: "test-trace-id", + correlation_id: correlationId, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: false, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-4", + challenge_type: "password", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 400, + json: async () => { + return { + error: "attributes_required", + error_description: "User attributes required", + error_codes: [55106], + timestamp: "yy-mm-dd 02:37:33Z", + trace_id: "test-trace-id", + correlation_id: correlationId, + continuation_token: "test-continuation-token-5", + required_attributes: [ + { + name: "displayName", + type: "string", + required: true, + options: { + regex: ".*@.**$", + }, + }, + { + name: "extension_2588abcdwhtfeehjjeeqwertc_age", + type: "string", + required: true, + }, + { + name: "postalCode", + type: "string", + required: true, + options: { + regex: "^[1-9][0-9]*$", + }, + }, + ], + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: false, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-6", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return TestServerTokenResponse; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const attributes: UserAccountAttributes = { + city: "test-city", + }; + + const signUpInputs: SignUpInputs = { + username: "test@test.com", + correlationId: correlationId, + attributes: attributes, + }; + + const startResult = await app.signUp(signUpInputs); + + expect(startResult).toBeInstanceOf(SignUpResult); + expect(startResult.error).toBeUndefined(); + expect(startResult.isCodeRequired()).toBe(true); + + const submitCodeResult = await ( + startResult.state as SignUpCodeRequiredState + ).submitCode("12345678"); + + expect(submitCodeResult).toBeInstanceOf(SignUpSubmitCodeResult); + expect(submitCodeResult.error).toBeUndefined(); + expect(submitCodeResult.isPasswordRequired()).toBe(true); + + const submitPasswordResult = await ( + submitCodeResult.state as SignUpPasswordRequiredState + ).submitPassword("valid-password"); + + expect(submitPasswordResult).toBeInstanceOf(SignUpSubmitPasswordResult); + expect(submitPasswordResult.error).toBeUndefined(); + expect(submitPasswordResult.isAttributesRequired()).toBe(true); + + const requiredAttributes: UserAccountAttributes = { + displayName: "test-display-name", + }; + const submitAttributesResult = await ( + submitPasswordResult.state as SignUpAttributesRequiredState + ).submitAttributes(requiredAttributes); + + expect(submitAttributesResult).toBeInstanceOf( + SignUpSubmitAttributesResult + ); + expect(submitAttributesResult.error).toBeUndefined(); + expect(submitAttributesResult.isCompleted()).toBe(true); + + const signInResult = await ( + submitAttributesResult.state as SignUpCompletedState + ).signIn(); + + expect(signInResult).toBeInstanceOf(SignInResult); + expect(signInResult.error).toBeUndefined(); + expect(signInResult.isCompleted()).toBe(true); + expect(signInResult.data).toBeDefined(); + expect(signInResult.data).toBeInstanceOf(CustomAuthAccountData); + expect(signInResult.data?.getAccount()?.idToken).toStrictEqual( + TestServerTokenResponse.id_token + ); + + // Sign out the user for clean up the state for the other tests. + signInResult.data?.signOut(); + }); + + it("should sign up successfully if the password and attributes are provided when starting the password reset", async () => { + (fetch as jest.Mock) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-1", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-2", + challenge_type: "oob", + binding_method: "prompt", + challenge_channel: "email", + challenge_target_label: "s****n@o*********m", + code_length: 8, + interval: 300, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-3", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return TestServerTokenResponse; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const attributes: UserAccountAttributes = { + city: "test-city", + }; + + const signUpInputs: SignUpInputs = { + username: "test@test.com", + correlationId: correlationId, + password: "valid-password", + attributes: attributes, + }; + + const startResult = await app.signUp(signUpInputs); + + expect(startResult).toBeInstanceOf(SignUpResult); + expect(startResult.error).toBeUndefined(); + expect(startResult.isCodeRequired()).toBe(true); + + const submitCodeResult = await ( + startResult.state as SignUpCodeRequiredState + ).submitCode("12345678"); + + expect(submitCodeResult).toBeInstanceOf(SignUpSubmitCodeResult); + expect(submitCodeResult.error).toBeUndefined(); + expect(submitCodeResult.isCompleted()).toBe(true); + + const signInResult = await ( + submitCodeResult.state as SignUpCompletedState + ).signIn(); + + expect(signInResult).toBeInstanceOf(SignInResult); + expect(signInResult.error).toBeUndefined(); + expect(signInResult.isCompleted()).toBe(true); + expect(signInResult.data).toBeDefined(); + expect(signInResult.data).toBeInstanceOf(CustomAuthAccountData); + expect(signInResult.data?.getAccount()?.idToken).toStrictEqual( + TestServerTokenResponse.id_token + ); + + // Sign out the user for clean up the state for the other tests. + signInResult.data?.signOut(); + }); + + it("should sign up failed if the redirect challenge returned", async () => { + (fetch as jest.Mock) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + continuation_token: "test-continuation-token-1", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }) + .mockResolvedValueOnce({ + status: 200, + json: async () => { + return { + challenge_type: "redirect", + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: true, + }); + + const signUpInputs = { + username: "test@test.com", + correlationId: correlationId, + }; + + const startResult = await app.signUp(signUpInputs); + + expect(startResult).toBeInstanceOf(SignUpResult); + expect(startResult.error).toBeDefined(); + expect(startResult.isFailed()).toBe(true); + expect(startResult.error?.isRedirectRequired()).toBe(true); + }); + + it("should sign up failed if the given user is not found", async () => { + (fetch as jest.Mock).mockResolvedValueOnce({ + status: 400, + json: async () => { + return { + error: "user_already_exists", + error_description: + "It looks like you may already have an account.", + error_codes: [1003037], + timestamp: "yyyy-mm-dd 10:15:00Z", + trace_id: "test-trace-id", + correlation_id: correlationId, + }; + }, + headers: new Headers({ "content-type": "application/json" }), + ok: false, + }); + + const signUpInputs = { + username: "test@test.com", + correlationId: correlationId, + }; + + const startResult = await app.signUp(signUpInputs); + + expect(startResult).toBeInstanceOf(SignUpResult); + expect(startResult.error).toBeDefined(); + expect(startResult.isFailed()).toBe(true); + expect(startResult.error?.isUserAlreadyExists()).toBe(true); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.spec.ts b/lib/msal-browser/test/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.spec.ts new file mode 100644 index 0000000000..8219e21a90 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.spec.ts @@ -0,0 +1,133 @@ +import { + CustomAuthApiError, + RedirectError, +} from "../../../../../src/custom_auth/core/error/CustomAuthApiError.js"; +import * as CustomAuthApiErrorCode from "../../../../../src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorCodes.js"; +import * as CustomAuthApiSuberror from "../../../../../src/custom_auth/core/network_client/custom_auth_api/types/ApiSuberrors.js"; +import { InvalidArgumentError } from "../../../../../src/custom_auth/index.js"; +import { + ResetPasswordError, + ResetPasswordResendCodeError, + ResetPasswordSubmitCodeError, + ResetPasswordSubmitPasswordError, +} from "../../../../../src/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.js"; + +describe("ResetPasswordError", () => { + it("should correctly identify user not found error", () => { + const error = new CustomAuthApiError( + "user_not_found", + "User not found" + ); + const resetPasswordError = new ResetPasswordError(error); + expect(resetPasswordError.isUserNotFound()).toBe(true); + }); + + it("should correctly identify invalid username error", () => { + const error = new InvalidArgumentError("Invalid username"); + const resetPasswordError = new ResetPasswordError(error); + expect(resetPasswordError.isInvalidUsername()).toBe(true); + + const error2 = new CustomAuthApiError( + "Some Error", + "username parameter is empty or not valid", + undefined, + [90100] + ); + const resetPasswordError2 = new ResetPasswordError(error2); + expect(resetPasswordError2.isInvalidUsername()).toBe(true); + }); + + it("should correctly identify unsupported challenge type error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_REQUEST, + "The challenge_type list parameter contains an unsupported challenge type" + ); + const resetPasswordError = new ResetPasswordError(error); + expect(resetPasswordError.isUnsupportedChallengeType()).toBe(true); + + const error2 = new CustomAuthApiError( + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + "Unsupported challenge type" + ); + const resetPasswordError2 = new ResetPasswordError(error2); + expect(resetPasswordError2.isUnsupportedChallengeType()).toBe(true); + }); + + it("should correctly identify redirect error", () => { + const error = new RedirectError("Redirecting..."); + const resetPasswordError = new ResetPasswordError(error); + expect(resetPasswordError.isRedirectRequired()).toBe(true); + }); +}); + +describe("ResetPasswordSubmitPasswordError", () => { + it("should correctly identify invalid password error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_GRANT, + "Invalid password", + undefined, + undefined, + CustomAuthApiSuberror.PASSWORD_IS_INVALID + ); + const resetPasswordError = new ResetPasswordSubmitPasswordError(error); + expect(resetPasswordError.isInvalidPassword()).toBe(true); + + const error2 = new InvalidArgumentError("password is required"); + const resetPasswordError2 = new ResetPasswordSubmitPasswordError( + error2 + ); + expect(resetPasswordError2.isInvalidPassword()).toBe(true); + }); + + it("should correctly identify password reset failed error", () => { + const error1 = new CustomAuthApiError( + "password_reset_timeout", + "Password reset timeout" + ); + const resetPasswordError1 = new ResetPasswordSubmitPasswordError( + error1 + ); + expect(resetPasswordError1.isPasswordResetFailed()).toBe(true); + + const error2 = new CustomAuthApiError( + "password_change_failed", + "Password reset is failed" + ); + const resetPasswordError2 = new ResetPasswordSubmitPasswordError( + error2 + ); + expect(resetPasswordError2.isPasswordResetFailed()).toBe(true); + }); +}); + +describe("ResetPasswordSubmitCodeError", () => { + it("should correctly identify invalid code error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_GRANT, + "Invalid code", + undefined, + undefined, + CustomAuthApiSuberror.INVALID_OOB_VALUE + ); + const resetPasswordError = new ResetPasswordSubmitCodeError(error); + expect(resetPasswordError.isInvalidCode()).toBe(true); + + const error2 = new InvalidArgumentError("Invalid code"); + const resetPasswordError2 = new ResetPasswordSubmitCodeError(error2); + expect(resetPasswordError2.isInvalidCode()).toBe(true); + }); + + it("should correctly identify redirect error", () => { + const error = new RedirectError("Redirecting..."); + const resetPasswordError = new ResetPasswordSubmitCodeError(error); + expect(resetPasswordError.isRedirectRequired()).toBe(true); + }); +}); + +describe("ResetPasswordResendCodeError", () => { + it("should correctly identify redirect error", () => { + const error = new RedirectError("Redirecting..."); + const resetPasswordError = new ResetPasswordResendCodeError(error); + expect(resetPasswordError.isRedirectRequired()).toBe(true); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.spec.ts b/lib/msal-browser/test/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.spec.ts new file mode 100644 index 0000000000..36e30279eb --- /dev/null +++ b/lib/msal-browser/test/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.spec.ts @@ -0,0 +1,126 @@ +import { CustomAuthBrowserConfiguration } from "../../../../../src/custom_auth/configuration/CustomAuthConfiguration.js"; +import { InvalidArgumentError } from "../../../../../src/custom_auth/core/error/InvalidArgumentError.js"; +import { ResetPasswordSubmitCodeError } from "../../../../../src/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.js"; +import { ResetPasswordResendCodeResult } from "../../../../../src/custom_auth/reset_password/auth_flow/result/ResetPasswordResendCodeResult.js"; +import { ResetPasswordSubmitCodeResult } from "../../../../../src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitCodeResult.js"; +import { ResetPasswordCodeRequiredState } from "../../../../../src/custom_auth/reset_password/auth_flow/state/ResetPasswordCodeRequiredState.js"; +import { ResetPasswordClient } from "../../../../../src/custom_auth/reset_password/interaction_client/ResetPasswordClient.js"; +import { SignInClient } from "../../../../../src/custom_auth/sign_in/interaction_client/SignInClient.js"; +import { CustomAuthSilentCacheClient } from "../../../../../src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { getDefaultLogger } from "../../../test_resources/TestModules.js"; + +describe("ResetPasswordCodeRequiredState", () => { + const clientId = "test-client-id"; + const mockConfig = { + auth: { clientId: clientId }, + customAuth: { challengeTypes: ["code"] }, + } as unknown as jest.Mocked; + + const mockResetPasswordClient = { + submitCode: jest.fn(), + resendCode: jest.fn(), + } as unknown as jest.Mocked; + + const mockSignInClient = {} as unknown as jest.Mocked; + + const username = "testuser"; + const correlationId = "test-correlation-id"; + const continuationToken = "test-continuation-token"; + + let state: ResetPasswordCodeRequiredState; + + beforeEach(() => { + state = new ResetPasswordCodeRequiredState({ + correlationId: correlationId, + logger: getDefaultLogger(), + continuationToken: continuationToken, + config: mockConfig, + resetPasswordClient: mockResetPasswordClient, + signInClient: mockSignInClient, + cacheClient: + {} as unknown as jest.Mocked, + username: username, + codeLength: 8, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("submitCode", () => { + it("should return an error result if code is empty", async () => { + const result = await state.submitCode(""); + + expect(result.isFailed()).toBeTruthy(); + expect(result.error).toBeInstanceOf(ResetPasswordSubmitCodeError); + expect(result.error?.isInvalidCode()).toBe(true); + expect(result.error?.errorData).toBeInstanceOf( + InvalidArgumentError + ); + expect(result.error?.errorData?.errorDescription).toContain("code"); + }); + + it("should successfully submit a code and return password required state", async () => { + mockResetPasswordClient.submitCode.mockResolvedValue({ + correlationId: correlationId, + continuationToken: "continuation-token", + }); + + const result = await state.submitCode("12345678"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(ResetPasswordSubmitCodeResult); + expect(result.isPasswordRequired()).toBe(true); + expect(mockResetPasswordClient.submitCode).toHaveBeenCalledWith({ + clientId: clientId, + correlationId: correlationId, + challengeType: ["code"], + continuationToken: continuationToken, + code: "12345678", + username: username, + }); + }); + + it("should successfully submit a code and return password-required state if password is required", async () => { + mockResetPasswordClient.submitCode.mockResolvedValue({ + correlationId: correlationId, + continuationToken: "new-continuation-token", + }); + + const result = await state.submitCode("12345678"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(ResetPasswordSubmitCodeResult); + expect(result.isPasswordRequired()).toBe(true); + expect(mockResetPasswordClient.submitCode).toHaveBeenCalledWith({ + clientId: clientId, + correlationId: correlationId, + challengeType: ["code"], + continuationToken: continuationToken, + code: "12345678", + username: username, + }); + }); + }); + + describe("resendCode", () => { + it("should successfully resend a code and return a code required state", async () => { + mockResetPasswordClient.resendCode.mockResolvedValue({ + correlationId: correlationId, + continuationToken: "new-continuation-token", + challengeChannel: "code", + challengeTargetLabel: "email", + codeLength: 6, + bindingMethod: "email-otp", + }); + + const result = await state.resendCode(); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(ResetPasswordResendCodeResult); + expect(result.data).toBeUndefined(); + expect(result.isCodeRequired()).toBeTruthy(); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.spec.ts b/lib/msal-browser/test/custom_auth/reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.spec.ts new file mode 100644 index 0000000000..29f2599229 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.spec.ts @@ -0,0 +1,121 @@ +import { CustomAuthBrowserConfiguration } from "../../../../../src/custom_auth/configuration/CustomAuthConfiguration.js"; +import { InvalidArgumentError } from "../../../../../src/custom_auth/core/error/InvalidArgumentError.js"; +import { ResetPasswordSubmitPasswordError } from "../../../../../src/custom_auth/reset_password/auth_flow/error_type/ResetPasswordError.js"; +import { ResetPasswordSubmitPasswordResult } from "../../../../../src/custom_auth/reset_password/auth_flow/result/ResetPasswordSubmitPasswordResult.js"; +import { ResetPasswordClient } from "../../../../../src/custom_auth/reset_password/interaction_client/ResetPasswordClient.js"; +import { SignInClient } from "../../../../../src/custom_auth/sign_in/interaction_client/SignInClient.js"; +import { CustomAuthSilentCacheClient } from "../../../../../src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { ResetPasswordPasswordRequiredState } from "../../../../../src/custom_auth/reset_password/auth_flow/state/ResetPasswordPasswordRequiredState.js"; +import { CustomAuthApiError } from "../../../../../src/custom_auth/core/error/CustomAuthApiError.js"; +import { getDefaultLogger } from "../../../test_resources/TestModules.js"; + +describe("ResetPasswordPasswordRequiredState", () => { + const clientId = "test-client-id"; + const mockConfig = { + auth: { clientId: clientId }, + customAuth: { challengeTypes: ["password"] }, + } as unknown as jest.Mocked; + + const mockResetPasswordClient = { + submitNewPassword: jest.fn(), + } as unknown as jest.Mocked; + + const mockSignInClient = {} as unknown as jest.Mocked; + + const username = "testuser"; + const correlationId = "test-correlation-id"; + const continuationToken = "test-continuation-token"; + + let state: ResetPasswordPasswordRequiredState; + + beforeEach(() => { + state = new ResetPasswordPasswordRequiredState({ + correlationId: correlationId, + logger: getDefaultLogger(), + continuationToken: continuationToken, + config: mockConfig, + resetPasswordClient: mockResetPasswordClient, + signInClient: mockSignInClient, + cacheClient: + {} as unknown as jest.Mocked, + username: username, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("submitPassword", () => { + it("should return an error result if password is empty", async () => { + const result = await state.submitNewPassword(""); + + expect(result.isFailed()).toBeTruthy(); + expect(result.error).toBeInstanceOf( + ResetPasswordSubmitPasswordError + ); + expect(result.error?.isInvalidPassword()).toBe(true); + expect(result.error?.errorData).toBeInstanceOf( + InvalidArgumentError + ); + expect(result.error?.errorData?.errorDescription).toContain( + "password" + ); + }); + + it("should successfully submit a password and return completed state", async () => { + mockResetPasswordClient.submitNewPassword.mockResolvedValue({ + correlationId: correlationId, + continuationToken: "new-continuation-token", + }); + + const result = await state.submitNewPassword("valid-password"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(ResetPasswordSubmitPasswordResult); + expect(result.isCompleted()).toBe(true); + expect( + mockResetPasswordClient.submitNewPassword + ).toHaveBeenCalledWith({ + clientId: clientId, + correlationId: correlationId, + challengeType: ["password"], + continuationToken: continuationToken, + newPassword: "valid-password", + username: username, + }); + }); + + it("should successfully submit a password and return completed state", async () => { + mockResetPasswordClient.submitNewPassword.mockRejectedValue( + new CustomAuthApiError( + "invalid_grant", + "Invalid grant", + correlationId, + [], + "password_too_weak" + ) + ); + + const result = await state.submitNewPassword("valid-password"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(ResetPasswordSubmitPasswordResult); + expect(result.isFailed()).toBe(true); + expect(result.error).toBeInstanceOf( + ResetPasswordSubmitPasswordError + ); + expect(result.error?.isInvalidPassword()).toBe(true); + expect( + mockResetPasswordClient.submitNewPassword + ).toHaveBeenCalledWith({ + clientId: clientId, + correlationId: correlationId, + challengeType: ["password"], + continuationToken: continuationToken, + newPassword: "valid-password", + username: username, + }); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/reset_password/interaction_client/ResetPasswordClient.spec.ts b/lib/msal-browser/test/custom_auth/reset_password/interaction_client/ResetPasswordClient.spec.ts new file mode 100644 index 0000000000..e34e7a5b25 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/reset_password/interaction_client/ResetPasswordClient.spec.ts @@ -0,0 +1,357 @@ +jest.mock("../../../../src/custom_auth/CustomAuthConstants.js", () => ({ + PasswordResetPollingTimeoutInMs: 5000, + ChallengeType: { + PASSWORD: "password", + OOB: "oob", + REDIRECT: "redirect", + }, + ResetPasswordPollStatus: { + IN_PROGRESS: "in_progress", + SUCCEEDED: "succeeded", + FAILED: "failed", + NOT_STARTED: "not_started", + }, +})); + +import { ResetPasswordClient } from "../../../../src/custom_auth/reset_password/interaction_client/ResetPasswordClient.js"; +import { customAuthConfig } from "../../test_resources/CustomAuthConfig.js"; +import { CustomAuthAuthority } from "../../../../src/custom_auth/core/CustomAuthAuthority.js"; +import { ChallengeType } from "../../../../src/custom_auth/CustomAuthConstants.js"; +import * as CustomAuthApiErrorCode from "../../../../src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorCodes.js"; +import { buildConfiguration } from "../../../../src/config/Configuration.js"; +import { StubbedNetworkModule } from "@azure/msal-common/browser"; +import { + getDefaultBrowserCacheManager, + getDefaultCrypto, + getDefaultEventHandler, + getDefaultLogger, + getDefaultNavigationClient, + getDefaultPerformanceClient, +} from "../../test_resources/TestModules.js"; + +jest.mock( + "../../../../src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.js", + () => { + let signInApiClient = { + initiate: jest.fn(), + requestChallenge: jest.fn(), + requestTokensWithPassword: jest.fn(), + requestTokensWithOob: jest.fn(), + signInWithContinuationToken: jest.fn(), + }; + let signUpApiClient = { + start: jest.fn(), + requestChallenge: jest.fn(), + continueWithCode: jest.fn(), + continueWithPassword: jest.fn(), + continueWithAttributes: jest.fn(), + }; + let resetPasswordApiClient = { + start: jest.fn(), + requestChallenge: jest.fn(), + continueWithCode: jest.fn(), + submitNewPassword: jest.fn(), + pollCompletion: jest.fn(), + }; + + const CustomAuthApiClient = jest.fn().mockImplementation(() => ({ + signInApi: signInApiClient, + signUpApi: signUpApiClient, + resetPasswordApi: resetPasswordApiClient, + })); + + const mockedApiClient = new CustomAuthApiClient(); + return { + mockedApiClient, + signInApiClient, + signUpApiClient, + resetPasswordApiClient, + }; + } +); + +describe("ResetPasswordClient", () => { + let client: ResetPasswordClient; + let authority: CustomAuthAuthority; + const { mockedApiClient, resetPasswordApiClient } = jest.requireMock( + "../../../../src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.js" + ); + + beforeEach(() => { + const clientId = customAuthConfig.auth.clientId; + const mockBrowserConfiguration = buildConfiguration( + { auth: { clientId: clientId } }, + false + ); + const mockLogger = getDefaultLogger(); + const mockPerformanceClient = getDefaultPerformanceClient(clientId); + const mockEventHandler = getDefaultEventHandler(); + const mockCrypto = getDefaultCrypto( + clientId, + mockLogger, + mockPerformanceClient + ); + const mockCacheManager = getDefaultBrowserCacheManager( + clientId, + mockLogger, + mockPerformanceClient, + mockEventHandler, + undefined, + mockBrowserConfiguration.cache + ); + + authority = new CustomAuthAuthority( + customAuthConfig.auth.authority ?? "", + mockBrowserConfiguration, + StubbedNetworkModule, + mockCacheManager, + mockLogger, + customAuthConfig.customAuth.authApiProxyUrl + ); + + client = new ResetPasswordClient( + mockBrowserConfiguration, + mockCacheManager, + mockCrypto, + mockLogger, + mockEventHandler, + getDefaultNavigationClient(), + mockPerformanceClient, + mockedApiClient, + authority + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("start", () => { + it("should return ResetPasswordCodeRequiredResult suceesfully", async () => { + resetPasswordApiClient.start.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + resetPasswordApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.OOB, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + code_length: 6, + challenge_channel: "email", + challenge_target_label: "email", + binding_method: "email", + }); + + const result = await client.start({ + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + expect(result.codeLength).toBe(6); + expect(result.challengeChannel).toBe("email"); + expect(result.challengeTargetLabel).toBe("email"); + expect(result.bindingMethod).toBe("email"); + }); + + it("should return ResetPasswordPasswordRequiredResult with error when challenge type is not OOB", async () => { + resetPasswordApiClient.start.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + resetPasswordApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.PASSWORD, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + }); + + await expect( + client.start({ + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }) + ).rejects.toMatchObject({ + error: CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + errorDescription: "Unsupported challenge type 'password'.", + correlationId: "corr123", + }); + }); + }); + + describe("submitCode", () => { + it("should return ResetPasswordPasswordRequiredResult successfully", async () => { + resetPasswordApiClient.continueWithCode.mockResolvedValue({ + continuation_token: "continuation_token_2", + correlation_id: "corr123", + expires_in: 3600, + }); + + const result = await client.submitCode({ + code: "123456", + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + }); + }); + + describe("submitNewPassword", () => { + it("should return ResetPasswordCompletedResult for valid password", async () => { + resetPasswordApiClient.submitNewPassword.mockResolvedValue({ + continuation_token: "continuation_token_2", + poll_interval: 1, + correlation_id: "corr123", + }); + + resetPasswordApiClient.pollCompletion + .mockResolvedValueOnce({ + status: "in-progress", + correlation_id: "corr123", + }) + .mockResolvedValueOnce({ + status: "in-progress", + correlation_id: "corr123", + }) + .mockResolvedValueOnce({ + status: "succeeded", + continuation_token: "continuation_token_3", + correlation_id: "corr123", + }); + + const result = await client.submitNewPassword({ + newPassword: "123456", + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_3"); + expect(resetPasswordApiClient.pollCompletion).toHaveBeenCalledTimes( + 3 + ); + }, 5000); + + it("should return ResetPasswordCompletedResult with error if the password-change is failed", async () => { + resetPasswordApiClient.submitNewPassword.mockResolvedValue({ + continuation_token: "continuation_token_2", + poll_interval: 1, + correlation_id: "corr123", + }); + + resetPasswordApiClient.pollCompletion.mockResolvedValue({ + status: "failed", + correlation_id: "corr123", + }); + + await expect( + client.submitNewPassword({ + newPassword: "123456", + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }) + ).rejects.toMatchObject({ + error: CustomAuthApiErrorCode.PASSWORD_CHANGE_FAILED, + errorDescription: "Password is failed to be reset.", + correlationId: "corr123", + }); + }, 5000); + + it("should return ResetPasswordCompletedResult with error if the reset password is timeout", async () => { + resetPasswordApiClient.submitNewPassword.mockResolvedValue({ + continuation_token: "continuation_token_2", + poll_interval: 1, + correlation_id: "corr123", + }); + + resetPasswordApiClient.pollCompletion.mockResolvedValue({ + status: "in-progress", + correlation_id: "corr123", + }); + + await expect( + client.submitNewPassword({ + newPassword: "123456", + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }) + ).rejects.toMatchObject({ + error: CustomAuthApiErrorCode.PASSWORD_RESET_TIMEOUT, + errorDescription: "Password reset flow has timed out.", + correlationId: "corr123", + }); + }, 10000); + }); + + describe("resendCode", () => { + it("should return ResetPasswordCodeRequiredResult", async () => { + resetPasswordApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.OOB, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + code_length: 6, + challenge_channel: "email", + challenge_target_label: "email", + binding_method: "email", + }); + + const result = await client.resendCode({ + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + expect(result.codeLength).toBe(6); + expect(result.challengeChannel).toBe("email"); + expect(result.challengeTargetLabel).toBe("email"); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/sign_in/auth_flow/error_type/SignInError.spec.ts b/lib/msal-browser/test/custom_auth/sign_in/auth_flow/error_type/SignInError.spec.ts new file mode 100644 index 0000000000..a464fe3d69 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/sign_in/auth_flow/error_type/SignInError.spec.ts @@ -0,0 +1,128 @@ +import { + CustomAuthApiError, + RedirectError, +} from "../../../../../src/custom_auth/core/error/CustomAuthApiError.js"; +import { InvalidArgumentError } from "../../../../../src/custom_auth/index.js"; +import { + SignInError, + SignInSubmitCodeError, + SignInSubmitPasswordError, +} from "../../../../../src/custom_auth/sign_in/auth_flow/error_type/SignInError.js"; + +describe("SignInError", () => { + const mockErrorData = { + error: "", + errorDescription: "", + }; + + it("should return true for isUserNotFound when error is USER_NOT_FOUND", () => { + const errorData = { ...mockErrorData, error: "user_not_found" }; + const signInError = new SignInError(errorData as any); + expect(signInError.isUserNotFound()).toBe(true); + }); + + it("should return true for isInvalidUsername when errorDescription mentions username", () => { + const errorData = new CustomAuthApiError( + "invalid_request", + "username parameter is empty or not valid", + "correlation-id", + [90100] + ); + + const signInError = new SignInError(errorData as any); + expect(signInError.isInvalidUsername()).toBe(true); + }); + + it("should return true for isInvalidPassword when error matches INVALID_GRANT with 50126", () => { + const errorData = new CustomAuthApiError( + "invalid_grant", + "Invalid grant", + "correlation-id", + [50126] + ); + const signInError = new SignInError(errorData); + expect(signInError.isPasswordIncorrect()).toBe(true); + }); + + it("should return true for isInvalidPassword when error is InvalidArgumentError and message includes 'password'", () => { + const errorData = new InvalidArgumentError("password"); + const signInError = new SignInError(errorData); + expect(signInError.isPasswordIncorrect()).toBe(true); + }); + + it("should return true for isUnsupportedChallengeType when error matches unsupported types", () => { + const errorData = { + ...mockErrorData, + error: "unsupported_challenge_type", + }; + const signInError = new SignInError(errorData as any); + expect(signInError.isUnsupportedChallengeType()).toBe(true); + }); + + it("should return true for isRedirect when error is an instance of RedirectError", () => { + const redirectError = new RedirectError(mockErrorData as any); + const signInError = new SignInError(redirectError as any); + expect(signInError.isRedirectRequired()).toBe(true); + }); + + it("should return false for all methods when error data does not match any condition", () => { + const errorData = { ...mockErrorData, error: "some_other_error" }; + const signInError = new SignInError(errorData as any); + + expect(signInError.isUserNotFound()).toBe(false); + expect(signInError.isInvalidUsername()).toBe(false); + expect(signInError.isPasswordIncorrect()).toBe(false); + expect(signInError.isUnsupportedChallengeType()).toBe(false); + expect(signInError.isRedirectRequired()).toBe(false); + }); + + it("should return true for isTokenExpired when error matches token expired types", () => { + const errorData = new CustomAuthApiError( + "expired_token", + "expired token", + "correlation-id", + [] + ); + const signInError = new SignInError(errorData as any); + expect(signInError.isTokenExpired()).toBe(true); + }); +}); + +describe("SignInSubmitPasswordError", () => { + it("should return true for isInvalidPassword when error matches INVALID_GRANT with 50126", () => { + const errorData = new CustomAuthApiError( + "invalid_grant", + "Invalid grant", + "correlation-id", + [50126] + ); + const submitPasswordError = new SignInSubmitPasswordError(errorData); + expect(submitPasswordError.isInvalidPassword()).toBe(true); + }); + + it("should return true for isInvalidPassword when error is InvalidArgumentError and message includes 'password'", () => { + const errorData = new InvalidArgumentError("password"); + const submitPasswordError = new SignInSubmitPasswordError(errorData); + expect(submitPasswordError.isInvalidPassword()).toBe(true); + }); +}); + +describe("SignInSubmitCodeError", () => { + it("should return true for isInvalidCode when error matches INVALID_GRANT and INVALID_OOB_VALUE", () => { + const errorData = new CustomAuthApiError( + "invalid_grant", + "Invalid grant", + "correlation-id", + [], + "invalid_oob_value" + ); + const submitCodeError = new SignInSubmitCodeError(errorData); + expect(submitCodeError.isInvalidCode()).toBe(true); + }); + + it("should return true for isInvalidCode when error is InvalidArgumentError and message includes 'code'", () => { + const errorData = new InvalidArgumentError("code"); + const submitCodeError = new SignInSubmitCodeError(errorData); + expect(submitCodeError.isInvalidCode()).toBe(true); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInCodeRequiredState.spec.ts b/lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInCodeRequiredState.spec.ts new file mode 100644 index 0000000000..3d862a32e0 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInCodeRequiredState.spec.ts @@ -0,0 +1,196 @@ +import { CustomAuthAccountData } from "../../../../../src/custom_auth/get_account/auth_flow/CustomAuthAccountData.js"; +import { CustomAuthBrowserConfiguration } from "../../../../../src/custom_auth/configuration/CustomAuthConfiguration.js"; +import { InvalidArgumentError } from "../../../../../src/custom_auth/core/error/InvalidArgumentError.js"; +import { + SignInResendCodeError, + SignInSubmitCodeError, +} from "../../../../../src/custom_auth/sign_in/auth_flow/error_type/SignInError.js"; +import { SignInResendCodeResult } from "../../../../../src/custom_auth/sign_in/auth_flow/result/SignInResendCodeResult.js"; +import { SignInSubmitCodeResult } from "../../../../../src/custom_auth/sign_in/auth_flow/result/SignInSubmitCodeResult.js"; +import { + createSignInCodeSendResult, + createSignInCompleteResult, +} from "../../../../../src/custom_auth/sign_in/interaction_client/result/SignInActionResult.js"; +import { SignInClient } from "../../../../../src/custom_auth/sign_in/interaction_client/SignInClient.js"; +import { CustomAuthSilentCacheClient } from "../../../../../src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { SignInCodeRequiredState } from "../../../../../src/custom_auth/sign_in/auth_flow/state/SignInCodeRequiredState.js"; +import { DefaultCustomAuthApiCodeLength } from "../../../../../src/custom_auth/CustomAuthConstants.js"; +import { getDefaultLogger } from "../../../test_resources/TestModules.js"; + +describe("SignInCodeRequiredState", () => { + const mockConfig = { + auth: { clientId: "test-client-id" }, + customAuth: { challengeTypes: ["code"] }, + } as unknown as jest.Mocked; + + const mockSignInClient = { + submitCode: jest.fn(), + resendCode: jest.fn(), + } as unknown as jest.Mocked; + + const mockCacheClient = + {} as unknown as jest.Mocked; + + const username = "testuser"; + const correlationId = "test-correlation-id"; + const continuationToken = "test-continuation-token"; + + let state: SignInCodeRequiredState; + + beforeEach(() => { + state = new SignInCodeRequiredState({ + username: username, + signInClient: mockSignInClient, + cacheClient: mockCacheClient, + correlationId: correlationId, + logger: getDefaultLogger(), + continuationToken: continuationToken, + config: mockConfig, + codeLength: 8, + scopes: ["scope1", "scope2"], + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("submitCode", () => { + it("should return an error result if code is invalid", async () => { + let result = await state.submitCode(""); + + expect(result.isFailed()).toBeTruthy(); + expect(result.error).toBeInstanceOf(SignInSubmitCodeError); + expect(result.error?.isInvalidCode()).toBe(true); + expect(result.error?.errorData).toBeInstanceOf( + InvalidArgumentError + ); + expect(result.error?.errorData?.errorDescription).toContain("code"); + }); + + it("should successfully submit a code and return a result", async () => { + mockSignInClient.submitCode.mockResolvedValue( + createSignInCompleteResult({ + correlationId: correlationId, + authenticationResult: { + accessToken: "test-access-token", + idToken: "test-id-token", + expiresOn: new Date(Date.now() + 3600 * 1000), + tokenType: "Bearer", + correlationId: correlationId, + authority: "https://test-authority.com", + tenantId: "test-tenant-id", + scopes: [], + account: { + homeAccountId: "", + environment: "", + tenantId: "test-tenant-id", + username: username, + localAccountId: "", + idToken: "test-id-token", + }, + idTokenClaims: {}, + fromCache: false, + uniqueId: "test-unique-id", + }, + }) + ); + + const result = await state.submitCode("12345678"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignInSubmitCodeResult); + expect(result.data).toBeInstanceOf(CustomAuthAccountData); + expect(mockSignInClient.submitCode).toHaveBeenCalledWith({ + clientId: "test-client-id", + correlationId: correlationId, + challengeType: ["code"], + scopes: ["scope1", "scope2"], + continuationToken: continuationToken, + code: "12345678", + username: username, + }); + }); + + it("should return an error result if submitCode throws an error", async () => { + const mockError = new Error("Submission failed"); + mockSignInClient.submitCode.mockRejectedValue(mockError); + + const result = await state.submitCode("valid-code"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignInSubmitCodeResult); + expect(result.error).toBeDefined(); + expect(result.error).toBeInstanceOf(SignInSubmitCodeError); + }); + + it("should still trigger the call to submit code even if no codeLength returned from previous call", async () => { + mockSignInClient.submitCode.mockResolvedValue( + createSignInCompleteResult({ + correlationId: correlationId, + authenticationResult: { + accessToken: "test-access-token", + idToken: "test-id-token", + expiresOn: new Date(Date.now() + 3600 * 1000), + tokenType: "Bearer", + correlationId: correlationId, + authority: "https://test-authority.com", + tenantId: "test-tenant-id", + scopes: [], + account: { + homeAccountId: "", + environment: "", + tenantId: "test-tenant-id", + username: username, + localAccountId: "", + idToken: "test-id-token", + }, + idTokenClaims: {}, + fromCache: false, + uniqueId: "test-unique-id", + }, + }) + ); + + (state as any)["stateParameters"]["codeLength"] = + DefaultCustomAuthApiCodeLength; + const result = await state.submitCode("12345678"); + expect(result.isCompleted()).toBeTruthy(); + expect(result.error).toBeUndefined(); + }); + }); + + describe("resendCode", () => { + it("should successfully resend a code and return a result", async () => { + mockSignInClient.resendCode.mockResolvedValue( + createSignInCodeSendResult({ + correlationId: correlationId, + continuationToken: "new-continuation-token", + challengeChannel: "code", + challengeTargetLabel: "email", + codeLength: 6, + bindingMethod: "email-otp", + }) + ); + + const result = await state.resendCode(); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignInResendCodeResult); + expect(result.data).toBeUndefined(); + expect(result.isCodeRequired()).toBeTruthy(); + }); + + it("should return an error result if resendCode throws an error", async () => { + const mockError = new Error("Resend code failed"); + mockSignInClient.resendCode.mockRejectedValue(mockError); + + const result = await state.resendCode(); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignInResendCodeResult); + expect(result.error).toBeDefined(); + expect(result.error).toBeInstanceOf(SignInResendCodeError); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInContinuationState.spec.ts b/lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInContinuationState.spec.ts new file mode 100644 index 0000000000..ba910c3d6f --- /dev/null +++ b/lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInContinuationState.spec.ts @@ -0,0 +1,107 @@ +import { CustomAuthAccountData } from "../../../../../src/custom_auth/get_account/auth_flow/CustomAuthAccountData.js"; +import { CustomAuthBrowserConfiguration } from "../../../../../src/custom_auth/configuration/CustomAuthConfiguration.js"; +import { SignInError } from "../../../../../src/custom_auth/sign_in/auth_flow/error_type/SignInError.js"; +import { SignInResult } from "../../../../../src/custom_auth/sign_in/auth_flow/result/SignInResult.js"; +import { SignInContinuationState } from "../../../../../src/custom_auth/sign_in/auth_flow/state/SignInContinuationState.js"; +import { createSignInCompleteResult } from "../../../../../src/custom_auth/sign_in/interaction_client/result/SignInActionResult.js"; +import { SignInClient } from "../../../../../src/custom_auth/sign_in/interaction_client/SignInClient.js"; +import { SignInScenario } from "../../../../../src/custom_auth/sign_in/auth_flow/SignInScenario.js"; +import { CustomAuthSilentCacheClient } from "../../../../../src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { getDefaultLogger } from "../../../test_resources/TestModules.js"; + +describe("SignInContinuationState", () => { + const mockConfig = { + auth: { clientId: "test-client-id" }, + customAuth: { challengeTypes: ["code", "password", "redirect"] }, + } as unknown as jest.Mocked; + + const mockSignInClient = { + signInWithContinuationToken: jest.fn(), + } as unknown as jest.Mocked; + + const mockCacheClient = + {} as unknown as jest.Mocked; + + const username = "testuser"; + const correlationId = "test-correlation-id"; + const continuationToken = "test-continuation-token"; + + let state: SignInContinuationState; + + beforeEach(() => { + state = new SignInContinuationState({ + username: username, + signInClient: mockSignInClient, + cacheClient: mockCacheClient, + correlationId: correlationId, + logger: getDefaultLogger(), + continuationToken: continuationToken, + config: mockConfig, + signInScenario: SignInScenario.SignInAfterSignUp, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should successfully sign in and return a result", async () => { + mockSignInClient.signInWithContinuationToken.mockResolvedValue( + createSignInCompleteResult({ + correlationId: correlationId, + authenticationResult: { + accessToken: "test-access-token", + idToken: "test-id-token", + expiresOn: new Date(Date.now() + 3600 * 1000), + tokenType: "Bearer", + correlationId: correlationId, + authority: "https://test-authority.com", + tenantId: "test-tenant-id", + scopes: [], + account: { + homeAccountId: "", + environment: "", + tenantId: "test-tenant-id", + username: username, + localAccountId: "", + idToken: "test-id-token", + }, + idTokenClaims: {}, + fromCache: false, + uniqueId: "test-unique-id", + }, + }) + ); + + const result = await state.signIn({ scopes: ["scope1", "scope2"] }); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignInResult); + expect(result.data).toBeInstanceOf(CustomAuthAccountData); + expect( + mockSignInClient.signInWithContinuationToken + ).toHaveBeenCalledWith({ + clientId: "test-client-id", + correlationId: correlationId, + challengeType: ["code", "password", "redirect"], + scopes: ["scope1", "scope2"], + continuationToken: continuationToken, + username: username, + signInScenario: SignInScenario.SignInAfterSignUp, + }); + }); + + it("should return an error result if signIn throws an error", async () => { + const mockError = new Error("Sign in failed"); + mockSignInClient.signInWithContinuationToken.mockRejectedValue( + mockError + ); + + const result = await state.signIn(); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignInResult); + expect(result.error).toBeDefined(); + expect(result.error).toBeInstanceOf(SignInError); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInPasswordRequiredState.spec.ts b/lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInPasswordRequiredState.spec.ts new file mode 100644 index 0000000000..43cecc8334 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/sign_in/auth_flow/state/SignInPasswordRequiredState.spec.ts @@ -0,0 +1,113 @@ +import { CustomAuthAccountData } from "../../../../../src/custom_auth/get_account/auth_flow/CustomAuthAccountData.js"; +import { CustomAuthBrowserConfiguration } from "../../../../../src/custom_auth/configuration/CustomAuthConfiguration.js"; +import { InvalidArgumentError } from "../../../../../src/custom_auth/core/error/InvalidArgumentError.js"; +import { SignInSubmitPasswordError } from "../../../../../src/custom_auth/sign_in/auth_flow/error_type/SignInError.js"; +import { SignInSubmitPasswordResult } from "../../../../../src/custom_auth/sign_in/auth_flow/result/SignInSubmitPasswordResult.js"; +import { SignInPasswordRequiredState } from "../../../../../src/custom_auth/sign_in/auth_flow/state/SignInPasswordRequiredState.js"; +import { createSignInCompleteResult } from "../../../../../src/custom_auth/sign_in/interaction_client/result/SignInActionResult.js"; +import { SignInClient } from "../../../../../src/custom_auth/sign_in/interaction_client/SignInClient.js"; +import { CustomAuthSilentCacheClient } from "../../../../../src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { getDefaultLogger } from "../../../test_resources/TestModules.js"; + +describe("SignInPasswordRequiredState", () => { + const mockConfig = { + auth: { clientId: "test-client-id" }, + customAuth: { challengeTypes: ["password"] }, + } as unknown as jest.Mocked; + + const mockSignInClient = { + submitPassword: jest.fn(), + } as unknown as jest.Mocked; + + const mockCacheClient = + {} as unknown as jest.Mocked; + + const username = "testuser"; + const correlationId = "test-correlation-id"; + const continuationToken = "test-continuation-token"; + + let state: SignInPasswordRequiredState; + + beforeEach(() => { + state = new SignInPasswordRequiredState({ + username: username, + signInClient: mockSignInClient, + cacheClient: mockCacheClient, + correlationId: correlationId, + logger: getDefaultLogger(), + continuationToken: continuationToken, + config: mockConfig, + scopes: ["scope1", "scope2"], + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it("should return an error result if password is empty", async () => { + const result = await state.submitPassword(""); + + expect(result.isFailed()).toBe(true); + expect(result.error).toBeInstanceOf(SignInSubmitPasswordError); + expect(result.error?.errorData).toBeInstanceOf(InvalidArgumentError); + expect(result.error?.errorData?.errorDescription).toContain("password"); + }); + + it("should successfully submit a password and return a result", async () => { + mockSignInClient.submitPassword.mockResolvedValue( + createSignInCompleteResult({ + correlationId: correlationId, + authenticationResult: { + accessToken: "test-access-token", + idToken: "test-id-token", + expiresOn: new Date(Date.now() + 3600 * 1000), + tokenType: "Bearer", + correlationId: correlationId, + authority: "https://test-authority.com", + tenantId: "test-tenant-id", + scopes: [], + account: { + homeAccountId: "", + environment: "", + tenantId: "test-tenant-id", + username: username, + localAccountId: "", + idToken: "test-id-token", + }, + idTokenClaims: {}, + fromCache: false, + uniqueId: "test-unique-id", + }, + }) + ); + + const result = await state.submitPassword("valid-password"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignInSubmitPasswordResult); + expect(result.isCompleted()).toBe(true); + expect(result.data).toBeInstanceOf(CustomAuthAccountData); + expect(mockSignInClient.submitPassword).toHaveBeenCalledWith({ + clientId: "test-client-id", + correlationId: correlationId, + challengeType: ["password"], + scopes: ["scope1", "scope2"], + continuationToken: continuationToken, + password: "valid-password", + username: username, + }); + }); + + it("should return an error result if submitPassword throws an error", async () => { + const mockError = new Error("Submission failed"); + mockSignInClient.submitPassword.mockRejectedValue(mockError); + + const result = await state.submitPassword("valid-password"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignInSubmitPasswordResult); + expect(result.error).toBeDefined(); + expect(result.error).toBeInstanceOf(SignInSubmitPasswordError); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/sign_in/interation_client/SignInClient.spec.ts b/lib/msal-browser/test/custom_auth/sign_in/interation_client/SignInClient.spec.ts new file mode 100644 index 0000000000..5b6e259b2a --- /dev/null +++ b/lib/msal-browser/test/custom_auth/sign_in/interation_client/SignInClient.spec.ts @@ -0,0 +1,358 @@ +import { SignInClient } from "../../../../src/custom_auth/sign_in/interaction_client/SignInClient.js"; +import { customAuthConfig } from "../../test_resources/CustomAuthConfig.js"; +import { CustomAuthAuthority } from "../../../../src/custom_auth/core/CustomAuthAuthority.js"; +import { ChallengeType } from "../../../../src/custom_auth/CustomAuthConstants.js"; +import { + SIGN_IN_CODE_SEND_RESULT_TYPE, + SIGN_IN_COMPLETED_RESULT_TYPE, + SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE, + SignInCodeSendResult, +} from "../../../../src/custom_auth/sign_in/interaction_client/result/SignInActionResult.js"; +import { SignInScenario } from "../../../../src/custom_auth/sign_in/auth_flow/SignInScenario.js"; +import { StubbedNetworkModule } from "@azure/msal-common/browser"; +import { buildConfiguration } from "../../../../src/config/Configuration.js"; +import { + getDefaultBrowserCacheManager, + getDefaultCrypto, + getDefaultEventHandler, + getDefaultLogger, + getDefaultNavigationClient, + getDefaultPerformanceClient, +} from "../../test_resources/TestModules.js"; +import { + TestServerTokenResponse, + TestTenantId, +} from "../../test_resources/TestConstants.js"; + +jest.mock( + "../../../../src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.js", + () => { + let signInApiClient = { + initiate: jest.fn(), + requestChallenge: jest.fn(), + requestTokensWithPassword: jest.fn(), + requestTokensWithOob: jest.fn(), + requestTokenWithContinuationToken: jest.fn(), + }; + let signUpApiClient = { + start: jest.fn(), + requestChallenge: jest.fn(), + continueWithCode: jest.fn(), + continueWithPassword: jest.fn(), + continueWithAttributes: jest.fn(), + }; + let resetPasswordApiClient = { + start: jest.fn(), + requestChallenge: jest.fn(), + continueWithCode: jest.fn(), + submitNewPassword: jest.fn(), + pollCompletion: jest.fn(), + }; + + // Set up the prototype or instance methods/properties + const CustomAuthApiClient = jest.fn().mockImplementation(() => ({ + signInApi: signInApiClient, + signUpApi: signUpApiClient, + resetPasswordApi: resetPasswordApiClient, + })); + + const mockedApiClient = new CustomAuthApiClient(); + return { + mockedApiClient, + signInApiClient, + signUpApiClient, + resetPasswordApiClient, + }; + } +); + +describe("SignInClient", () => { + let client: SignInClient; + let authority: CustomAuthAuthority; + const { mockedApiClient, signInApiClient } = jest.requireMock( + "../../../../src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.js" + ); + + beforeEach(() => { + const clientId = customAuthConfig.auth.clientId; + const mockBrowserConfiguration = buildConfiguration( + { auth: { clientId: clientId } }, + false + ); + const mockLogger = getDefaultLogger(); + const mockPerformanceClient = getDefaultPerformanceClient(clientId); + const mockEventHandler = getDefaultEventHandler(); + const mockCrypto = getDefaultCrypto( + clientId, + mockLogger, + mockPerformanceClient + ); + const mockCacheManager = getDefaultBrowserCacheManager( + clientId, + mockLogger, + mockPerformanceClient, + mockEventHandler, + undefined, + mockBrowserConfiguration.cache + ); + + authority = new CustomAuthAuthority( + customAuthConfig.auth.authority ?? "", + mockBrowserConfiguration, + StubbedNetworkModule, + mockCacheManager, + mockLogger, + customAuthConfig.customAuth.authApiProxyUrl + ); + + client = new SignInClient( + mockBrowserConfiguration, + mockCacheManager, + mockCrypto, + mockLogger, + mockEventHandler, + getDefaultNavigationClient(), + mockPerformanceClient, + mockedApiClient, + authority + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("start", () => { + it("should return SignInCodeSendResult when challenge type is OOB", async () => { + signInApiClient.initiate.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + signInApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.OOB, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + code_length: 6, + challenge_channel: "email", + challenge_target_label: "email", + }); + + const result = await client.start({ + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type === SIGN_IN_CODE_SEND_RESULT_TYPE).toBeTruthy(); + + const codeSendResult = result as SignInCodeSendResult; + expect(codeSendResult.correlationId).toBe("corr123"); + expect(codeSendResult.continuationToken).toBe( + "continuation_token_2" + ); + expect(codeSendResult.codeLength).toBe(6); + expect(codeSendResult.challengeChannel).toBe("email"); + expect(codeSendResult.challengeTargetLabel).toBe("email"); + }); + + it("should return SignInContinuationTokenResult when challenge type is PASSWORD", async () => { + signInApiClient.initiate.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + signInApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.PASSWORD, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + }); + + const result = await client.start({ + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual( + SIGN_IN_PASSWORD_REQUIRED_RESULT_TYPE + ); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + }); + }); + + describe("submitCode", () => { + it("should return SignInCompleteResult for valid code", async () => { + signInApiClient.requestTokensWithOob.mockResolvedValue( + TestServerTokenResponse + ); + + const result = await client.submitCode({ + code: "123456", + continuationToken: "continuation_token_1", + username: "abc@test.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + scopes: [], + }); + + expect(result.type).toStrictEqual(SIGN_IN_COMPLETED_RESULT_TYPE); + expect(result.correlationId).toBe( + TestServerTokenResponse.correlation_id + ); + expect(result.authenticationResult).toBeDefined(); + expect(result.authenticationResult.accessToken).toBe( + TestServerTokenResponse.access_token + ); + expect(result.authenticationResult.idToken).toBe( + TestServerTokenResponse.id_token + ); + expect(result.authenticationResult.expiresOn).toBeDefined(); + expect(result.authenticationResult.tokenType).toBe( + TestServerTokenResponse.token_type + ); + expect(result.authenticationResult.authority).toBe( + authority.canonicalAuthority + ); + expect(result.authenticationResult.tenantId).toBe(TestTenantId); + expect(result.authenticationResult.account).toBeDefined(); + expect(result.authenticationResult.account.username).toBe( + "abc@test.com" + ); + }); + }); + + describe("submitPassword", () => { + it("should return SignInCompleteResult for valid password", async () => { + signInApiClient.requestTokensWithPassword.mockResolvedValue( + TestServerTokenResponse + ); + + const result = await client.submitPassword({ + password: "123456", + continuationToken: "continuation_token_1", + username: "abc@test.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + scopes: [], + }); + + expect(result.type).toStrictEqual(SIGN_IN_COMPLETED_RESULT_TYPE); + expect(result.correlationId).toBe( + TestServerTokenResponse.correlation_id + ); + expect(result.authenticationResult).toBeDefined(); + expect(result.authenticationResult.accessToken).toBe( + TestServerTokenResponse.access_token + ); + expect(result.authenticationResult.idToken).toBe( + TestServerTokenResponse.id_token + ); + expect(result.authenticationResult.expiresOn).toBeDefined(); + expect(result.authenticationResult.tokenType).toBe( + TestServerTokenResponse.token_type + ); + expect(result.authenticationResult.authority).toBe( + authority.canonicalAuthority + ); + expect(result.authenticationResult.tenantId).toBe(TestTenantId); + expect(result.authenticationResult.account).toBeDefined(); + expect(result.authenticationResult.account.username).toBe( + "abc@test.com" + ); + }); + }); + + describe("resendCode", () => { + it("should return SignInCodeSendResult", async () => { + signInApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.OOB, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + code_length: 6, + challenge_channel: "email", + challenge_target_label: "email", + }); + + const result = await client.resendCode({ + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + expect(result.codeLength).toBe(6); + expect(result.challengeChannel).toBe("email"); + expect(result.challengeTargetLabel).toBe("email"); + }); + }); + + describe("signInWithContinuationToken", () => { + it("should return SignInCompleteResult", async () => { + signInApiClient.requestTokenWithContinuationToken.mockResolvedValue( + TestServerTokenResponse + ); + + const result = await client.signInWithContinuationToken({ + continuationToken: "continuation_token_1", + username: "abc@test.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + scopes: [], + signInScenario: SignInScenario.SignInAfterSignUp, + }); + + expect(result.correlationId).toBe( + TestServerTokenResponse.correlation_id + ); + expect(result.authenticationResult).toBeDefined(); + expect(result.authenticationResult.accessToken).toBe( + TestServerTokenResponse.access_token + ); + expect(result.authenticationResult.idToken).toBe( + TestServerTokenResponse.id_token + ); + expect(result.authenticationResult.expiresOn).toBeDefined(); + expect(result.authenticationResult.tokenType).toBe( + TestServerTokenResponse.token_type + ); + expect(result.authenticationResult.authority).toBe( + authority.canonicalAuthority + ); + expect(result.authenticationResult.tenantId).toBe(TestTenantId); + expect(result.authenticationResult.account).toBeDefined(); + expect(result.authenticationResult.account.username).toBe( + "abc@test.com" + ); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/sign_up/auth_flow/error_type/SignUpError.spec.ts b/lib/msal-browser/test/custom_auth/sign_up/auth_flow/error_type/SignUpError.spec.ts new file mode 100644 index 0000000000..f38030c112 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/sign_up/auth_flow/error_type/SignUpError.spec.ts @@ -0,0 +1,189 @@ +import { + CustomAuthApiError, + RedirectError, +} from "../../../../../src/custom_auth/core/error/CustomAuthApiError.js"; +import * as CustomAuthApiErrorCode from "../../../../../src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorCodes.js"; +import * as CustomAuthApiSuberror from "../../../../../src/custom_auth/core/network_client/custom_auth_api/types/ApiSuberrors.js"; +import { InvalidArgumentError } from "../../../../../src/custom_auth/index.js"; +import { + SignUpError, + SignUpResendCodeError, + SignUpSubmitAttributesError, + SignUpSubmitCodeError, + SignUpSubmitPasswordError, +} from "../../../../../src/custom_auth/sign_up/auth_flow/error_type/SignUpError.js"; + +describe("SignUpError", () => { + it("should correctly identify user already exists error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.USER_ALREADY_EXISTS, + "User already exists" + ); + const signUpError = new SignUpError(error); + expect(signUpError.isUserAlreadyExists()).toBe(true); + }); + + it("should correctly identify invalid username error", () => { + const error = new InvalidArgumentError("Invalid username"); + const signUpError = new SignUpError(error); + expect(signUpError.isInvalidUsername()).toBe(true); + + const error2 = new CustomAuthApiError( + "Some Error", + "username parameter is empty or not valid", + undefined, + [90100] + ); + const signUpError2 = new SignUpError(error2); + expect(signUpError2.isInvalidUsername()).toBe(true); + }); + + it("should correctly identify invalid password error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_GRANT, + "Invalid password", + undefined, + undefined, + CustomAuthApiSuberror.PASSWORD_IS_INVALID + ); + const signUpError = new SignUpError(error); + expect(signUpError.isInvalidPassword()).toBe(true); + }); + + it("should correctly identify missing required attributes error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.ATTRIBUTES_REQUIRED, + "Attributes required" + ); + const signUpError = new SignUpError(error); + expect(signUpError.isMissingRequiredAttributes()).toBe(true); + }); + + it("should correctly identify attributes validation failed error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_GRANT, + "Attributes validation failed", + undefined, + undefined, + CustomAuthApiSuberror.ATTRIBUTE_VALIATION_FAILED + ); + const signUpError = new SignUpError(error); + expect(signUpError.isAttributesValidationFailed()).toBe(true); + }); + + it("should correctly identify unsupported challenge type error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_REQUEST, + "The challenge_type list parameter contains an unsupported challenge type" + ); + const signUpError = new SignUpError(error); + expect(signUpError.isUnsupportedChallengeType()).toBe(true); + + const error2 = new CustomAuthApiError( + CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + "Unsupported challenge type" + ); + const signUpError2 = new SignUpError(error2); + expect(signUpError2.isUnsupportedChallengeType()).toBe(true); + }); + + it("should correctly identify redirect error", () => { + const error = new RedirectError("Redirecting..."); + const signUpError = new SignUpError(error); + expect(signUpError.isRedirectRequired()).toBe(true); + }); +}); + +describe("SignUpSubmitPasswordError", () => { + it("should correctly identify invalid password error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_GRANT, + "Invalid password", + undefined, + undefined, + CustomAuthApiSuberror.PASSWORD_IS_INVALID + ); + const signUpError = new SignUpSubmitPasswordError(error); + expect(signUpError.isInvalidPassword()).toBe(true); + + const error2 = new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_GRANT, + "Incorrect password", + undefined, + [50126] + ); + const signUpError2 = new SignUpSubmitPasswordError(error2); + expect(signUpError2.isInvalidPassword()).toBe(true); + + const error3 = new InvalidArgumentError("password is required"); + const signUpError3 = new SignUpSubmitPasswordError(error3); + expect(signUpError3.isInvalidPassword()).toBe(true); + }); + + it("should correctly identify redirect error", () => { + const error = new RedirectError("Redirecting..."); + const signUpError = new SignUpSubmitPasswordError(error); + expect(signUpError.isRedirectRequired()).toBe(true); + }); +}); + +describe("SignUpSubmitCodeError", () => { + it("should correctly identify invalid code error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_GRANT, + "Invalid code", + undefined, + undefined, + CustomAuthApiSuberror.INVALID_OOB_VALUE + ); + const signUpError = new SignUpSubmitCodeError(error); + expect(signUpError.isInvalidCode()).toBe(true); + + const error2 = new InvalidArgumentError("Invalid code"); + const signUpError2 = new SignUpSubmitCodeError(error2); + expect(signUpError2.isInvalidCode()).toBe(true); + }); + + it("should correctly identify redirect error", () => { + const error = new RedirectError("Redirecting..."); + const signUpError = new SignUpSubmitCodeError(error); + expect(signUpError.isRedirectRequired()).toBe(true); + }); +}); + +describe("SignUpSubmitAttributesError", () => { + it("should correctly identify missing required attributes error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.ATTRIBUTES_REQUIRED, + "Attributes required" + ); + const signUpError = new SignUpSubmitAttributesError(error); + expect(signUpError.isMissingRequiredAttributes()).toBe(true); + }); + + it("should correctly identify attributes validation failed error", () => { + const error = new CustomAuthApiError( + CustomAuthApiErrorCode.INVALID_GRANT, + "Attributes validation failed", + undefined, + undefined, + CustomAuthApiSuberror.ATTRIBUTE_VALIATION_FAILED + ); + const signUpError = new SignUpSubmitAttributesError(error); + expect(signUpError.isAttributesValidationFailed()).toBe(true); + }); + + it("should correctly identify redirect error", () => { + const error = new RedirectError("Redirecting..."); + const signUpError = new SignUpSubmitAttributesError(error); + expect(signUpError.isRedirectRequired()).toBe(true); + }); +}); + +describe("SignUpResendCodeError", () => { + it("should correctly identify redirect error", () => { + const error = new RedirectError("Redirecting..."); + const signUpError = new SignUpResendCodeError(error); + expect(signUpError.isRedirectRequired()).toBe(true); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpAttributesRequiredState.spec.ts b/lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpAttributesRequiredState.spec.ts new file mode 100644 index 0000000000..c20049044e --- /dev/null +++ b/lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpAttributesRequiredState.spec.ts @@ -0,0 +1,97 @@ +import { CustomAuthBrowserConfiguration } from "../../../../../src/custom_auth/configuration/CustomAuthConfiguration.js"; +import { SignUpSubmitAttributesError } from "../../../../../src/custom_auth/sign_up/auth_flow/error_type/SignUpError.js"; +import { SignUpSubmitAttributesResult } from "../../../../../src/custom_auth/sign_up/auth_flow/result/SignUpSubmitAttributesResult.js"; +import { SignUpAttributesRequiredState } from "../../../../../src/custom_auth/sign_up/auth_flow/state/SignUpAttributesRequiredState.js"; +import { createSignUpCompletedResult } from "../../../../../src/custom_auth/sign_up/interaction_client/result/SignUpActionResult.js"; +import { SignUpClient } from "../../../../../src/custom_auth/sign_up/interaction_client/SignUpClient.js"; +import { SignInClient } from "../../../../../src/custom_auth/sign_in/interaction_client/SignInClient.js"; +import { UserAccountAttributes } from "../../../../../src/custom_auth/UserAccountAttributes.js"; +import { CustomAuthSilentCacheClient } from "../../../../../src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { getDefaultLogger } from "../../../test_resources/TestModules.js"; + +describe("SignUpAttributesRequiredState", () => { + const mockConfig = { + auth: { clientId: "test-client-id" }, + customAuth: { challengeTypes: ["attributes"] }, + } as unknown as jest.Mocked; + + const mockSignUpClient = { + submitAttributes: jest.fn(), + } as unknown as jest.Mocked; + + const mockSignInClient = {} as unknown as jest.Mocked; + + const username = "testuser"; + const correlationId = "test-correlation-id"; + const continuationToken = "test-continuation-token"; + const requiredAttributes: UserAccountAttributes = { + displayName: "test-value", + }; + + let state: SignUpAttributesRequiredState; + + beforeEach(() => { + state = new SignUpAttributesRequiredState({ + username: username, + signUpClient: mockSignUpClient, + signInClient: mockSignInClient, + cacheClient: + {} as unknown as jest.Mocked, + correlationId: correlationId, + logger: getDefaultLogger(), + continuationToken: continuationToken, + config: mockConfig, + requiredAttributes: [ + { + name: "name", + type: "string", + }, + ], + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("submitAttributes", () => { + it("should return an error result if attributes is empty", async () => { + const result1 = await state.submitAttributes( + null as unknown as UserAccountAttributes + ); + + expect(result1.isFailed()).toBeTruthy(); + expect(result1.error).toBeInstanceOf(SignUpSubmitAttributesError); + expect(result1.error?.isAttributesValidationFailed()).toBe(true); + + const result2 = await state.submitAttributes({}); + + expect(result2.isFailed()).toBeTruthy(); + expect(result2.error).toBeInstanceOf(SignUpSubmitAttributesError); + expect(result2.error?.isAttributesValidationFailed()).toBe(true); + }); + + it("should successfully submit a attributes and return completed state if no credentail required", async () => { + mockSignUpClient.submitAttributes.mockResolvedValue( + createSignUpCompletedResult({ + correlationId: correlationId, + continuationToken: "continuation-token", + }) + ); + + const result = await state.submitAttributes(requiredAttributes); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignUpSubmitAttributesResult); + expect(result.isCompleted()).toBe(true); + expect(mockSignUpClient.submitAttributes).toHaveBeenCalledWith({ + clientId: "test-client-id", + correlationId: correlationId, + challengeType: ["attributes"], + continuationToken: continuationToken, + attributes: requiredAttributes, + username: username, + }); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpCodeRequiredState.spec.ts b/lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpCodeRequiredState.spec.ts new file mode 100644 index 0000000000..3d2baa65fd --- /dev/null +++ b/lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpCodeRequiredState.spec.ts @@ -0,0 +1,168 @@ +import { CustomAuthBrowserConfiguration } from "../../../../../src/custom_auth/configuration/CustomAuthConfiguration.js"; +import { InvalidArgumentError } from "../../../../../src/custom_auth/core/error/InvalidArgumentError.js"; +import { SignUpSubmitCodeError } from "../../../../../src/custom_auth/sign_up/auth_flow/error_type/SignUpError.js"; +import { SignUpResendCodeResult } from "../../../../../src/custom_auth/sign_up/auth_flow/result/SignUpResendCodeResult.js"; +import { SignUpSubmitCodeResult } from "../../../../../src/custom_auth/sign_up/auth_flow/result/SignUpSubmitCodeResult.js"; +import { SignUpCodeRequiredState } from "../../../../../src/custom_auth/sign_up/auth_flow/state/SignUpCodeRequiredState.js"; +import { + createSignUpAttributesRequiredResult, + createSignUpCodeRequiredResult, + createSignUpCompletedResult, + createSignUpPasswordRequiredResult, +} from "../../../../../src/custom_auth/sign_up/interaction_client/result/SignUpActionResult.js"; +import { SignUpClient } from "../../../../../src/custom_auth/sign_up/interaction_client/SignUpClient.js"; +import { SignInClient } from "../../../../../src/custom_auth/sign_in/interaction_client/SignInClient.js"; +import { CustomAuthSilentCacheClient } from "../../../../../src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { getDefaultLogger } from "../../../test_resources/TestModules.js"; + +describe("SignUpCodeRequiredState", () => { + const mockConfig = { + auth: { clientId: "test-client-id" }, + customAuth: { challengeTypes: ["code"] }, + } as unknown as jest.Mocked; + + const mockSignUpClient = { + submitCode: jest.fn(), + resendCode: jest.fn(), + } as unknown as jest.Mocked; + + const mockSignInClient = {} as unknown as jest.Mocked; + + const username = "testuser"; + const correlationId = "test-correlation-id"; + const continuationToken = "test-continuation-token"; + + let state: SignUpCodeRequiredState; + + beforeEach(() => { + state = new SignUpCodeRequiredState({ + username: username, + signUpClient: mockSignUpClient, + signInClient: mockSignInClient, + cacheClient: + {} as unknown as jest.Mocked, + correlationId: correlationId, + logger: getDefaultLogger(), + continuationToken: continuationToken, + config: mockConfig, + codeLength: 8, + codeResendInterval: 60, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("submitCode", () => { + it("should return an error result if code is empty", async () => { + const result = await state.submitCode(""); + + expect(result.isFailed()).toBeTruthy(); + expect(result.error).toBeInstanceOf(SignUpSubmitCodeError); + expect(result.error?.isInvalidCode()).toBe(true); + expect(result.error?.errorData).toBeInstanceOf( + InvalidArgumentError + ); + expect(result.error?.errorData?.errorDescription).toContain("code"); + }); + + it("should successfully submit a code and return completed state if no credentail required", async () => { + mockSignUpClient.submitCode.mockResolvedValue( + createSignUpCompletedResult({ + correlationId: correlationId, + continuationToken: "continuation-token", + }) + ); + + const result = await state.submitCode("12345678"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignUpSubmitCodeResult); + expect(result.isCompleted()).toBe(true); + expect(mockSignUpClient.submitCode).toHaveBeenCalledWith({ + clientId: "test-client-id", + correlationId: correlationId, + challengeType: ["code"], + continuationToken: continuationToken, + code: "12345678", + username: username, + }); + }); + + it("should successfully submit a code and return password-required state if password is required", async () => { + mockSignUpClient.submitCode.mockResolvedValue( + createSignUpPasswordRequiredResult({ + correlationId: correlationId, + continuationToken: "continuation-token", + }) + ); + + const result = await state.submitCode("12345678"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignUpSubmitCodeResult); + expect(result.isPasswordRequired()).toBe(true); + expect(mockSignUpClient.submitCode).toHaveBeenCalledWith({ + clientId: "test-client-id", + correlationId: correlationId, + challengeType: ["code"], + continuationToken: continuationToken, + code: "12345678", + username: username, + }); + }); + + it("should successfully submit a code and return attributes-required state if attributes are required", async () => { + mockSignUpClient.submitCode.mockResolvedValue( + createSignUpAttributesRequiredResult({ + correlationId: correlationId, + continuationToken: "continuation-token", + requiredAttributes: [ + { + name: "name", + type: "string", + }, + ], + }) + ); + + const result = await state.submitCode("12345678"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignUpSubmitCodeResult); + expect(result.isAttributesRequired()).toBe(true); + expect(mockSignUpClient.submitCode).toHaveBeenCalledWith({ + clientId: "test-client-id", + correlationId: correlationId, + challengeType: ["code"], + continuationToken: continuationToken, + code: "12345678", + username: username, + }); + }); + }); + + describe("resendCode", () => { + it("should successfully resend a code and return a code required state", async () => { + mockSignUpClient.resendCode.mockResolvedValue( + createSignUpCodeRequiredResult({ + correlationId: correlationId, + continuationToken: "new-continuation-token", + challengeChannel: "code", + challengeTargetLabel: "email", + codeLength: 6, + interval: 60, + bindingMethod: "email-otp", + }) + ); + + const result = await state.resendCode(); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignUpResendCodeResult); + expect(result.data).toBeUndefined(); + expect(result.isCodeRequired()).toBeTruthy(); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpPasswordRequiredState.spec.ts b/lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpPasswordRequiredState.spec.ts new file mode 100644 index 0000000000..d93712fedf --- /dev/null +++ b/lib/msal-browser/test/custom_auth/sign_up/auth_flow/state/SignUpPasswordRequiredState.spec.ts @@ -0,0 +1,118 @@ +import { CustomAuthBrowserConfiguration } from "../../../../../src/custom_auth/configuration/CustomAuthConfiguration.js"; +import { InvalidArgumentError } from "../../../../../src/custom_auth/core/error/InvalidArgumentError.js"; +import { SignUpSubmitPasswordError } from "../../../../../src/custom_auth/sign_up/auth_flow/error_type/SignUpError.js"; +import { SignUpSubmitPasswordResult } from "../../../../../src/custom_auth/sign_up/auth_flow/result/SignUpSubmitPasswordResult.js"; +import { SignUpPasswordRequiredState } from "../../../../../src/custom_auth/sign_up/auth_flow/state/SignUpPasswordRequiredState.js"; +import { + createSignUpAttributesRequiredResult, + createSignUpCompletedResult, +} from "../../../../../src/custom_auth/sign_up/interaction_client/result/SignUpActionResult.js"; +import { SignUpClient } from "../../../../../src/custom_auth/sign_up/interaction_client/SignUpClient.js"; +import { SignInClient } from "../../../../../src/custom_auth/sign_in/interaction_client/SignInClient.js"; +import { CustomAuthSilentCacheClient } from "../../../../../src/custom_auth/get_account/interaction_client/CustomAuthSilentCacheClient.js"; +import { getDefaultLogger } from "../../../test_resources/TestModules.js"; + +describe("SignUpPasswordRequiredState", () => { + const mockConfig = { + auth: { clientId: "test-client-id" }, + customAuth: { challengeTypes: ["password"] }, + } as unknown as jest.Mocked; + + const mockSignUpClient = { + submitPassword: jest.fn(), + } as unknown as jest.Mocked; + + const mockSignInClient = {} as unknown as jest.Mocked; + + const username = "testuser"; + const correlationId = "test-correlation-id"; + const continuationToken = "test-continuation-token"; + + let state: SignUpPasswordRequiredState; + + beforeEach(() => { + state = new SignUpPasswordRequiredState({ + username: username, + signUpClient: mockSignUpClient, + signInClient: mockSignInClient, + cacheClient: + {} as unknown as jest.Mocked, + correlationId: correlationId, + logger: getDefaultLogger(), + continuationToken: continuationToken, + config: mockConfig, + }); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("submitPassword", () => { + it("should return an error result if password is empty", async () => { + const result = await state.submitPassword(""); + + expect(result.isFailed()).toBeTruthy(); + expect(result.error).toBeInstanceOf(SignUpSubmitPasswordError); + expect(result.error?.isInvalidPassword()).toBe(true); + expect(result.error?.errorData).toBeInstanceOf( + InvalidArgumentError + ); + expect(result.error?.errorData?.errorDescription).toContain( + "password" + ); + }); + + it("should successfully submit a password and return completed state if no credentail required", async () => { + mockSignUpClient.submitPassword.mockResolvedValue( + createSignUpCompletedResult({ + correlationId: correlationId, + continuationToken: "continuation-token", + }) + ); + + const result = await state.submitPassword("valid-password"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignUpSubmitPasswordResult); + expect(result.isCompleted()).toBe(true); + expect(mockSignUpClient.submitPassword).toHaveBeenCalledWith({ + clientId: "test-client-id", + correlationId: correlationId, + challengeType: ["password"], + continuationToken: continuationToken, + password: "valid-password", + username: username, + }); + }); + + it("should successfully submit a password and return attributes-required state if attributes are required", async () => { + mockSignUpClient.submitPassword.mockResolvedValue( + createSignUpAttributesRequiredResult({ + correlationId: correlationId, + continuationToken: "continuation-token", + requiredAttributes: [ + { + name: "name", + type: "string", + }, + ], + }) + ); + + const result = await state.submitPassword("valid-password"); + + expect(result).toBeDefined(); + expect(result).toBeInstanceOf(SignUpSubmitPasswordResult); + expect(result.isAttributesRequired()).toBe(true); + expect(mockSignUpClient.submitPassword).toHaveBeenCalledWith({ + clientId: "test-client-id", + correlationId: correlationId, + challengeType: ["password"], + continuationToken: continuationToken, + password: "valid-password", + username: username, + }); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/sign_up/interaction_client/SignUpClient.spec.ts b/lib/msal-browser/test/custom_auth/sign_up/interaction_client/SignUpClient.spec.ts new file mode 100644 index 0000000000..34f010e72c --- /dev/null +++ b/lib/msal-browser/test/custom_auth/sign_up/interaction_client/SignUpClient.spec.ts @@ -0,0 +1,655 @@ +import { SignUpClient } from "../../../../src/custom_auth/sign_up/interaction_client/SignUpClient.js"; +import { customAuthConfig } from "../../test_resources/CustomAuthConfig.js"; +import { CustomAuthAuthority } from "../../../../src/custom_auth/core/CustomAuthAuthority.js"; +import { ChallengeType } from "../../../../src/custom_auth/CustomAuthConstants.js"; +import { + SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE, + SIGN_UP_CODE_REQUIRED_RESULT_TYPE, + SIGN_UP_COMPLETED_RESULT_TYPE, + SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE, + SignUpCodeRequiredResult, +} from "../../../../src/custom_auth/sign_up/interaction_client/result/SignUpActionResult.js"; +import { CustomAuthApiError } from "../../../../src/custom_auth/index.js"; +import * as CustomAuthApiErrorCode from "../../../../src/custom_auth/core/network_client/custom_auth_api/types/ApiErrorCodes.js"; +import { StubbedNetworkModule } from "@azure/msal-common/browser"; +import { buildConfiguration } from "../../../../src/config/Configuration.js"; +import { + getDefaultBrowserCacheManager, + getDefaultCrypto, + getDefaultEventHandler, + getDefaultLogger, + getDefaultNavigationClient, + getDefaultPerformanceClient, +} from "../../test_resources/TestModules.js"; + +jest.mock( + "../../../../src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.js", + () => { + let signInApiClient = { + initiate: jest.fn(), + requestChallenge: jest.fn(), + requestTokensWithPassword: jest.fn(), + requestTokensWithOob: jest.fn(), + signInWithContinuationToken: jest.fn(), + }; + let signUpApiClient = { + start: jest.fn(), + requestChallenge: jest.fn(), + continueWithCode: jest.fn(), + continueWithPassword: jest.fn(), + continueWithAttributes: jest.fn(), + }; + let resetPasswordApiClient = { + start: jest.fn(), + requestChallenge: jest.fn(), + continueWithCode: jest.fn(), + submitNewPassword: jest.fn(), + pollCompletion: jest.fn(), + }; + + const CustomAuthApiClient = jest.fn().mockImplementation(() => ({ + signInApi: signInApiClient, + signUpApi: signUpApiClient, + resetPasswordApi: resetPasswordApiClient, + })); + + const mockedApiClient = new CustomAuthApiClient(); + return { + mockedApiClient, + signInApiClient, + signUpApiClient, + resetPasswordApiClient, + }; + } +); + +describe("SignUpClient", () => { + let client: SignUpClient; + let authority: CustomAuthAuthority; + const { mockedApiClient, signUpApiClient } = jest.requireMock( + "../../../../src/custom_auth/core/network_client/custom_auth_api/CustomAuthApiClient.js" + ); + beforeEach(() => { + const clientId = customAuthConfig.auth.clientId; + const mockBrowserConfiguration = buildConfiguration( + { auth: { clientId: clientId } }, + false + ); + const mockLogger = getDefaultLogger(); + const mockPerformanceClient = getDefaultPerformanceClient(clientId); + const mockEventHandler = getDefaultEventHandler(); + const mockCrypto = getDefaultCrypto( + clientId, + mockLogger, + mockPerformanceClient + ); + const mockCacheManager = getDefaultBrowserCacheManager( + clientId, + mockLogger, + mockPerformanceClient, + mockEventHandler, + undefined, + mockBrowserConfiguration.cache + ); + + authority = new CustomAuthAuthority( + customAuthConfig.auth.authority ?? "", + mockBrowserConfiguration, + StubbedNetworkModule, + mockCacheManager, + mockLogger, + customAuthConfig.customAuth.authApiProxyUrl + ); + + client = new SignUpClient( + mockBrowserConfiguration, + mockCacheManager, + mockCrypto, + mockLogger, + mockEventHandler, + getDefaultNavigationClient(), + mockPerformanceClient, + mockedApiClient, + authority + ); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + describe("start", () => { + it("should return SignUpCodeRequiredResult when challenge type is OOB", async () => { + signUpApiClient.start.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + signUpApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.OOB, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + code_length: 6, + challenge_channel: "email", + challenge_target_label: "email", + }); + + const result = await client.start({ + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual( + SIGN_UP_CODE_REQUIRED_RESULT_TYPE + ); + const codeSendResult = result as SignUpCodeRequiredResult; + expect(codeSendResult.correlationId).toBe("corr123"); + expect(codeSendResult.continuationToken).toBe( + "continuation_token_2" + ); + expect(codeSendResult.codeLength).toBe(6); + expect(codeSendResult.challengeChannel).toBe("email"); + expect(codeSendResult.challengeTargetLabel).toBe("email"); + }); + + it("should return SignUpPasswordRequiredResult when challenge type is PASSWORD", async () => { + signUpApiClient.start.mockResolvedValue({ + continuation_token: "continuation_token_1", + }); + signUpApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.PASSWORD, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + }); + + const result = await client.start({ + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual( + SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE + ); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + }); + }); + + describe("submitCode", () => { + it("should return SignUpCompletedResult for valid code", async () => { + signUpApiClient.continueWithCode.mockResolvedValue({ + continuation_token: "continuation_token_2", + }); + + const result = await client.submitCode({ + code: "123456", + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual(SIGN_UP_COMPLETED_RESULT_TYPE); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + }); + + it("should return SignUpPasswordRequiredResult if password is required", async () => { + signUpApiClient.continueWithCode.mockRejectedValue( + new CustomAuthApiError( + CustomAuthApiErrorCode.CREDENTIAL_REQUIRED, + "Password required", + "corr123", + [55103], + undefined, + undefined, + "continuation_token_1" + ) + ); + + signUpApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.PASSWORD, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + }); + + const result = await client.submitCode({ + code: "123456", + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual( + SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE + ); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + + expect(signUpApiClient.requestChallenge).toHaveBeenCalledWith( + expect.objectContaining({ + correlationId: "corr123", + continuation_token: "continuation_token_1", + }) + ); + }); + + it("should throw error if credential is required but challenge type password isn't supported", async () => { + signUpApiClient.continueWithCode.mockRejectedValue( + new CustomAuthApiError( + CustomAuthApiErrorCode.CREDENTIAL_REQUIRED, + "Password required", + "corr123", + [55103], + undefined, + undefined, + "continuation_token_1" + ) + ); + + signUpApiClient.requestChallenge.mockResolvedValue({ + challenge_type: "passkey", + correlation_id: "corr123", + continuation_token: "continuation_token_2", + }); + + await expect( + client.submitCode({ + code: "123456", + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }) + ).rejects.toMatchObject({ + error: CustomAuthApiErrorCode.UNSUPPORTED_CHALLENGE_TYPE, + errorDescription: "Unsupported challenge type 'passkey'.", + correlationId: "corr123", + }); + + expect(signUpApiClient.requestChallenge).toHaveBeenCalledWith( + expect.objectContaining({ + correlationId: "corr123", + continuation_token: "continuation_token_1", + }) + ); + }); + + it("should return SignUpAttributesRequiredResult if attributes are required", async () => { + signUpApiClient.continueWithCode.mockRejectedValue( + new CustomAuthApiError( + CustomAuthApiErrorCode.ATTRIBUTES_REQUIRED, + "User attributes required", + "corr123", + [55106], + undefined, + [ + { + name: "name", + type: "string", + }, + ], + "continuation_token_1" + ) + ); + + const result = await client.submitCode({ + code: "123456", + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual( + SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE + ); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_1"); + }); + }); + + describe("submitPassword", () => { + it("should return SignUpCompletedResult for valid password", async () => { + signUpApiClient.continueWithPassword.mockResolvedValue({ + continuation_token: "continuation_token_2", + }); + + const result = await client.submitPassword({ + password: "123456", + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual(SIGN_UP_COMPLETED_RESULT_TYPE); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + }); + + it("should return SignUpCodeRequiredResult if oob is required", async () => { + signUpApiClient.continueWithPassword.mockRejectedValue( + new CustomAuthApiError( + CustomAuthApiErrorCode.CREDENTIAL_REQUIRED, + "credential required", + "corr123", + [55103], + undefined, + undefined, + "continuation_token_1" + ) + ); + + signUpApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.OOB, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + code_length: 6, + challenge_channel: "email", + challenge_target_label: "email", + }); + + const result = await client.submitPassword({ + password: "123456", + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual( + SIGN_UP_CODE_REQUIRED_RESULT_TYPE + ); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + + expect(signUpApiClient.requestChallenge).toHaveBeenCalledWith( + expect.objectContaining({ + correlationId: "corr123", + continuation_token: "continuation_token_1", + }) + ); + }); + + it("should return SignUpAttributesRequiredResult if attributes are required", async () => { + signUpApiClient.continueWithPassword.mockRejectedValue( + new CustomAuthApiError( + CustomAuthApiErrorCode.ATTRIBUTES_REQUIRED, + "User attributes required", + "corr123", + [55106], + undefined, + [ + { + name: "name", + type: "string", + }, + ], + "continuation_token_1" + ) + ); + + const result = await client.submitPassword({ + password: "123456", + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual( + SIGN_UP_ATTRIBUTES_REQUIRED_RESULT_TYPE + ); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_1"); + }); + }); + + describe("submitAttributes", () => { + it("should return SignUpCompletedResult for valid password", async () => { + signUpApiClient.continueWithAttributes.mockResolvedValue({ + continuation_token: "continuation_token_2", + }); + + const result = await client.submitAttributes({ + attributes: { name: "John Doe" }, + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual(SIGN_UP_COMPLETED_RESULT_TYPE); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + }); + + it("should return SignUpCodeRequiredResult if oob is required", async () => { + signUpApiClient.continueWithAttributes.mockRejectedValue( + new CustomAuthApiError( + CustomAuthApiErrorCode.CREDENTIAL_REQUIRED, + "credential required", + "corr123", + [55103], + undefined, + undefined, + "continuation_token_1" + ) + ); + + signUpApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.OOB, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + code_length: 6, + challenge_channel: "email", + target_challenge_label: "email", + }); + + const result = await client.submitAttributes({ + attributes: { name: "John Doe" }, + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual( + SIGN_UP_CODE_REQUIRED_RESULT_TYPE + ); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + + expect(signUpApiClient.requestChallenge).toHaveBeenCalledWith( + expect.objectContaining({ + correlationId: "corr123", + continuation_token: "continuation_token_1", + }) + ); + }); + + it("should return SignUpPasswordRequiredResult if password is required", async () => { + signUpApiClient.continueWithAttributes.mockRejectedValue( + new CustomAuthApiError( + CustomAuthApiErrorCode.CREDENTIAL_REQUIRED, + "Password required", + "corr123", + [55103], + undefined, + undefined, + "continuation_token_1" + ) + ); + + signUpApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.PASSWORD, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + }); + + const result = await client.submitAttributes({ + attributes: { name: "John Doe" }, + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual( + SIGN_UP_PASSWORD_REQUIRED_RESULT_TYPE + ); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + + expect(signUpApiClient.requestChallenge).toHaveBeenCalledWith( + expect.objectContaining({ + correlationId: "corr123", + continuation_token: "continuation_token_1", + }) + ); + }); + + it("should throw error if some required attributes are missing", async () => { + signUpApiClient.continueWithAttributes.mockRejectedValue( + new CustomAuthApiError( + CustomAuthApiErrorCode.ATTRIBUTES_REQUIRED, + "User attributes required", + "corr123", + [55106], + undefined, + [ + { + name: "name", + type: "string", + }, + ], + "continuation_token_1" + ) + ); + + await expect( + client.submitAttributes({ + attributes: { name: "John Doe" }, + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }) + ).rejects.toMatchObject({ + error: CustomAuthApiErrorCode.ATTRIBUTES_REQUIRED, + errorDescription: "User attributes required", + correlationId: "corr123", + errorCodes: [], + subError: "", + attributes: [ + { + name: "name", + type: "string", + }, + ], + continuationToken: "continuation_token_1", + }); + }); + }); + + describe("resendCode", () => { + it("should return SignUpCodeRequiredResult", async () => { + signUpApiClient.requestChallenge.mockResolvedValue({ + challenge_type: ChallengeType.OOB, + correlation_id: "corr123", + continuation_token: "continuation_token_2", + code_length: 6, + challenge_channel: "email", + challenge_target_label: "email", + }); + + const result = await client.resendCode({ + continuationToken: "continuation_token_1", + username: "abc@abc.com", + clientId: customAuthConfig.auth.clientId, + challengeType: [ + ChallengeType.OOB, + ChallengeType.PASSWORD, + ChallengeType.REDIRECT, + ], + correlationId: "corr123", + }); + + expect(result.type).toStrictEqual( + SIGN_UP_CODE_REQUIRED_RESULT_TYPE + ); + expect(result.correlationId).toBe("corr123"); + expect(result.continuationToken).toBe("continuation_token_2"); + expect(result.codeLength).toBe(6); + expect(result.challengeChannel).toBe("email"); + expect(result.challengeTargetLabel).toBe("email"); + }); + }); +}); diff --git a/lib/msal-browser/test/custom_auth/test_resources/CustomAuthConfig.ts b/lib/msal-browser/test/custom_auth/test_resources/CustomAuthConfig.ts new file mode 100644 index 0000000000..e4f0b87279 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/test_resources/CustomAuthConfig.ts @@ -0,0 +1,49 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { LogLevel } from "@azure/msal-browser"; +import { CustomAuthConfiguration } from "../../../src/custom_auth/configuration/CustomAuthConfiguration.js"; + +export const customAuthConfig: CustomAuthConfiguration = { + customAuth: { + challengeTypes: ["password", "oob", "redirect"], + authApiProxyUrl: + "https://myspafunctiont1.azurewebsites.net/api/ReverseProxy/", + }, + auth: { + clientId: "d5e97fb9-24bb-418d-8e7a-4e1918303c92", + authority: "https://spasamples.ciamlogin.com/", + redirectUri: "/", + }, + cache: { + cacheLocation: "sessionStorage", + storeAuthStateInCookie: false, + }, + system: { + loggerOptions: { + loggerCallback: (level, message, containsPii) => { + if (containsPii) { + return; + } + switch (level) { + case LogLevel.Error: + console.info(`[Error] ${message}`); + return; + case LogLevel.Info: + console.info(`[Info] ${message}`); + return; + case LogLevel.Verbose: + console.info(`[Verbose] ${message}`); + return; + case LogLevel.Warning: + console.info(`[Warning] ${message}`); + return; + default: + return; + } + }, + }, + }, +}; diff --git a/lib/msal-browser/test/custom_auth/test_resources/TestConstants.ts b/lib/msal-browser/test/custom_auth/test_resources/TestConstants.ts new file mode 100644 index 0000000000..7a4e1f71a8 --- /dev/null +++ b/lib/msal-browser/test/custom_auth/test_resources/TestConstants.ts @@ -0,0 +1,57 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export const TestTokenResponse = { + ACCESS_TOKEN: "fake-access-token", + REFRESH_TOKEN: "fake-refresh-token", + // This is a mock id token with a valid signature (signed by HS265 with a fake secret key), but the claims are not real. + ID_TOKEN: + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdWQiOiI4ZDljNzYzNS0wOTMzLTRiOTctYjJhZC03YzUzZDkxZGY1ZGEiLCJpc3MiOiJodHRwczovL2QzN2U1NjQ1LTQxNzAtNGNlMC1hNjE4LTFiOTAwOGIxNGU1OC5jaWFtbG9naW4uY29tL2QzN2U1NjQ1LTQxNzAtNGNlMC1hNjE4LTFiOTAwOGIxNGU1OC92Mi4wIiwiaWF0IjoxNzQwMDQ5Mjg4LCJuYmYiOjE3NDAwNDkyODgsImV4cCI6MTc0MDA1MzE4OCwiYWlvIjoiQVdRQW0vOFpBQUFBM1phQmdmWkRhaGhUOGVadThTUzhtUHFxelRIbjk5QjBIMmlUa3NvZW9mbW9pMTIya2ZvaXNqZmVnREVUVTFSczc0TkNUMDlUeUVWWjM0c3NNVnVmaHFDTVRYYjFnTUlLSFBUdEF2MlVBa2p1akZuZCtaZE8iLCJpZHAiOiJtYWlsIiwibmFtZSI6InVua25vd24iLCJvaWQiOiJkOGRjY2VlOC1iOGJjLTQ1MmMtOGJjYy1hNmViOTUzZGI0NTkiLCJwcmVmZXJyZWRfdXNlcm5hbWUiOiJhYmNAdGVzdC5jb20iLCJyaCI6IjEuQWM4QXpYUzVIc1VOcHNmZXdmZWtDZmFKU3huMVUxeFVCTHhzZmV3ZnNmZVBBSTdQQUEuIiwic2lkIjoiZGNiMDQ4NjItZjk1Ni00MzAxLWIzZmMtMGZkMzhmYTViZTdmIiwic3ViIjoiYkh5VlVkUHNmc2Fmc2RmZU16TDdhM1JYdklVbGJlSVVZQVoxMm8iLCJ0aWQiOiJkMzdlNTY0NS00MTcwLTRjZTAtYTYxOC0xYjkwMDhiMTRlNTgiLCJ1dGkiOiJZWDFPREZKX3NlZnVFbUhaZGZodWVKRERCbFFEQUEiLCJ2ZXIiOiIyLjAifQ.M0FBAIMmwwGTGpVbGFEWBy3vUfBEqNdem9MT2L5r39Y", + CLIENT_INFO: + "eyJ1aWQiOiI1MTIyZWZiMS1mM2EzLTRhNWQtYjVhZS1jNTQ3NGVhMWM3YmQiLCJ1dGlkIjoiZDM3ZTU2NDUtNDE3MC00Y2UwLWE2MTgtMWI5MDA4YjE0ZTU4In0=", +} as const; + +export const TestHomeAccountId = + "5122efb1-f3a3-4a5d-b5ae-c5474ea1c7bd.d37e5645-4170-4ce0-a618-1b9008b14e58"; // fake homeAccountId +export const TestTenantId = "d37e5645-4170-4ce0-a618-1b9008b14e58"; // fake tenantId +export const TestUsername = "abc@test.com"; // fake username + +export const TestAccounDetails = { + homeAccountId: TestHomeAccountId, + environment: "spasamples.ciamlogin.com", + tenantId: TestTenantId, + username: TestUsername, + localAccountId: "d8dcce8-b8bc-452c-8bcc-a6eb953db459", + idTokenClaims: { + tid: TestTenantId, + oid: "dcb04862-f956-4301-b3fc-0fd38fa5be7f", + preferred_username: TestUsername, + }, + name: "Test User", + idToken: TestTokenResponse.ID_TOKEN, +}; + +// mock response of POST /token endpoint when renew access token +export const TestServerTokenResponse = { + status: 200, + token_type: "Bearer", + scope: "openid profile User.Read email", + expires_in: 3600, + access_token: TestTokenResponse.ACCESS_TOKEN, + refresh_token: TestTokenResponse.REFRESH_TOKEN, + id_token: TestTokenResponse.ID_TOKEN, + client_info: TestTokenResponse.CLIENT_INFO, + correlation_id: "correlation-id", +}; + +// // mock decoded id token claims +export const TestIdTokenClaims = { + name: "unknown", +}; + +export const RenewedTokens = { + ACCESS_TOKEN: "renewed-access-token", + REFRESH_TOKEN: "renewed-refresh-token", +}; diff --git a/lib/msal-browser/test/custom_auth/test_resources/TestModules.ts b/lib/msal-browser/test/custom_auth/test_resources/TestModules.ts new file mode 100644 index 0000000000..be6be081be --- /dev/null +++ b/lib/msal-browser/test/custom_auth/test_resources/TestModules.ts @@ -0,0 +1,76 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ICrypto, IPerformanceClient, Logger } from "@azure/msal-common"; +import { EventHandler } from "../../../src/event/EventHandler.js"; +import { CryptoOps } from "../../../src/crypto/CryptoOps.js"; +import { BrowserCacheManager } from "../../../src/cache/BrowserCacheManager.js"; +import { + buildConfiguration, + CacheOptions, +} from "../../../src/config/Configuration.js"; +import { BrowserPerformanceClient } from "../../../src/telemetry/BrowserPerformanceClient.js"; +import { NavigationClient } from "../../../src/navigation/NavigationClient.js"; +import { INavigationClient } from "../../../src/navigation/INavigationClient.js"; + +export function getDefaultLogger(): Logger { + return new Logger({}, "test", "1.0.0"); +} + +export function getDefaultPerformanceClient( + clientId: string = "client-id" +): IPerformanceClient { + return new BrowserPerformanceClient({ + auth: { + clientId: clientId, + }, + }); +} + +export function getDefaultEventHandler(): EventHandler { + return new EventHandler(); +} + +export function getDefaultCrypto( + clientId: string = "client-id", + logger: Logger = getDefaultLogger(), + performanceClient: IPerformanceClient = getDefaultPerformanceClient( + clientId + ) +): ICrypto { + return new CryptoOps(logger, performanceClient); +} + +export function getDefaultNavigationClient(): INavigationClient { + return new NavigationClient(); +} + +export function getDefaultBrowserCacheManager( + clientId: string = "client-id", + logger: Logger = getDefaultLogger(), + performanceClient: IPerformanceClient = getDefaultPerformanceClient( + clientId + ), + eventHandler: EventHandler = getDefaultEventHandler(), + crypto: ICrypto = getDefaultCrypto(clientId, logger, performanceClient), + cacheOption?: Required +): BrowserCacheManager { + if (!cacheOption) { + const config = buildConfiguration( + { auth: { clientId: clientId } }, + true + ); + cacheOption = config.cache; + } + + return new BrowserCacheManager( + clientId, + cacheOption, + crypto, + logger, + performanceClient, + eventHandler + ); +} diff --git a/lib/msal-browser/test/error/NativeAuthError.spec.ts b/lib/msal-browser/test/error/NativeAuthError.spec.ts index 3c4326f0fc..4513c887c4 100644 --- a/lib/msal-browser/test/error/NativeAuthError.spec.ts +++ b/lib/msal-browser/test/error/NativeAuthError.spec.ts @@ -6,6 +6,7 @@ import { } from "../../src/error/NativeAuthError"; import { InteractionRequiredAuthError, + InteractionRequiredAuthErrorCodes, InteractionRequiredAuthErrorMessage, } from "@azure/msal-common"; import { @@ -119,6 +120,23 @@ describe("NativeAuthError Unit Tests", () => { ); }); + it("translates UX_NOT_ALLOWED status into corresponding InteractionRequiredError", () => { + const error = createNativeAuthError( + "interaction_required", + "interaction is required", + { + error: 1, + protocol_error: "testProtocolError", + properties: {}, + status: NativeStatusCode.UX_NOT_ALLOWED, + } + ); + expect(error).toBeInstanceOf(InteractionRequiredAuthError); + expect(error.errorCode).toBe( + InteractionRequiredAuthErrorCodes.uxNotAllowed + ); + }); + it("translates USER_CANCEL status into corresponding BrowserAuthError", () => { const error = createNativeAuthError( "user_cancel", diff --git a/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts index b81b4e470a..22c98976c1 100644 --- a/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/BaseInteractionClient.spec.ts @@ -21,6 +21,7 @@ import { DEFAULT_OPENID_CONFIG_RESPONSE, ID_TOKEN_CLAIMS, ID_TOKEN_ALT_CLAIMS, + RANDOM_TEST_GUID, } from "../utils/StringConstants.js"; import { BaseInteractionClient } from "../../src/interaction_client/BaseInteractionClient.js"; import { @@ -36,7 +37,7 @@ class testInteractionClient extends BaseInteractionClient { } logout(request: EndSessionRequest): Promise { - return this.clearCacheOnLogout(request.account); + return this.clearCacheOnLogout(RANDOM_TEST_GUID, request.account); } } diff --git a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts similarity index 66% rename from lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts rename to lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts index bac564dc61..8c542f7fba 100644 --- a/lib/msal-browser/test/interaction_client/NativeInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PlatformAuthInteractionClient.spec.ts @@ -14,16 +14,12 @@ import { CredentialType, TimeUtils, CacheManager, - Logger, - CacheRecord, - AADServerParamKeys, IPerformanceClient, InProgressPerformanceEvent, - PerformanceEvents, } from "@azure/msal-common"; -import { NativeMessageHandler } from "../../src/broker/nativeBroker/NativeMessageHandler.js"; +import { PlatformAuthExtensionHandler } from "../../src/broker/nativeBroker/PlatformAuthExtensionHandler.js"; import { ApiId } from "../../src/utils/BrowserConstants.js"; -import { NativeInteractionClient } from "../../src/interaction_client/NativeInteractionClient.js"; +import { PlatformAuthInteractionClient } from "../../src/interaction_client/PlatformAuthInteractionClient.js"; import { PublicClientApplication } from "../../src/app/PublicClientApplication.js"; import { ID_TOKEN_CLAIMS, @@ -39,12 +35,11 @@ import { NativeAuthErrorCodes, NativeAuthErrorMessages, } from "../../src/error/NativeAuthError.js"; -import { NativeExtensionRequestBody } from "../../src/broker/nativeBroker/NativeRequest.js"; +import { PlatformAuthRequest } from "../../src/broker/nativeBroker/PlatformAuthRequest.js"; import { getDefaultPerformanceClient } from "../utils/TelemetryUtils.js"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; import { BrowserPerformanceClient, - IPublicClientApplication, PopupRequest, SsoSilentRequest, } from "../../src/index.js"; @@ -52,8 +47,10 @@ import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; import { version } from "../../src/packageMetadata.js"; import { BrowserConstants } from "../../src/utils/BrowserConstants.js"; import * as NativeStatusCodes from "../../src/broker/nativeBroker/NativeStatusCodes.js"; +import { PlatformAuthResponse } from "../../src/broker/nativeBroker/PlatformAuthResponse.js"; +import { PlatformAuthDOMHandler } from "../../src/broker/nativeBroker/PlatformAuthDOMHandler.js"; -const MOCK_WAM_RESPONSE = { +const MOCK_WAM_RESPONSE: PlatformAuthResponse = { access_token: TEST_TOKENS.ACCESS_TOKEN, id_token: TEST_TOKENS.IDTOKEN_V2, scope: "User.Read", @@ -61,8 +58,26 @@ const MOCK_WAM_RESPONSE = { client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, account: { id: "nativeAccountId", + properties: {}, + userName: "test_username", }, properties: {}, + state: "", +}; + +const MOCK_WAM_RESPONSE_STRING_EXPIRES_IN: PlatformAuthResponse = { + access_token: TEST_TOKENS.ACCESS_TOKEN, + id_token: TEST_TOKENS.IDTOKEN_V2, + scope: "User.Read", + expires_in: 3600, + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + account: { + id: "nativeAccountId", + properties: {}, + userName: "test_username", + }, + properties: {}, + state: "", }; const testAccountEntity: AccountEntity = buildAccountFromIdTokenClaims( @@ -97,16 +112,15 @@ const testAccessTokenEntity: AccessTokenEntity = { cachedAt: `${TimeUtils.nowSeconds()}`, }; -describe("NativeInteractionClient Tests", () => { - globalThis.MessageChannel = require("worker_threads").MessageChannel; // jsdom does not include an implementation for MessageChannel - +describe("PlatformAuthInteractionClient Tests", () => { let pca: PublicClientApplication; - let nativeInteractionClient: NativeInteractionClient; + let platformAuthInteractionClient: PlatformAuthInteractionClient; + let platformAuthDOMHandler: PlatformAuthDOMHandler; let browserCacheManager: BrowserCacheManager; let internalStorage: BrowserCacheManager; - let wamProvider: NativeMessageHandler; + let wamProvider: PlatformAuthExtensionHandler; let postMessageSpy: jest.SpyInstance; let mcPort: MessagePort; let perfClient: IPerformanceClient; @@ -142,13 +156,13 @@ describe("NativeInteractionClient Tests", () => { //@ts-ignore internalStorage = pca.nativeInternalStorage; - wamProvider = new NativeMessageHandler( + wamProvider = new PlatformAuthExtensionHandler( pca.getLogger(), 2000, getDefaultPerformanceClient() ); - nativeInteractionClient = new NativeInteractionClient( + platformAuthInteractionClient = new PlatformAuthInteractionClient( // @ts-ignore pca.config, // @ts-ignore @@ -211,7 +225,7 @@ describe("NativeInteractionClient Tests", () => { }); it("Tokens found in cache", async () => { - const response = await nativeInteractionClient.acquireToken({ + const response = await platformAuthInteractionClient.acquireToken({ scopes: TEST_CONFIG.DEFAULT_SCOPES, }); expect(response.accessToken).toEqual(testAccessTokenEntity.secret); @@ -228,16 +242,118 @@ describe("NativeInteractionClient Tests", () => { }); describe("acquireToken Tests", () => { - it("acquires token successfully", async () => { + it("Extension: acquires token successfully", async () => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((): Promise => { + ).mockImplementation((): Promise => { return Promise.resolve(MOCK_WAM_RESPONSE); }); - const response = await nativeInteractionClient.acquireToken({ + const response = await platformAuthInteractionClient.acquireToken({ + scopes: ["User.Read"], + }); + expect(response.accessToken).toEqual( + MOCK_WAM_RESPONSE.access_token + ); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); + expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); + expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); + expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); + expect(response.authority).toEqual(TEST_CONFIG.validAuthority); + expect(response.scopes).toContain(MOCK_WAM_RESPONSE.scope); + expect(response.correlationId).toEqual(RANDOM_TEST_GUID); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); + expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); + }); + + it("Extension: token request contains user input extra params", async () => { + const sendMessageSpy = jest + .spyOn(PlatformAuthExtensionHandler.prototype, "sendMessage") + .mockImplementation((): Promise => { + return Promise.resolve(MOCK_WAM_RESPONSE); + }); + const response = await platformAuthInteractionClient.acquireToken({ + scopes: ["User.Read"], + extraQueryParameters: { + testQP1: "testQP1", + testQP2: "testQP2", + }, + }); + + expect(sendMessageSpy).toHaveProperty( + "mock.calls[0][0].extraParameters", + { + telemetry: "MATS", + testQP1: "testQP1", + testQP2: "testQP2", + "x-client-xtra-sku": `${BrowserConstants.MSAL_SKU}|${version},|,|,|`, + } + ); + + expect(response.accessToken).toEqual( + MOCK_WAM_RESPONSE.access_token + ); + expect(response.idToken).toEqual(MOCK_WAM_RESPONSE.id_token); + expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); + expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); + expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); + expect(response.authority).toEqual(TEST_CONFIG.validAuthority); + expect(response.scopes).toContain(MOCK_WAM_RESPONSE.scope); + expect(response.correlationId).toEqual(RANDOM_TEST_GUID); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); + expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); + }); + + it("DOM API: acquires token successfully", async () => { + platformAuthDOMHandler = new PlatformAuthDOMHandler( + pca.getLogger(), + getDefaultPerformanceClient(), + "test-correlation-id" + ); + + const testInterctionClient = new PlatformAuthInteractionClient( + // @ts-ignore + pca.config, + // @ts-ignore + pca.browserStorage, + // @ts-ignore + pca.browserCrypto, + pca.getLogger(), + // @ts-ignore + pca.eventHandler, + // @ts-ignore + pca.navigationClient, + ApiId.acquireTokenRedirect, + perfClient, + platformAuthDOMHandler, + "nativeAccountId", + // @ts-ignore + pca.nativeInternalStorage, + RANDOM_TEST_GUID + ); + + const sendMessageSpy = jest + .spyOn(PlatformAuthDOMHandler.prototype, "sendMessage") + .mockImplementation((): Promise => { + return Promise.resolve(MOCK_WAM_RESPONSE); + }); + const response = await testInterctionClient.acquireToken({ scopes: ["User.Read"], + extraQueryParameters: { + testQP1: "testQP1", + testQP2: "testQP2", + }, }); + + expect(sendMessageSpy).toHaveProperty( + "mock.calls[0][0].extraParameters", + { + telemetry: "MATS", + testQP1: "testQP1", + testQP2: "testQP2", + "x-client-xtra-sku": `${BrowserConstants.MSAL_SKU}|${version},|,|,|`, + } + ); expect(response.accessToken).toEqual( MOCK_WAM_RESPONSE.access_token ); @@ -252,8 +368,37 @@ describe("NativeInteractionClient Tests", () => { expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); }); + it("acquires token successfully with string expires_in", async () => { + jest.spyOn( + PlatformAuthExtensionHandler.prototype, + "sendMessage" + ).mockImplementation((): Promise => { + return Promise.resolve(MOCK_WAM_RESPONSE_STRING_EXPIRES_IN); + }); + const response = await platformAuthInteractionClient.acquireToken({ + scopes: ["User.Read"], + }); + expect(response.accessToken).toEqual( + MOCK_WAM_RESPONSE.access_token + ); + expect(response.idToken).toEqual( + MOCK_WAM_RESPONSE_STRING_EXPIRES_IN.id_token + ); + expect(response.uniqueId).toEqual(ID_TOKEN_CLAIMS.oid); + expect(response.tenantId).toEqual(ID_TOKEN_CLAIMS.tid); + expect(response.idTokenClaims).toEqual(ID_TOKEN_CLAIMS); + expect(response.authority).toEqual(TEST_CONFIG.validAuthority); + expect(response.scopes).toContain( + MOCK_WAM_RESPONSE_STRING_EXPIRES_IN.scope + ); + expect(response.correlationId).toEqual(RANDOM_TEST_GUID); + expect(response.account).toEqual(TEST_ACCOUNT_INFO); + expect(response.tokenType).toEqual(AuthenticationScheme.BEARER); + expect(response.expiresOn).toBeDefined(); + }); + it("throws if prompt: select_account", (done) => { - nativeInteractionClient + platformAuthInteractionClient .acquireToken({ scopes: ["User.Read"], prompt: PromptValue.SELECT_ACCOUNT, @@ -270,7 +415,7 @@ describe("NativeInteractionClient Tests", () => { }); it("throws if prompt: create", (done) => { - nativeInteractionClient + platformAuthInteractionClient .acquireToken({ scopes: ["User.Read"], prompt: PromptValue.CREATE, @@ -288,12 +433,12 @@ describe("NativeInteractionClient Tests", () => { it("prompt: none succeeds", async () => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((): Promise => { + ).mockImplementation((): Promise => { return Promise.resolve(MOCK_WAM_RESPONSE); }); - const response = await nativeInteractionClient.acquireToken({ + const response = await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], prompt: PromptValue.NONE, }); @@ -313,12 +458,12 @@ describe("NativeInteractionClient Tests", () => { it("prompt: consent succeeds", async () => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((): Promise => { + ).mockImplementation((): Promise => { return Promise.resolve(MOCK_WAM_RESPONSE); }); - const response = await nativeInteractionClient.acquireToken({ + const response = await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], prompt: PromptValue.CONSENT, }); @@ -338,12 +483,12 @@ describe("NativeInteractionClient Tests", () => { it("prompt: login succeeds", async () => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((): Promise => { + ).mockImplementation((): Promise => { return Promise.resolve(MOCK_WAM_RESPONSE); }); - const response = await nativeInteractionClient.acquireToken({ + const response = await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], prompt: PromptValue.LOGIN, }); @@ -362,16 +507,22 @@ describe("NativeInteractionClient Tests", () => { }); it("does not throw account switch error when homeaccountid is same", (done) => { - const mockWamResponse = { + const raw_client_info = + "eyJ1aWQiOiAiMDAwMDAwMDAtMDAwMC0wMDAwLTY2ZjMtMzMzMmVjYTdlYTgxIiwgInV0aWQiOiIzMzM4MDQwZC02YzY3LTRjNWItYjExMi0zNmEzMDRiNjZkYWQifQ=="; + + const mockWamResponse: PlatformAuthResponse = { access_token: TEST_TOKENS.ACCESS_TOKEN, - id_token: TEST_TOKENS.IDTOKEN_V2, + id_token: TEST_TOKENS.IDTOKEN_V2_ALT, scope: "User.Read", expires_in: 3600, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + client_info: raw_client_info, account: { id: "different-nativeAccountId", + properties: {}, + userName: "test_username", }, properties: {}, + state: "", }; jest.spyOn( @@ -380,12 +531,12 @@ describe("NativeInteractionClient Tests", () => { ).mockReturnValue(TEST_ACCOUNT_INFO); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((): Promise => { + ).mockImplementation((): Promise => { return Promise.resolve(mockWamResponse); }); - nativeInteractionClient + platformAuthInteractionClient .acquireToken({ scopes: ["User.Read"], }) @@ -408,7 +559,7 @@ describe("NativeInteractionClient Tests", () => { const raw_client_info = "eyJ1aWQiOiAiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwgInV0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcifQ=="; - const mockWamResponse = { + const mockWamResponse: PlatformAuthResponse = { access_token: TEST_TOKENS.ACCESS_TOKEN, id_token: TEST_TOKENS.IDTOKEN_V2_ALT, scope: "User.Read", @@ -416,8 +567,11 @@ describe("NativeInteractionClient Tests", () => { client_info: raw_client_info, account: { id: "different-nativeAccountId", + properties: {}, + userName: "test_username", }, properties: {}, + state: "", }; jest.spyOn( @@ -426,12 +580,12 @@ describe("NativeInteractionClient Tests", () => { ).mockReturnValue(TEST_ACCOUNT_INFO); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((): Promise => { + ).mockImplementation((): Promise => { return Promise.resolve(mockWamResponse); }); - nativeInteractionClient + platformAuthInteractionClient .acquireToken({ scopes: ["User.Read"], }) @@ -444,18 +598,76 @@ describe("NativeInteractionClient Tests", () => { }); }); - it("ssoSilent overwrites prompt to be 'none' and succeeds", async () => { + it("does not throw error on user switch for double brokering", (done) => { + const raw_client_info = + "eyJ1aWQiOiAiMDAwMDAwMDAtMDAwMC0wMDAwLTAwMDAtMDAwMDAwMDAwMDAwIiwgInV0aWQiOiI3MmY5ODhiZi04NmYxLTQxYWYtOTFhYi0yZDdjZDAxMWRiNDcifQ=="; + + const mockWamResponse: PlatformAuthResponse = { + access_token: TEST_TOKENS.ACCESS_TOKEN, + id_token: TEST_TOKENS.IDTOKEN_V2_ALT, + scope: "User.Read", + expires_in: 3600, + client_info: raw_client_info, + account: { + id: "different-nativeAccountId", + properties: {}, + userName: "test_username", + }, + properties: {}, + state: "", + }; + + jest.spyOn( + CacheManager.prototype, + "getAccountInfoFilteredBy" + ).mockReturnValue(TEST_ACCOUNT_INFO); + jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((nativeRequest): Promise => { - expect( - nativeRequest.request && nativeRequest.request.prompt - ).toBe(PromptValue.NONE); - return Promise.resolve(MOCK_WAM_RESPONSE); + ).mockImplementation((): Promise => { + return Promise.resolve(mockWamResponse); }); + + platformAuthInteractionClient + .acquireToken({ + scopes: ["User.Read"], + redirectUri: "localhost", + extraQueryParameters: { + brk_client_id: "broker_client_id", + brk_redirect_uri: "https://broker_redirect_uri.com", + client_id: "parent_client_id", + }, + }) + .catch((e) => { + console.error( + "User switch error should not have been thrown." + ); + expect(e.errorCode).not.toBe( + NativeAuthErrorCodes.userSwitch + ); + expect(e.errorMessage).not.toBe( + NativeAuthErrorMessages[NativeAuthErrorCodes.userSwitch] + ); + done(); + }); + done(); + }); + + it("ssoSilent overwrites prompt to be 'none' and succeeds", async () => { + jest.spyOn( + PlatformAuthExtensionHandler.prototype, + "sendMessage" + ).mockImplementation( + (nativeRequest): Promise => { + expect(nativeRequest && nativeRequest.prompt).toBe( + PromptValue.NONE + ); + return Promise.resolve(MOCK_WAM_RESPONSE); + } + ); // @ts-ignore - const nativeInteractionClient = new NativeInteractionClient( + const nativeInteractionClient = new PlatformAuthInteractionClient( // @ts-ignore pca.config, // @ts-ignore @@ -497,16 +709,18 @@ describe("NativeInteractionClient Tests", () => { it("acquireTokenSilent overwrites prompt to be 'none' and succeeds", async () => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((nativeRequest): Promise => { - expect( - nativeRequest.request && nativeRequest.request.prompt - ).toBe(PromptValue.NONE); - return Promise.resolve(MOCK_WAM_RESPONSE); - }); + ).mockImplementation( + (nativeRequest): Promise => { + expect(nativeRequest && nativeRequest.prompt).toBe( + PromptValue.NONE + ); + return Promise.resolve(MOCK_WAM_RESPONSE); + } + ); // @ts-ignore - const nativeInteractionClient = new NativeInteractionClient( + const nativeInteractionClient = new PlatformAuthInteractionClient( // @ts-ignore pca.config, // @ts-ignore @@ -548,42 +762,40 @@ describe("NativeInteractionClient Tests", () => { it("adds MSAL.js SKU to request extra query parameters", async () => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((message): Promise => { - expect( - message.request?.extraParameters!["x-client-xtra-sku"] - ).toEqual(`${BrowserConstants.MSAL_SKU}|${version},|,|,|`); + ).mockImplementation((request): Promise => { + expect(request?.extraParameters!["x-client-xtra-sku"]).toEqual( + `${BrowserConstants.MSAL_SKU}|${version},|,|,|` + ); return Promise.resolve(MOCK_WAM_RESPONSE); }); - await nativeInteractionClient.acquireToken({ + await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], }); }); it("adds MSAL.js and Chrome extension SKUs to request extra query parameters", async () => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((message): Promise => { - expect( - message.request?.extraParameters!["x-client-xtra-sku"] - ).toEqual( + ).mockImplementation((request): Promise => { + expect(request.extraParameters!["x-client-xtra-sku"]).toEqual( `${BrowserConstants.MSAL_SKU}|${version},|,chrome|1.0.2,|` ); return Promise.resolve(MOCK_WAM_RESPONSE); }); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "getExtensionId" ).mockReturnValue("ppnbnpeolgkicgegkbkbjmhlideopiji"); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "getExtensionVersion" ).mockReturnValue("1.0.2"); - nativeInteractionClient = new NativeInteractionClient( + platformAuthInteractionClient = new PlatformAuthInteractionClient( // @ts-ignore pca.config, // @ts-ignore @@ -604,34 +816,32 @@ describe("NativeInteractionClient Tests", () => { RANDOM_TEST_GUID ); - await nativeInteractionClient.acquireToken({ + await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], }); }); it("adds MSAL.js and unknown extension SKUs to request extra query parameters", async () => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((message): Promise => { - expect( - message.request?.extraParameters!["x-client-xtra-sku"] - ).toEqual( + ).mockImplementation((request): Promise => { + expect(request.extraParameters!["x-client-xtra-sku"]).toEqual( `${BrowserConstants.MSAL_SKU}|${version},|,unknown|2.3.4,|` ); return Promise.resolve(MOCK_WAM_RESPONSE); }); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "getExtensionId" ).mockReturnValue("random_extension_id"); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "getExtensionVersion" ).mockReturnValue("2.3.4"); - nativeInteractionClient = new NativeInteractionClient( + platformAuthInteractionClient = new PlatformAuthInteractionClient( // @ts-ignore pca.config, // @ts-ignore @@ -652,20 +862,20 @@ describe("NativeInteractionClient Tests", () => { RANDOM_TEST_GUID ); - await nativeInteractionClient.acquireToken({ + await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], }); }); it("does not set native broker error to server telemetry", async () => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((message): Promise => { + ).mockImplementation((message): Promise => { return Promise.resolve(MOCK_WAM_RESPONSE); }); - await nativeInteractionClient.acquireToken({ + await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], }); expect( @@ -683,15 +893,15 @@ describe("NativeInteractionClient Tests", () => { it("sets native broker error to server telemetry", async () => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((message): Promise => { + ).mockImplementation((message): Promise => { return Promise.reject( new NativeAuthError("test_native_error_code") ); }); try { - await nativeInteractionClient.acquireToken({ + await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], }); } catch (e) {} @@ -711,24 +921,28 @@ describe("NativeInteractionClient Tests", () => { it("resets native broker error in server telemetry", async () => { const sendMessageStub = jest - .spyOn(NativeMessageHandler.prototype, "sendMessage") + .spyOn(PlatformAuthExtensionHandler.prototype, "sendMessage") .mockImplementation(); sendMessageStub - .mockImplementationOnce((message): Promise => { - return Promise.reject( - new NativeAuthError( - "test_native_error_code", - "test_error_desc", - { status: NativeStatusCodes.PERSISTENT_ERROR } - ) - ); - }) - .mockImplementationOnce((message): Promise => { - return Promise.resolve(MOCK_WAM_RESPONSE); - }); + .mockImplementationOnce( + (message): Promise => { + return Promise.reject( + new NativeAuthError( + "test_native_error_code", + "test_error_desc", + { status: NativeStatusCodes.PERSISTENT_ERROR } + ) + ); + } + ) + .mockImplementationOnce( + (message): Promise => { + return Promise.resolve(MOCK_WAM_RESPONSE); + } + ); try { - await nativeInteractionClient.acquireToken({ + await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], }); } catch (e) {} @@ -745,7 +959,7 @@ describe("NativeInteractionClient Tests", () => { nativeBrokerErrorCode: "test_native_error_code", }); - await nativeInteractionClient.acquireToken({ + await platformAuthInteractionClient.acquireToken({ scopes: ["User.Read"], }); expect( @@ -766,18 +980,19 @@ describe("NativeInteractionClient Tests", () => { beforeEach(() => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" ).mockResolvedValue(MOCK_WAM_RESPONSE); }); it("does not store idToken if storeInCache.idToken = false", async () => { - const response = await nativeInteractionClient.acquireToken({ - scopes: ["User.Read"], - storeInCache: { - idToken: false, - }, - }); + const response = + await platformAuthInteractionClient.acquireToken({ + scopes: ["User.Read"], + storeInCache: { + idToken: false, + }, + }); expect(response.accessToken).toEqual( MOCK_WAM_RESPONSE.access_token ); @@ -797,12 +1012,13 @@ describe("NativeInteractionClient Tests", () => { }); it("does not store accessToken if storeInCache.accessToken = false", async () => { - const response = await nativeInteractionClient.acquireToken({ - scopes: ["User.Read"], - storeInCache: { - accessToken: false, - }, - }); + const response = + await platformAuthInteractionClient.acquireToken({ + scopes: ["User.Read"], + storeInCache: { + accessToken: false, + }, + }); expect(response.accessToken).toEqual( MOCK_WAM_RESPONSE.access_token ); @@ -822,12 +1038,13 @@ describe("NativeInteractionClient Tests", () => { }); it("does not store refreshToken if storeInCache.refreshToken = false", async () => { - const response = await nativeInteractionClient.acquireToken({ - scopes: ["User.Read"], - storeInCache: { - refreshToken: false, - }, - }); + const response = + await platformAuthInteractionClient.acquireToken({ + scopes: ["User.Read"], + storeInCache: { + refreshToken: false, + }, + }); expect(response.accessToken).toEqual( MOCK_WAM_RESPONSE.access_token ); @@ -859,12 +1076,12 @@ describe("NativeInteractionClient Tests", () => { return Promise.resolve(true); }); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((): Promise => { + ).mockImplementation((): Promise => { return Promise.resolve(MOCK_WAM_RESPONSE); }); - nativeInteractionClient.acquireTokenRedirect( + platformAuthInteractionClient.acquireTokenRedirect( { scopes: ["User.Read"], }, @@ -881,9 +1098,9 @@ describe("NativeInteractionClient Tests", () => { return Promise.resolve(true); }); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((): Promise => { + ).mockImplementation((): Promise => { return Promise.resolve(MOCK_WAM_RESPONSE); }); const callbackId = pca.addPerformanceCallback((events) => { @@ -892,7 +1109,7 @@ describe("NativeInteractionClient Tests", () => { pca.removePerformanceCallback(callbackId); done(); }); - nativeInteractionClient.acquireTokenRedirect( + platformAuthInteractionClient.acquireTokenRedirect( { scopes: ["User.Read"], }, @@ -902,9 +1119,9 @@ describe("NativeInteractionClient Tests", () => { it("throws if native token acquisition fails with fatal error", (done) => { jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((): Promise => { + ).mockImplementation((): Promise => { return Promise.reject( new NativeAuthError( "ContentError", @@ -912,7 +1129,7 @@ describe("NativeInteractionClient Tests", () => { ) ); }); - nativeInteractionClient + platformAuthInteractionClient .acquireTokenRedirect( { scopes: ["User.Read"] }, perfMeasurement @@ -933,15 +1150,15 @@ describe("NativeInteractionClient Tests", () => { return Promise.resolve(true); }); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((message): Promise => { - expect( - message.request?.extraParameters!["x-client-xtra-sku"] - ).toEqual(`${BrowserConstants.MSAL_SKU}|${version},|,|,|`); + ).mockImplementation((request): Promise => { + expect(request.extraParameters!["x-client-xtra-sku"]).toEqual( + `${BrowserConstants.MSAL_SKU}|${version},|,|,|` + ); return Promise.resolve(MOCK_WAM_RESPONSE); }); - nativeInteractionClient.acquireTokenRedirect( + platformAuthInteractionClient.acquireTokenRedirect( { scopes: ["User.Read"], }, @@ -969,14 +1186,14 @@ describe("NativeInteractionClient Tests", () => { return Promise.resolve(true); }); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((message): Promise => { + ).mockImplementation((message): Promise => { return Promise.reject( new NativeAuthError("test_native_error_code") ); }); - nativeInteractionClient.acquireTokenRedirect( + platformAuthInteractionClient.acquireTokenRedirect( { scopes: ["User.Read"], }, @@ -992,28 +1209,34 @@ describe("NativeInteractionClient Tests", () => { return Promise.resolve(true); }); const sendMessageStub = jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" ); sendMessageStub - .mockImplementationOnce((message): Promise => { - return Promise.reject( - new NativeAuthError( - "test_native_error_code", - "test_error_desc", - { status: NativeStatusCodes.PERSISTENT_ERROR } - ) - ); - }) - .mockImplementationOnce((message): Promise => { - return Promise.resolve(MOCK_WAM_RESPONSE); - }) - .mockImplementationOnce((message): Promise => { - return Promise.resolve(MOCK_WAM_RESPONSE); - }); + .mockImplementationOnce( + (message): Promise => { + return Promise.reject( + new NativeAuthError( + "test_native_error_code", + "test_error_desc", + { status: NativeStatusCodes.PERSISTENT_ERROR } + ) + ); + } + ) + .mockImplementationOnce( + (message): Promise => { + return Promise.resolve(MOCK_WAM_RESPONSE); + } + ) + .mockImplementationOnce( + (message): Promise => { + return Promise.resolve(MOCK_WAM_RESPONSE); + } + ); try { - await nativeInteractionClient.acquireTokenRedirect( + await platformAuthInteractionClient.acquireTokenRedirect( { scopes: ["User.Read"], }, @@ -1034,7 +1257,7 @@ describe("NativeInteractionClient Tests", () => { nativeBrokerErrorCode: "test_native_error_code", }); - await nativeInteractionClient.acquireTokenRedirect( + await platformAuthInteractionClient.acquireTokenRedirect( { scopes: ["User.Read"], }, @@ -1042,7 +1265,7 @@ describe("NativeInteractionClient Tests", () => { ); // @ts-ignore pca.browserStorage.setInteractionInProgress(true); - await nativeInteractionClient.handleRedirectPromise(); + await platformAuthInteractionClient.handleRedirectPromise(); expect( JSON.parse( @@ -1059,7 +1282,7 @@ describe("NativeInteractionClient Tests", () => { it("should not include onRedirectNavigate call back function in request", (done) => { jest.spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, // @ts-ignore "initializeNativeRequest" // @ts-ignore @@ -1068,7 +1291,7 @@ describe("NativeInteractionClient Tests", () => { expect(request.onRedirectNavigate).toBeUndefined(); done(); }); - nativeInteractionClient.acquireTokenRedirect( + platformAuthInteractionClient.acquireTokenRedirect( { scopes: ["User.Read"], onRedirectNavigate: (url: string) => { @@ -1090,21 +1313,21 @@ describe("NativeInteractionClient Tests", () => { return Promise.resolve(true); }); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((): Promise => { + ).mockImplementation((): Promise => { return Promise.resolve(MOCK_WAM_RESPONSE); }); // @ts-ignore pca.browserStorage.setInteractionInProgress(true); - await nativeInteractionClient.acquireTokenRedirect( + await platformAuthInteractionClient.acquireTokenRedirect( { scopes: ["User.Read"], }, perfMeasurement ); const response = - await nativeInteractionClient.handleRedirectPromise(); + await platformAuthInteractionClient.handleRedirectPromise(); expect(response).not.toBe(null); const testTokenResponse: AuthenticationResult = { @@ -1116,7 +1339,7 @@ describe("NativeInteractionClient Tests", () => { idTokenClaims: ID_TOKEN_CLAIMS, accessToken: MOCK_WAM_RESPONSE.access_token, fromCache: false, - state: undefined, + state: "", correlationId: RANDOM_TEST_GUID, expiresOn: response && response.expiresOn, // Steal the expires on from the response as this is variable account: TEST_ACCOUNT_INFO, @@ -1136,19 +1359,19 @@ describe("NativeInteractionClient Tests", () => { return Promise.resolve(true); }); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" ).mockImplementation( - (messageBody: NativeExtensionRequestBody): Promise => { - expect( - messageBody.request && messageBody.request.prompt - ).toBe(undefined); + ( + request: PlatformAuthRequest + ): Promise => { + expect(request && request.prompt).toBe(undefined); return Promise.resolve(MOCK_WAM_RESPONSE); } ); // @ts-ignore pca.browserStorage.setInteractionInProgress(true); - await nativeInteractionClient.acquireTokenRedirect( + await platformAuthInteractionClient.acquireTokenRedirect( { scopes: ["User.Read"], prompt: "login", @@ -1156,7 +1379,7 @@ describe("NativeInteractionClient Tests", () => { perfMeasurement ); const response = - await nativeInteractionClient.handleRedirectPromise(); + await platformAuthInteractionClient.handleRedirectPromise(); expect(response).not.toBe(null); const testTokenResponse: AuthenticationResult = { @@ -1168,7 +1391,7 @@ describe("NativeInteractionClient Tests", () => { idTokenClaims: ID_TOKEN_CLAIMS, accessToken: MOCK_WAM_RESPONSE.access_token, fromCache: false, - state: undefined, + state: "", correlationId: RANDOM_TEST_GUID, expiresOn: response && response.expiresOn, // Steal the expires on from the response as this is variable account: TEST_ACCOUNT_INFO, @@ -1178,54 +1401,6 @@ describe("NativeInteractionClient Tests", () => { expect(response).toEqual(testTokenResponse); }); - it("clears interaction in progress if native broker call fails", (done) => { - //here - - jest.spyOn( - NavigationClient.prototype, - "navigateExternal" - ).mockImplementation((url: string) => { - expect(url).toBe(window.location.href); - return Promise.resolve(true); - }); - let firstTime = true; - jest.spyOn( - NativeMessageHandler.prototype, - "sendMessage" - ).mockImplementation((): Promise => { - if (firstTime) { - firstTime = false; - return Promise.resolve(MOCK_WAM_RESPONSE); // The acquireTokenRedirect call should succeed - } - return Promise.reject( - new NativeAuthError("ContentError", "extension call failed") - ); // handleRedirectPromise call should fail - }); - // @ts-ignore - pca.browserStorage.setInteractionInProgress(true); - nativeInteractionClient - .acquireTokenRedirect( - { scopes: ["User.Read"] }, - perfMeasurement - ) - .then(() => { - const inProgress = - // @ts-ignore - pca.browserStorage.getInteractionInProgress(); - expect(inProgress).toBeTruthy(); - nativeInteractionClient - .handleRedirectPromise() - .catch((e) => { - expect(e.errorCode).toBe("ContentError"); - const isInProgress = - // @ts-ignore - pca.browserStorage.getInteractionInProgress(); - expect(isInProgress).toBeFalsy(); - done(); - }); - }); - }); - it("returns null if interaction is not in progress", async () => { //here @@ -1237,19 +1412,19 @@ describe("NativeInteractionClient Tests", () => { return Promise.resolve(true); }); jest.spyOn( - NativeMessageHandler.prototype, + PlatformAuthExtensionHandler.prototype, "sendMessage" - ).mockImplementation((): Promise => { + ).mockImplementation((): Promise => { return Promise.resolve(MOCK_WAM_RESPONSE); }); - await nativeInteractionClient.acquireTokenRedirect( + await platformAuthInteractionClient.acquireTokenRedirect( { scopes: ["User.Read"], }, perfMeasurement ); const response = - await nativeInteractionClient.handleRedirectPromise(); + await platformAuthInteractionClient.handleRedirectPromise(); expect(response).toBe(null); }); @@ -1257,7 +1432,7 @@ describe("NativeInteractionClient Tests", () => { // @ts-ignore pca.browserStorage.setInteractionInProgress(true); const response = - await nativeInteractionClient.handleRedirectPromise(); + await platformAuthInteractionClient.handleRedirectPromise(); expect(response).toBe(null); }); }); @@ -1266,7 +1441,7 @@ describe("NativeInteractionClient Tests", () => { it("pick up default params", async () => { const nativeRequest = // @ts-ignore - await nativeInteractionClient.initializeNativeRequest({ + await platformAuthInteractionClient.initializeNativeRequest({ scopes: ["User.Read"], prompt: PromptValue.LOGIN, }); @@ -1278,7 +1453,7 @@ describe("NativeInteractionClient Tests", () => { it("pick up broker extra query parameters", async () => { const nativeRequest = // @ts-ignore - await nativeInteractionClient.initializeNativeRequest({ + await platformAuthInteractionClient.initializeNativeRequest({ scopes: ["User.Read"], prompt: PromptValue.LOGIN, redirectUri: "localhost", @@ -1300,5 +1475,28 @@ describe("NativeInteractionClient Tests", () => { "https://broker_redirect_uri.com" ); }); + + it("pick up user input extra query parameters", async () => { + const nativeRequest = + // @ts-ignore + await platformAuthInteractionClient.initializeNativeRequest({ + scopes: ["User.Read"], + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + extraQueryParameters: { + userEQP1: "customUserParam1", + userEQP2: "customUserParam2", + }, + }); + + expect(nativeRequest.clientId).toEqual(TEST_CONFIG.MSAL_CLIENT_ID); + expect(nativeRequest.extraParameters!["userEQP1"]).toEqual( + "customUserParam1" + ); + expect(nativeRequest.extraParameters!["userEQP2"]).toEqual( + "customUserParam2" + ); + expect(nativeRequest.redirectUri).toEqual("localhost"); + }); }); }); diff --git a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts index ac1410bdf5..f91d78b419 100644 --- a/lib/msal-browser/test/interaction_client/PopupClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/PopupClient.spec.ts @@ -17,6 +17,9 @@ import { TEST_SSH_VALUES, TEST_TOKEN_RESPONSE, ID_TOKEN_CLAIMS, + validEarJWK, + getTestAuthenticationResult, + validEarJWE, } from "../utils/StringConstants.js"; import { Constants, @@ -40,26 +43,31 @@ import { TemporaryCacheKeys, ApiId, BrowserConstants, + StaticCacheKeys, } from "../../src/utils/BrowserConstants.js"; import * as BrowserCrypto from "../../src/crypto/BrowserCrypto.js"; import * as PkceGenerator from "../../src/crypto/PkceGenerator.js"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; import { NavigationClient } from "../../src/navigation/NavigationClient.js"; import { EndSessionPopupRequest } from "../../src/request/EndSessionPopupRequest.js"; import { PopupClient } from "../../src/interaction_client/PopupClient.js"; -import { NativeInteractionClient } from "../../src/interaction_client/NativeInteractionClient.js"; -import { NativeMessageHandler } from "../../src/broker/nativeBroker/NativeMessageHandler.js"; +import { PlatformAuthInteractionClient } from "../../src/interaction_client/PlatformAuthInteractionClient.js"; +import { PlatformAuthExtensionHandler } from "../../src/broker/nativeBroker/PlatformAuthExtensionHandler.js"; import { BrowserAuthError, createBrowserAuthError, BrowserAuthErrorMessage, + BrowserAuthErrorCodes, } from "../../src/error/BrowserAuthError.js"; import { InteractionHandler } from "../../src/interaction_handler/InteractionHandler.js"; import { getDefaultPerformanceClient } from "../utils/TelemetryUtils.js"; import { AuthenticationResult } from "../../src/response/AuthenticationResult.js"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; -import { BrowserAuthErrorCodes, BrowserUtils } from "../../src/index.js"; +import * as BrowserUtils from "../../src/utils/BrowserUtils.js"; import { FetchClient } from "../../src/network/FetchClient.js"; import { TestTimeUtils } from "msal-test-utils"; +import { PopupRequest } from "../../src/request/PopupRequest.js"; +import { version } from "../../src/packageMetadata.js"; const testPopupWondowDefaults = { height: BrowserConstants.POPUP_HEIGHT, @@ -69,7 +77,6 @@ const testPopupWondowDefaults = { }; describe("PopupClient", () => { - globalThis.MessageChannel = require("worker_threads").MessageChannel; // jsdom does not include an implementation for MessageChannel let popupClient: PopupClient; let pca: PublicClientApplication; let browserCacheManager: BrowserCacheManager; @@ -369,8 +376,8 @@ describe("PopupClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn( PopupClient.prototype, @@ -386,7 +393,7 @@ describe("PopupClient", () => { TEST_HASHES.TEST_SUCCESS_NATIVE_ACCOUNT_ID_POPUP ); jest.spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireToken" ).mockResolvedValue(testTokenResponse); jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ @@ -396,7 +403,7 @@ describe("PopupClient", () => { jest.spyOn(BrowserCrypto, "createNewGuid").mockReturnValue( RANDOM_TEST_GUID ); - const nativeMessageHandler = new NativeMessageHandler( + const nativeMessageHandler = new PlatformAuthExtensionHandler( //@ts-ignore pca.logger, 2000, @@ -496,8 +503,8 @@ describe("PopupClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn( PopupClient.prototype, @@ -513,7 +520,7 @@ describe("PopupClient", () => { TEST_HASHES.TEST_SUCCESS_NATIVE_ACCOUNT_ID_POPUP ); jest.spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireToken" ).mockResolvedValue(testTokenResponse); jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ @@ -604,8 +611,8 @@ describe("PopupClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(PopupClient.prototype, "initiateAuthRequest") .mockClear() @@ -786,8 +793,8 @@ describe("PopupClient", () => { "Error in creating a login url" ); jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn( PopupClient.prototype, @@ -809,7 +816,10 @@ describe("PopupClient", () => { }); } catch (e) { // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(1); + expect(window.sessionStorage).toHaveLength(2); + expect( + window.sessionStorage.getItem(StaticCacheKeys.VERSION) + ).toEqual(version); const failures = window.sessionStorage.getItem( `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` ); @@ -824,6 +834,73 @@ describe("PopupClient", () => { expect(e).toEqual(testError); } }); + + describe("EAR Flow Tests", () => { + let popupWindow: Window; + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + protocolMode: ProtocolMode.EAR, + }, + }); + await pca.initialize(); + + jest.spyOn(BrowserCrypto, "generateEarKey").mockResolvedValue( + validEarJWK + ); + popupWindow = { + ...window, + //@ts-ignore + location: { + assign: () => {}, + }, + focus: () => {}, + close: () => {}, + }; + }); + + it("Invokes EAR flow when protocolMode is set to EAR", async () => { + const validRequest: PopupRequest = { + authority: TEST_CONFIG.validAuthority, + scopes: ["openid", "profile", "offline_access"], + correlationId: TEST_CONFIG.CORRELATION_ID, + redirectUri: window.location.href, + state: TEST_STATE_VALUES.USER_STATE, + nonce: ID_TOKEN_CLAIMS.nonce, + }; + jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue( + TEST_STATE_VALUES.TEST_STATE_POPUP + ); + jest.spyOn( + PopupClient.prototype, + "openSizedPopup" + ).mockReturnValue(popupWindow); + const earFormSpy = jest + .spyOn(HTMLFormElement.prototype, "submit") + .mockImplementation(() => { + // Suppress navigation + }); + jest.spyOn( + PopupClient.prototype, + "monitorPopupForHash" + ).mockResolvedValue( + `#ear_jwe=${validEarJWE}&state=${TEST_STATE_VALUES.TEST_STATE_POPUP}` + ); + + const result = await pca.acquireTokenPopup(validRequest); + expect(result).toEqual(getTestAuthenticationResult()); + expect(earFormSpy).toHaveBeenCalled(); + }); + }); }); describe("logout", () => { diff --git a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts index f13014801d..963ebc965b 100644 --- a/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/RedirectClient.spec.ts @@ -13,13 +13,16 @@ import { TEST_TOKEN_LIFETIMES, RANDOM_TEST_GUID, DEFAULT_OPENID_CONFIG_RESPONSE, - testNavUrl, TEST_STATE_VALUES, DEFAULT_TENANT_DISCOVERY_RESPONSE, testLogoutUrl, TEST_SSH_VALUES, ID_TOKEN_CLAIMS, TEST_TOKEN_RESPONSE, + verifyUrl, + validEarJWK, + getTestAuthenticationResult, + validEarJWE, } from "../utils/StringConstants.js"; import { ServerError, @@ -28,7 +31,6 @@ import { TokenClaims, CommonAuthorizationCodeRequest, CommonAuthorizationUrlRequest, - PersistentCacheKeys, AuthorizationCodeClient, ResponseMode, ProtocolUtils, @@ -38,8 +40,6 @@ import { LogLevel, NetworkResponse, ServerAuthorizationTokenResponse, - CcsCredential, - CcsCredentialType, CommonEndSessionRequest, ServerTelemetryManager, AccountEntity, @@ -47,9 +47,9 @@ import { createClientConfigurationError, ClientConfigurationErrorCodes, IdTokenEntity, - CredentialType, InProgressPerformanceEvent, StubPerformanceClient, + ProtocolMode, } from "@azure/msal-common"; import * as BrowserUtils from "../../src/utils/BrowserUtils.js"; import { @@ -57,6 +57,7 @@ import { ApiId, BrowserCacheLocation, InteractionType, + StaticCacheKeys, } from "../../src/utils/BrowserConstants.js"; import { base64Encode } from "../../src/encode/Base64Encode.js"; import { FetchClient } from "../../src/network/FetchClient.js"; @@ -64,11 +65,12 @@ import { createBrowserAuthError, BrowserAuthErrorMessage, BrowserAuthErrorCodes, + BrowserAuthError, } from "../../src/error/BrowserAuthError.js"; -import { RedirectHandler } from "../../src/interaction_handler/RedirectHandler.js"; import { CryptoOps } from "../../src/crypto/CryptoOps.js"; import * as BrowserCrypto from "../../src/crypto/BrowserCrypto.js"; import * as PkceGenerator from "../../src/crypto/PkceGenerator.js"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; import { RedirectRequest } from "../../src/request/RedirectRequest.js"; import { NavigationClient } from "../../src/navigation/NavigationClient.js"; @@ -76,8 +78,8 @@ import { NavigationOptions } from "../../src/navigation/NavigationOptions.js"; import { RedirectClient } from "../../src/interaction_client/RedirectClient.js"; import { EventHandler } from "../../src/event/EventHandler.js"; import { EventType } from "../../src/event/EventType.js"; -import { NativeInteractionClient } from "../../src/interaction_client/NativeInteractionClient.js"; -import { NativeMessageHandler } from "../../src/broker/nativeBroker/NativeMessageHandler.js"; +import { PlatformAuthInteractionClient } from "../../src/interaction_client/PlatformAuthInteractionClient.js"; +import { PlatformAuthExtensionHandler } from "../../src/broker/nativeBroker/PlatformAuthExtensionHandler.js"; import { getDefaultPerformanceClient } from "../utils/TelemetryUtils.js"; import { AuthenticationResult } from "../../src/response/AuthenticationResult.js"; import { @@ -86,6 +88,7 @@ import { TestTimeUtils, } from "msal-test-utils"; import { BrowserPerformanceClient } from "../../src/telemetry/BrowserPerformanceClient.js"; +import { version } from "../../src/packageMetadata.js"; const cacheConfig = { cacheLocation: BrowserCacheLocation.SessionStorage, @@ -96,6 +99,17 @@ const cacheConfig = { claimsBasedCachingEnabled: false, }; +const testRequest: CommonAuthorizationUrlRequest = { + redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + authority: `${Constants.DEFAULT_AUTHORITY}`, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, + responseMode: ResponseMode.FRAGMENT, + state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, + nonce: ID_TOKEN_CLAIMS.nonce, +}; + const loggerOptions = { loggerCallback: ( level: LogLevel, @@ -110,7 +124,6 @@ const loggerOptions = { }; describe("RedirectClient", () => { - globalThis.MessageChannel = require("worker_threads").MessageChannel; // jsdom does not include an implementation for MessageChannel let redirectClient: RedirectClient; let browserStorage: BrowserCacheManager; let pca: PublicClientApplication; @@ -150,7 +163,6 @@ describe("RedirectClient", () => { // @ts-ignore browserStorage = pca.browserStorage; - // @ts-ignore redirectClient = new RedirectClient( //@ts-ignore pca.config, @@ -184,70 +196,63 @@ describe("RedirectClient", () => { describe("handleRedirectPromise", () => { it("does nothing if no hash is detected", (done) => { browserStorage.setInteractionInProgress(true); - const stateString = TEST_STATE_VALUES.TEST_STATE_REDIRECT; - const browserCrypto = new CryptoOps(new Logger({})); - const stateId = ProtocolUtils.parseRequestState( - browserCrypto, - stateString - ).libraryState.id; - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); + redirectClient - .handleRedirectPromise("", rootMeasurement) + .handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ) .then((response) => { expect(response).toBe(null); expect(window.localStorage.length).toEqual(0); - expect(window.sessionStorage.length).toEqual(0); + expect(window.sessionStorage.length).toEqual(1); + expect( + window.sessionStorage.getItem(StaticCacheKeys.VERSION) + ).toEqual(version); // Validate that the one item in sessionStorage is what we expect done(); }); }); it("cleans temporary cache and return null if no state", (done) => { browserStorage.setInteractionInProgress(true); - const stateString = TEST_STATE_VALUES.TEST_STATE_REDIRECT; - const browserCrypto = new CryptoOps(new Logger({})); - const stateId = ProtocolUtils.parseRequestState( - browserCrypto, - stateString - ).libraryState.id; - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); + redirectClient .handleRedirectPromise( "#code=ThisIsAnAuthCode", + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ) .then((response) => { expect(response).toBe(null); expect(window.localStorage.length).toEqual(0); - expect(window.sessionStorage.length).toEqual(0); + expect(window.sessionStorage.length).toEqual(1); + expect( + window.sessionStorage.getItem(StaticCacheKeys.VERSION) + ).toEqual(version); // Validate that the one item in sessionStorage is what we expect done(); }); }); it("If response hash is not a Redirect response cleans temporary cache, return null and do not remove hash", (done) => { browserStorage.setInteractionInProgress(true); - const stateString = TEST_STATE_VALUES.TEST_STATE_REDIRECT; - const browserCrypto = new CryptoOps(new Logger({})); - const stateId = ProtocolUtils.parseRequestState( - browserCrypto, - stateString - ).libraryState.id; - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); window.location.hash = TEST_HASHES.TEST_SUCCESS_CODE_HASH_POPUP; redirectClient - .handleRedirectPromise("", rootMeasurement) + .handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ) .then((response) => { expect(response).toBe(null); expect(window.localStorage.length).toEqual(0); - expect(window.sessionStorage.length).toEqual(0); + expect(window.sessionStorage.length).toEqual(1); + expect( + window.sessionStorage.getItem(StaticCacheKeys.VERSION) + ).toEqual(version); // Validate that the one item in sessionStorage is what we expect expect(window.location.hash).toEqual( TEST_HASHES.TEST_SUCCESS_CODE_HASH_POPUP ); @@ -255,78 +260,28 @@ describe("RedirectClient", () => { }); }); - it("cleans temporary cache and rethrows if error is thrown", (done) => { - browserStorage.setInteractionInProgress(true); - const stateString = TEST_STATE_VALUES.TEST_STATE_REDIRECT; - const browserCrypto = new CryptoOps(new Logger({})); - const stateId = ProtocolUtils.parseRequestState( - browserCrypto, - stateString - ).libraryState.id; - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - const testError: AuthError = new AuthError( - "Unexpected error!", - "Unexpected error" - ); - jest.spyOn( - RedirectClient.prototype, - "getRedirectResponse" - ).mockImplementation(() => { - throw testError; - }); - redirectClient - .handleRedirectPromise("", rootMeasurement) - .catch((e) => { - expect(e).toMatchObject(testError); - expect(window.localStorage.length).toEqual(0); - expect(window.sessionStorage.length).toEqual(1); // telemetry - done(); - }); - }); - - it("cleans temporary cache and return null if state cannot be decoded", (done) => { + it("return null if state cannot be decoded", (done) => { browserStorage.setInteractionInProgress(true); - const stateString = TEST_STATE_VALUES.TEST_STATE_REDIRECT; - const browserCrypto = new CryptoOps(new Logger({})); - const stateId = ProtocolUtils.parseRequestState( - browserCrypto, - stateString - ).libraryState.id; - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); redirectClient .handleRedirectPromise( TEST_HASHES.TEST_SUCCESS_HASH_STATE_NO_META, + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ) .then((response) => { expect(response).toBe(null); - expect(window.localStorage.length).toEqual(0); - expect(window.sessionStorage.length).toEqual(0); done(); }); }); - it("cleans temporary cache and re-throws error thrown by handleResponse when loginRequestUrl == current url", (done) => { + it("re-throws error thrown by handleResponse when loginRequestUrl == current url", (done) => { browserStorage.setInteractionInProgress(true); browserStorage.setTemporaryCache( TemporaryCacheKeys.ORIGIN_URI, window.location.href, true ); - const statekey = browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - browserStorage.setTemporaryCache( - statekey, - TEST_STATE_VALUES.TEST_STATE_REDIRECT, - true - ); jest.spyOn( RedirectClient.prototype, @@ -335,17 +290,17 @@ describe("RedirectClient", () => { redirectClient .handleRedirectPromise( TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT, + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ) .catch((e) => { expect(e).toEqual("Error in handleResponse"); - expect(window.localStorage.length).toEqual(0); - expect(window.sessionStorage.length).toEqual(0); done(); }); }); - it("cleans temporary cache and re-throws error thrown by handleResponse after clientside navigation to loginRequestUrl", (done) => { + it("re-throws error thrown by handleResponse after clientside navigation to loginRequestUrl", (done) => { jest.spyOn( NavigationClient.prototype, "navigateInternal" @@ -357,14 +312,6 @@ describe("RedirectClient", () => { window.location.href + "/differentPath", true ); - const statekey = browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - browserStorage.setTemporaryCache( - statekey, - TEST_STATE_VALUES.TEST_STATE_REDIRECT, - true - ); jest.spyOn( RedirectClient.prototype, @@ -373,31 +320,23 @@ describe("RedirectClient", () => { redirectClient .handleRedirectPromise( TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT, + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ) .catch((e) => { expect(e).toEqual("Error in handleResponse"); - expect(window.localStorage.length).toEqual(0); - expect(window.sessionStorage.length).toEqual(0); done(); }); }); - it("cleans temporary cache and re-throws error thrown by handleResponse when navigateToLoginRequestUrl is false", (done) => { + it("re-throws error thrown by handleResponse when navigateToLoginRequestUrl is false", (done) => { browserStorage.setInteractionInProgress(true); browserStorage.setTemporaryCache( TemporaryCacheKeys.ORIGIN_URI, window.location.href + "/differentPath", true ); - const statekey = browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - browserStorage.setTemporaryCache( - statekey, - TEST_STATE_VALUES.TEST_STATE_REDIRECT, - true - ); jest.spyOn( RedirectClient.prototype, @@ -431,36 +370,21 @@ describe("RedirectClient", () => { redirectClient .handleRedirectPromise( TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT, + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ) .catch((e) => { expect(e).toEqual("Error in handleResponse"); - expect(window.localStorage.length).toEqual(0); - expect(window.sessionStorage.length).toEqual(0); done(); }); }); it("gets hash from cache and processes response", async () => { - const stateString = TEST_STATE_VALUES.TEST_STATE_REDIRECT; - const browserCrypto = new CryptoOps(new Logger({})); - const stateId = ProtocolUtils.parseRequestState( - browserCrypto, - stateString - ).libraryState.id; - window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.ORIGIN_URI}`, TEST_URIS.TEST_REDIR_URI ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.AUTHORITY}.${stateId}`, - TEST_CONFIG.validAuthority - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}`, TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT @@ -469,10 +393,6 @@ describe("RedirectClient", () => { `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, TEST_CONFIG.MSAL_CLIENT_ID ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.NONCE_IDTOKEN}.${stateId}`, - "123523" - ); const testTokenReq: CommonAuthorizationCodeRequest = { redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, code: "thisIsATestCode", @@ -539,6 +459,8 @@ describe("RedirectClient", () => { const tokenResponse = await redirectClient.handleRedirectPromise( "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ); expect(tokenResponse?.uniqueId).toEqual(testTokenResponse.uniqueId); @@ -575,7 +497,7 @@ describe("RedirectClient", () => { pca = (pca as any).controller; // @ts-ignore - const nativeMessageHandler = new NativeMessageHandler( + const nativeMessageHandler = new PlatformAuthExtensionHandler( //@ts-ignore pca.logger, 2000, @@ -612,14 +534,6 @@ describe("RedirectClient", () => { `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.ORIGIN_URI}`, TEST_URIS.TEST_REDIR_URI ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.AUTHORITY}.${stateId}`, - TEST_CONFIG.validAuthority - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}`, TEST_HASHES.TEST_SUCCESS_NATIVE_ACCOUNT_ID_REDIRECT @@ -628,10 +542,6 @@ describe("RedirectClient", () => { `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, TEST_CONFIG.MSAL_CLIENT_ID ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.NONCE_IDTOKEN}.${stateId}`, - "123523" - ); const testTokenReq: CommonAuthorizationCodeRequest = { redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, code: "thisIsATestCode", @@ -695,12 +605,14 @@ describe("RedirectClient", () => { } }); jest.spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireToken" ).mockResolvedValue(testTokenResponse); const tokenResponse = await redirectClient.handleRedirectPromise( "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ); expect(tokenResponse?.uniqueId).toEqual(testTokenResponse.uniqueId); @@ -764,14 +676,6 @@ describe("RedirectClient", () => { `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.ORIGIN_URI}`, TEST_URIS.TEST_REDIR_URI ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.AUTHORITY}.${stateId}`, - TEST_CONFIG.validAuthority - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}`, TEST_HASHES.TEST_SUCCESS_NATIVE_ACCOUNT_ID_REDIRECT @@ -780,10 +684,6 @@ describe("RedirectClient", () => { `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, TEST_CONFIG.MSAL_CLIENT_ID ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.NONCE_IDTOKEN}.${stateId}`, - "123523" - ); const testTokenReq: CommonAuthorizationCodeRequest = { redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, code: "thisIsATestCode", @@ -800,7 +700,12 @@ describe("RedirectClient", () => { ); redirectClient - .handleRedirectPromise("", rootMeasurement) + .handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ) .catch((e) => { expect(e.errorCode).toEqual( BrowserAuthErrorMessage.nativeConnectionNotEstablished @@ -814,62 +719,6 @@ describe("RedirectClient", () => { }); }); - it("throws no cached authority error if authority is not in cache", (done) => { - const stateString = TEST_STATE_VALUES.TEST_STATE_REDIRECT; - const browserCrypto = new CryptoOps(new Logger({})); - const stateId = ProtocolUtils.parseRequestState( - browserCrypto, - stateString - ).libraryState.id; - - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.ORIGIN_URI}`, - TEST_URIS.TEST_REDIR_URI - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}`, - TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, - TEST_CONFIG.MSAL_CLIENT_ID - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.NONCE_IDTOKEN}.${stateId}`, - "123523" - ); - const testTokenReq: CommonAuthorizationCodeRequest = { - redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, - code: "thisIsATestCode", - scopes: TEST_CONFIG.DEFAULT_SCOPES, - codeVerifier: TEST_CONFIG.TEST_VERIFIER, - authority: `${Constants.DEFAULT_AUTHORITY}`, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - }; - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_PARAMS}`, - base64Encode(JSON.stringify(testTokenReq)) - ); - - redirectClient - .handleRedirectPromise("", rootMeasurement) - .catch((e) => { - expect(e).toMatchObject( - createBrowserAuthError( - BrowserAuthErrorCodes.noCachedAuthorityError - ) - ); - expect(window.sessionStorage.length).toEqual(1); // telemetry - done(); - }); - }); - it("gets hash from cache and processes error", (done) => { const testAuthCodeRequest: CommonAuthorizationCodeRequest = { redirectUri: TEST_URIS.TEST_REDIR_URI, @@ -896,14 +745,6 @@ describe("RedirectClient", () => { `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.ORIGIN_URI}`, TEST_URIS.TEST_REDIR_URI ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.AUTHORITY}.${stateId}`, - TEST_CONFIG.validAuthority - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}`, TEST_HASHES.TEST_ERROR_HASH @@ -914,7 +755,12 @@ describe("RedirectClient", () => { ); redirectClient - .handleRedirectPromise("", rootMeasurement) + .handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ) .catch((err) => { expect(err instanceof ServerError).toBeTruthy(); done(); @@ -934,22 +780,10 @@ describe("RedirectClient", () => { `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.ORIGIN_URI}`, TEST_URIS.TEST_REDIR_URI ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.AUTHORITY}.${stateId}`, - TEST_CONFIG.validAuthority - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, TEST_CONFIG.MSAL_CLIENT_ID ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.NONCE_IDTOKEN}.${stateId}`, - "123523" - ); const testTokenReq: CommonAuthorizationCodeRequest = { redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, @@ -1049,6 +883,8 @@ describe("RedirectClient", () => { const tokenResponse = await redirectClient.handleRedirectPromise( "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ); expect(tokenResponse?.uniqueId).toEqual(testTokenResponse.uniqueId); @@ -1083,22 +919,10 @@ describe("RedirectClient", () => { `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.ORIGIN_URI}`, TEST_URIS.TEST_ALTERNATE_REDIR_URI ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.AUTHORITY}.${stateId}`, - TEST_CONFIG.validAuthority - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, TEST_CONFIG.MSAL_CLIENT_ID ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.NONCE_IDTOKEN}.${stateId}`, - "123523" - ); const testTokenReq: CommonAuthorizationCodeRequest = { redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, @@ -1212,6 +1036,8 @@ describe("RedirectClient", () => { const tokenResponse = await redirectClient.handleRedirectPromise( "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ); if (!tokenResponse) { @@ -1249,22 +1075,10 @@ describe("RedirectClient", () => { `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.ORIGIN_URI}`, TEST_URIS.TEST_ALTERNATE_REDIR_URI ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.AUTHORITY}.${stateId}`, - TEST_CONFIG.validAuthority - ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.REQUEST_STATE}.${stateId}`, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); window.sessionStorage.setItem( `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, TEST_CONFIG.MSAL_CLIENT_ID ); - window.sessionStorage.setItem( - `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.NONCE_IDTOKEN}.${stateId}`, - "123523" - ); const testTokenReq: CommonAuthorizationCodeRequest = { redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, @@ -1364,6 +1178,8 @@ describe("RedirectClient", () => { const tokenResponse = await redirectClient.handleRedirectPromise( "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ); expect(tokenResponse?.uniqueId).toEqual(testTokenResponse.uniqueId); @@ -1393,7 +1209,12 @@ describe("RedirectClient", () => { TEST_URIS.TEST_ALTERNATE_REDIR_URI ); expect( - await redirectClient.handleRedirectPromise("", rootMeasurement) + await redirectClient.handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ) ).toBe(null); }); @@ -1424,7 +1245,12 @@ describe("RedirectClient", () => { true ); expect( - await redirectClient.handleRedirectPromise("", rootMeasurement) + await redirectClient.handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ) ).toBe(null); }); @@ -1451,7 +1277,12 @@ describe("RedirectClient", () => { return Promise.resolve(true); } ); - await redirectClient.handleRedirectPromise("", rootMeasurement); + await redirectClient.handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ); expect( window.sessionStorage.getItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}` @@ -1521,7 +1352,12 @@ describe("RedirectClient", () => { return Promise.resolve(true); } ); - await redirectClient.handleRedirectPromise("", rootMeasurement); + await redirectClient.handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ); expect( window.sessionStorage.getItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}` @@ -1552,7 +1388,12 @@ describe("RedirectClient", () => { return Promise.resolve(true); } ); - redirectClient.handleRedirectPromise("", rootMeasurement); + redirectClient.handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ); expect( window.sessionStorage.getItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}` @@ -1587,7 +1428,12 @@ describe("RedirectClient", () => { return Promise.resolve(true); } ); - redirectClient.handleRedirectPromise("", rootMeasurement); + redirectClient.handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ); expect( window.sessionStorage.getItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}` @@ -1618,7 +1464,12 @@ describe("RedirectClient", () => { return Promise.resolve(true); } ); - redirectClient.handleRedirectPromise("", rootMeasurement); + redirectClient.handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ); expect( window.sessionStorage.getItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}` @@ -1650,7 +1501,12 @@ describe("RedirectClient", () => { return Promise.resolve(true); } ); - redirectClient.handleRedirectPromise("", rootMeasurement); + redirectClient.handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ); expect( window.sessionStorage.getItem( `${Constants.CACHE_PREFIX}.${TEST_CONFIG.MSAL_CLIENT_ID}.${TemporaryCacheKeys.URL_HASH}` @@ -1677,7 +1533,12 @@ describe("RedirectClient", () => { }); }); redirectClient - .handleRedirectPromise("", rootMeasurement) + .handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ) .then(() => { expect(window.location.href).toEqual(loginRequestUrl); }); @@ -1703,6 +1564,8 @@ describe("RedirectClient", () => { redirectClient .handleRedirectPromise( TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT, + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ) .then(() => { @@ -1736,7 +1599,12 @@ describe("RedirectClient", () => { }); redirectClient - .handleRedirectPromise("", rootMeasurement) + .handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ) .then(() => { expect(clearHashSpy).not.toHaveBeenCalled(); expect(window.location.hash).toEqual("#testHash"); @@ -1764,7 +1632,12 @@ describe("RedirectClient", () => { }); done(); }); - redirectClient.handleRedirectPromise("", rootMeasurement); + redirectClient.handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ); }); it("returns null if inside an iframe", (done) => { @@ -1778,7 +1651,12 @@ describe("RedirectClient", () => { ); redirectClient - .handleRedirectPromise("", rootMeasurement) + .handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ) .then((response) => { expect(response).toBe(null); done(); @@ -1835,7 +1713,12 @@ describe("RedirectClient", () => { }); done(); }); - redirectClient.handleRedirectPromise("", rootMeasurement); + redirectClient.handleRedirectPromise( + "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, + rootMeasurement + ); }); it("mutes no_server_response error when back navigation is detected", async () => { @@ -1853,6 +1736,8 @@ describe("RedirectClient", () => { ); const res = await redirectClient.handleRedirectPromise( "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ); expect(res).toBeNull(); @@ -1874,6 +1759,8 @@ describe("RedirectClient", () => { ); const res = await redirectClient.handleRedirectPromise( "", + testRequest, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ); expect(res).toBeNull(); @@ -1925,11 +1812,11 @@ describe("RedirectClient", () => { it("navigates to created login url", (done) => { jest.spyOn( - RedirectHandler.prototype, + RedirectClient.prototype, "initiateAuthRequest" ).mockImplementation(async (navigateUrl): Promise => { try { - expect(navigateUrl).toEqual(testNavUrl); + verifyUrl(navigateUrl, ["user.read"]); return Promise.resolve(done()); } catch (err) { Promise.reject(err); @@ -1954,81 +1841,15 @@ describe("RedirectClient", () => { redirectClient.acquireToken(loginRequest); }); - it("Updates cache entries correctly", async () => { - const emptyRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: [], - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - }; - - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - - jest.spyOn( - NavigationClient.prototype, - "navigateExternal" - ).mockImplementation( - ( - urlNavigate: string, - options: NavigationOptions - ): Promise => { - expect(options.noHistory).toBeFalsy(); - expect(urlNavigate).not.toBe(""); - return Promise.resolve(true); - } - ); - const testLogger = new Logger(loggerOptions); - - const browserCrypto = new CryptoOps(new Logger({})); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - testLogger, - new StubPerformanceClient(), - new EventHandler() - ); - await redirectClient.acquireToken(emptyRequest); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(TEST_STATE_VALUES.TEST_STATE_REDIRECT); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateNonceKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(RANDOM_TEST_GUID); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateAuthorityKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(`${Constants.DEFAULT_AUTHORITY}`); - }); - - it("Temporary cache is cleared when 'pageshow' event is fired", (done) => { - let bfCacheCallback: (event: object) => any; - jest.spyOn(window, "addEventListener").mockImplementation( - (eventName, callback) => { - expect(eventName).toEqual("pageshow"); - // @ts-ignore - bfCacheCallback = callback; - } - ); + it("Temporary cache is cleared when 'pageshow' event is fired", (done) => { + let bfCacheCallback: (event: object) => any; + jest.spyOn(window, "addEventListener").mockImplementation( + (eventName, callback) => { + expect(eventName).toEqual("pageshow"); + // @ts-ignore + bfCacheCallback = callback; + } + ); const emptyRequest: CommonAuthorizationUrlRequest = { redirectUri: TEST_URIS.TEST_REDIR_URI, scopes: [], @@ -2070,27 +1891,6 @@ describe("RedirectClient", () => { options: NavigationOptions ): Promise => { expect(browserStorage.isInteractionInProgress()).toBe(true); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(TEST_STATE_VALUES.TEST_STATE_REDIRECT); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateNonceKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(RANDOM_TEST_GUID); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateAuthorityKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(`${Constants.DEFAULT_AUTHORITY}`); bfCacheCallback({ persisted: true }); expect(eventSpy).toHaveBeenCalledWith( EventType.RESTORE_FROM_BFCACHE, @@ -2099,27 +1899,6 @@ describe("RedirectClient", () => { expect(browserStorage.isInteractionInProgress()).toBe( false ); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(null); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateNonceKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(null); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateAuthorityKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(null); done(); return Promise.resolve(true); } @@ -2128,162 +1907,6 @@ describe("RedirectClient", () => { redirectClient.acquireToken(emptyRequest); }); - it("Adds login_hint as CCS cache entry to the cache and urlNavigate", async () => { - const testCcsCred: CcsCredential = { - credential: ID_TOKEN_CLAIMS.preferred_username || "", - type: CcsCredentialType.UPN, - }; - const emptyRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: [], - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - loginHint: ID_TOKEN_CLAIMS.preferred_username || "", - }; - - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - - jest.spyOn( - NavigationClient.prototype, - "navigateExternal" - ).mockImplementation( - ( - urlNavigate: string, - options: NavigationOptions - ): Promise => { - expect(options.noHistory).toBeFalsy(); - expect(urlNavigate).not.toBe(""); - return Promise.resolve(true); - } - ); - const testLogger = new Logger(loggerOptions); - - const browserCrypto = new CryptoOps(new Logger({})); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - testLogger, - new StubPerformanceClient(), - new EventHandler() - ); - await redirectClient.acquireToken(emptyRequest); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(TEST_STATE_VALUES.TEST_STATE_REDIRECT); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateNonceKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(RANDOM_TEST_GUID); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateAuthorityKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(`${Constants.DEFAULT_AUTHORITY}`); - expect( - browserStorage.getTemporaryCache( - TemporaryCacheKeys.CCS_CREDENTIAL, - true - ) - ).toEqual(JSON.stringify(testCcsCred)); - }); - - it("Adds account homeAccountId as CCS cache entry to the cache and urlNavigate", async () => { - const testAccount: AccountInfo = - buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); - const testCcsCred: CcsCredential = { - credential: testAccount.homeAccountId, - type: CcsCredentialType.HOME_ACCOUNT_ID, - }; - const emptyRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: [], - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - account: testAccount, - }; - - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - - jest.spyOn( - NavigationClient.prototype, - "navigateExternal" - ).mockImplementation( - ( - urlNavigate: string, - options: NavigationOptions - ): Promise => { - expect(options.noHistory).toBeFalsy(); - expect(urlNavigate).not.toBe(""); - return Promise.resolve(true); - } - ); - const testLogger = new Logger(loggerOptions); - - const browserCrypto = new CryptoOps(new Logger({})); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - testLogger, - new StubPerformanceClient(), - new EventHandler() - ); - await redirectClient.acquireToken(emptyRequest); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(TEST_STATE_VALUES.TEST_STATE_REDIRECT); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateNonceKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(RANDOM_TEST_GUID); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateAuthorityKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(`${Constants.DEFAULT_AUTHORITY}`); - expect( - browserStorage.getTemporaryCache( - TemporaryCacheKeys.CCS_CREDENTIAL, - true - ) - ).toEqual(JSON.stringify(testCcsCred)); - }); - it("Caches token request correctly", async () => { const tokenRequest: CommonAuthorizationUrlRequest = { redirectUri: TEST_URIS.TEST_REDIR_URI, @@ -2327,18 +1950,10 @@ describe("RedirectClient", () => { new EventHandler() ); await redirectClient.acquireToken(tokenRequest); - const cachedRequest: CommonAuthorizationCodeRequest = JSON.parse( - browserCrypto.base64Decode( - browserStorage.getTemporaryCache( - TemporaryCacheKeys.REQUEST_PARAMS, - true - ) || "" - ) - ); + const [cachedRequest, codeVerifier] = + browserStorage.getCachedRequest(); expect(cachedRequest.scopes).toEqual([]); - expect(cachedRequest.codeVerifier).toEqual( - TEST_CONFIG.TEST_VERIFIER - ); + expect(codeVerifier).toEqual(TEST_CONFIG.TEST_VERIFIER); expect(cachedRequest.authority).toEqual( `${Constants.DEFAULT_AUTHORITY}` ); @@ -2348,68 +1963,12 @@ describe("RedirectClient", () => { ); }); - it("Cleans cache before error is thrown", async () => { - const emptyRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: [], - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - }; - - const browserCrypto = new CryptoOps(new Logger({})); - const testLogger = new Logger(loggerOptions); - new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - testLogger, - new StubPerformanceClient(), - new EventHandler() - ); - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - - const testError = { - errorCode: "create_login_url_error", - errorMessage: "Error in creating a login url", - correlationId: TEST_CONFIG.CORRELATION_ID, - }; - jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockRejectedValue(createBrowserAuthError(testError.errorCode)); - try { - await redirectClient.acquireToken(emptyRequest); - } catch (e) { - // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(1); - const failures = window.sessionStorage.getItem( - `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` - ); - const failureObj = JSON.parse( - failures || "" - ) as ServerTelemetryEntity; - expect(failureObj.failedRequests).toHaveLength(2); - expect(failureObj.failedRequests[0]).toEqual( - ApiId.acquireTokenRedirect - ); - expect(failureObj.errors[0]).toEqual(testError.errorCode); - } - }); - it("navigates to created login url", (done) => { jest.spyOn( - RedirectHandler.prototype, + RedirectClient.prototype, "initiateAuthRequest" ).mockImplementation((navigateUrl): Promise => { - expect(navigateUrl).toEqual(testNavUrl); + verifyUrl(navigateUrl, ["user.read"]); return Promise.resolve(done()); }); jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ @@ -2431,24 +1990,17 @@ describe("RedirectClient", () => { it("passes onRedirectNavigate callback", (done) => { const onRedirectNavigate = (url: string) => { - expect(url).toEqual(testNavUrl); + verifyUrl(url, ["user.read"]); done(); }; jest.spyOn( - RedirectHandler.prototype, + RedirectClient.prototype, "initiateAuthRequest" ).mockImplementation( - ( - navigateUrl, - { - redirectTimeout: timeout, - redirectStartPage, - onRedirectNavigate: onRedirectNavigateCb, - } - ): Promise => { + (navigateUrl, onRedirectNavigateCb): Promise => { expect(onRedirectNavigateCb).toEqual(onRedirectNavigate); - expect(navigateUrl).toEqual(testNavUrl); + verifyUrl(navigateUrl, ["user.read"]); onRedirectNavigate(navigateUrl); return Promise.resolve(); } @@ -2466,188 +2018,6 @@ describe("RedirectClient", () => { redirectClient.acquireToken(loginRequest); }); - it("Updates cache entries correctly", async () => { - const testScope = "testscope"; - const emptyRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: [testScope], - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - }; - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - jest.spyOn( - NavigationClient.prototype, - "navigateExternal" - ).mockImplementation( - ( - urlNavigate: string, - options: NavigationOptions - ): Promise => { - expect(options.noHistory).toBeFalsy(); - expect(urlNavigate).not.toBe(""); - return Promise.resolve(true); - } - ); - const browserCrypto = new CryptoOps(new Logger({})); - const testLogger = new Logger(loggerOptions); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - testLogger, - new StubPerformanceClient(), - new EventHandler() - ); - await redirectClient.acquireToken(emptyRequest); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(TEST_STATE_VALUES.TEST_STATE_REDIRECT); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateNonceKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(RANDOM_TEST_GUID); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateAuthorityKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ) - ) - ).toEqual(`${Constants.DEFAULT_AUTHORITY}`); - }); - - it("Caches token request correctly", async () => { - const testScope = "testscope"; - const tokenRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: [testScope], - correlationId: RANDOM_TEST_GUID, - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - }; - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - jest.spyOn( - NavigationClient.prototype, - "navigateExternal" - ).mockImplementation( - ( - urlNavigate: string, - options: NavigationOptions - ): Promise => { - expect(options.noHistory).toBeFalsy(); - expect(urlNavigate).not.toBe(""); - return Promise.resolve(true); - } - ); - const browserCrypto = new CryptoOps(new Logger({})); - const testLogger = new Logger(loggerOptions); - const browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - testLogger, - new StubPerformanceClient(), - new EventHandler() - ); - await redirectClient.acquireToken(tokenRequest); - const cachedRequest: CommonAuthorizationCodeRequest = JSON.parse( - browserCrypto.base64Decode( - browserStorage.getTemporaryCache( - TemporaryCacheKeys.REQUEST_PARAMS, - true - ) || "" - ) - ); - expect(cachedRequest.scopes).toEqual([testScope]); - expect(cachedRequest.codeVerifier).toEqual( - TEST_CONFIG.TEST_VERIFIER - ); - expect(cachedRequest.authority).toEqual( - `${Constants.DEFAULT_AUTHORITY}` - ); - expect(cachedRequest.correlationId).toEqual(RANDOM_TEST_GUID); - expect(cachedRequest.authenticationScheme).toEqual( - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme - ); - }); - - it("Cleans cache before error is thrown", async () => { - const testScope = "testscope"; - const emptyRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: [testScope], - state: "", - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - }; - const browserCrypto = new CryptoOps(new Logger({})); - const testLogger = new Logger(loggerOptions); - new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - cacheConfig, - browserCrypto, - testLogger, - new StubPerformanceClient(), - new EventHandler() - ); - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - - const testError: AuthError = new AuthError( - "create_login_url_error", - "Error in creating a login url" - ); - jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockRejectedValue(testError); - try { - await redirectClient.acquireToken(emptyRequest); - } catch (e) { - // Test that error was cached for telemetry purposes and then thrown - expect(window.sessionStorage).toHaveLength(1); - const failures = window.sessionStorage.getItem( - `server-telemetry-${TEST_CONFIG.MSAL_CLIENT_ID}` - ); - const failureObj = JSON.parse( - failures || "" - ) as ServerTelemetryEntity; - expect(failureObj.failedRequests).toHaveLength(2); - expect(failureObj.failedRequests[0]).toEqual( - ApiId.acquireTokenRedirect - ); - expect(failureObj.errors[0]).toEqual(testError.errorCode); - expect(e).toEqual(testError); - } - }); - describe("storeInCache tests", () => { beforeEach(() => { jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue( @@ -2668,20 +2038,27 @@ describe("RedirectClient", () => { it("does not store idToken if storeInCache.idToken = false", async () => { browserStorage.setInteractionInProgress(true); - await redirectClient.acquireToken({ + const request: CommonAuthorizationUrlRequest = { redirectUri: TEST_URIS.TEST_REDIR_URI, scopes: TEST_CONFIG.DEFAULT_SCOPES, storeInCache: { idToken: false, }, nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken - onRedirectNavigate: () => { - return false; // Supress navigation - }, + responseMode: ResponseMode.FRAGMENT, + state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, + correlationId: RANDOM_TEST_GUID, + authority: TEST_CONFIG.validAuthority, + }; + await redirectClient.acquireToken({ + ...request, + onRedirectNavigate: () => false, }); const tokenResp = await redirectClient.handleRedirectPromise( TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT, + request, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ); if (!tokenResp) { @@ -2705,20 +2082,27 @@ describe("RedirectClient", () => { it("does not store accessToken if storeInCache.accessToken = false", async () => { browserStorage.setInteractionInProgress(true); - await redirectClient.acquireToken({ + const request: CommonAuthorizationUrlRequest = { redirectUri: TEST_URIS.TEST_REDIR_URI, scopes: TEST_CONFIG.DEFAULT_SCOPES, storeInCache: { accessToken: false, }, nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken - onRedirectNavigate: () => { - return false; // Supress navigation - }, + responseMode: ResponseMode.FRAGMENT, + state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, + correlationId: RANDOM_TEST_GUID, + authority: TEST_CONFIG.validAuthority, + }; + await redirectClient.acquireToken({ + ...request, + onRedirectNavigate: () => false, }); const tokenResp = await redirectClient.handleRedirectPromise( TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT, + request, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ); if (!tokenResp) { @@ -2742,20 +2126,27 @@ describe("RedirectClient", () => { it("does not store refreshToken if storeInCache.refreshToken = false", async () => { browserStorage.setInteractionInProgress(true); - await redirectClient.acquireToken({ + const request: CommonAuthorizationUrlRequest = { redirectUri: TEST_URIS.TEST_REDIR_URI, scopes: TEST_CONFIG.DEFAULT_SCOPES, storeInCache: { refreshToken: false, }, nonce: ID_TOKEN_CLAIMS.nonce, // Ensures nonce matches the mocked idToken - onRedirectNavigate: () => { - return false; // Supress navigation - }, + responseMode: ResponseMode.FRAGMENT, + state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, + correlationId: RANDOM_TEST_GUID, + authority: TEST_CONFIG.validAuthority, + }; + await redirectClient.acquireToken({ + ...request, + onRedirectNavigate: () => false, }); const tokenResp = await redirectClient.handleRedirectPromise( TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT, + request, + TEST_CONFIG.TEST_VERIFIER, rootMeasurement ); if (!tokenResp) { @@ -3436,4 +2827,149 @@ describe("RedirectClient", () => { }); }); }); + + describe("initiateAuthRequest()", () => { + it("throws error if requestUrl is empty", (done) => { + redirectClient.initiateAuthRequest("").catch((e) => { + expect(e).toBeInstanceOf(BrowserAuthError); + expect(e.errorCode).toEqual( + BrowserAuthErrorCodes.emptyNavigateUri + ); + done(); + }); + }); + + it("navigates browser window to given window location", (done) => { + const navigationClient = new NavigationClient(); + navigationClient.navigateExternal = ( + requestUrl: string, + options: NavigationOptions + ): Promise => { + expect(requestUrl).toEqual(TEST_URIS.TEST_ALTERNATE_REDIR_URI); + expect(options.timeout).toEqual(30000); + done(); + return Promise.resolve(true); + }; + + //@ts-ignore + redirectClient.navigationClient = navigationClient; + + redirectClient.initiateAuthRequest( + TEST_URIS.TEST_ALTERNATE_REDIR_URI + ); + }); + + it("doesnt navigate if onRedirectNavigate returns false", (done) => { + const navigationClient = new NavigationClient(); + navigationClient.navigateExternal = ( + urlNavigate: string, + options: NavigationOptions + ): Promise => { + done( + "Navigatation should not happen if onRedirectNavigate returns false" + ); + return Promise.reject(); + }; + + const onRedirectNavigate = (url: string) => { + expect(url).toEqual(TEST_URIS.TEST_ALTERNATE_REDIR_URI); + done(); + return false; + }; + redirectClient.initiateAuthRequest( + TEST_URIS.TEST_ALTERNATE_REDIR_URI, + onRedirectNavigate + ); + }); + + it("navigates if onRedirectNavigate doesnt return false", (done) => { + const navigationClient = new NavigationClient(); + navigationClient.navigateExternal = ( + requestUrl, + options + ): Promise => { + expect(requestUrl).toEqual(TEST_URIS.TEST_ALTERNATE_REDIR_URI); + done(); + return Promise.resolve(true); + }; + + //@ts-ignore + redirectClient.navigationClient = navigationClient; + + const onRedirectNavigate = (url: string) => { + expect(url).toEqual(TEST_URIS.TEST_ALTERNATE_REDIR_URI); + }; + redirectClient.initiateAuthRequest( + TEST_URIS.TEST_ALTERNATE_REDIR_URI, + onRedirectNavigate + ); + }); + }); + + describe("EAR Flow Tests", () => { + beforeEach(async () => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + protocolMode: ProtocolMode.EAR, + }, + system: { + redirectNavigationTimeout: 1000, + }, + }); + await pca.initialize(); + + jest.spyOn(BrowserCrypto, "generateEarKey").mockResolvedValue( + validEarJWK + ); + }); + + it("Invokes EAR flow when protocolMode is set to EAR", (done) => { + const validRequest: RedirectRequest = { + authority: TEST_CONFIG.validAuthority, + scopes: ["openid", "profile", "offline_access"], + correlationId: TEST_CONFIG.CORRELATION_ID, + redirectUri: window.location.href, + state: TEST_STATE_VALUES.USER_STATE, + nonce: ID_TOKEN_CLAIMS.nonce, + }; + jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue( + TEST_STATE_VALUES.TEST_STATE_REDIRECT + ); + jest.spyOn(HTMLFormElement.prototype, "submit").mockImplementation( + () => { + // Supress navigation + pca.handleRedirectPromise( + `#ear_jwe=${validEarJWE}&state=${TEST_STATE_VALUES.TEST_STATE_REDIRECT}` + ).then((result) => { + expect(result).toEqual(getTestAuthenticationResult()); + done(); + }); + } + ); + + pca.acquireTokenRedirect(validRequest).catch(() => {}); + }); + + it("Throws a timeout error if the form post failed to redirect within the alloted time", async () => { + const validRequest: RedirectRequest = { + scopes: ["openid", "profile", "offline_access"], + }; + const earFormSpy = jest + .spyOn(HTMLFormElement.prototype, "submit") + .mockImplementation(() => { + // Supress navigation + }); + + await expect(() => + pca.acquireTokenRedirect(validRequest) + ).rejects.toEqual( + createBrowserAuthError( + BrowserAuthErrorCodes.timedOut, + "failed_to_redirect" + ) + ); + expect(earFormSpy).toHaveBeenCalled(); + }); + }); }); diff --git a/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts index bed7fde446..0ab8d1c13e 100644 --- a/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentAuthCodeClient.spec.ts @@ -34,6 +34,7 @@ import { } from "../../src/index.js"; import { InteractionHandler } from "../../src/interaction_handler/InteractionHandler.js"; import { FetchClient } from "../../src/network/FetchClient.js"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; import { TestTimeUtils } from "msal-test-utils"; describe("SilentAuthCodeClient", () => { @@ -137,8 +138,8 @@ describe("SilentAuthCodeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); const handleCodeSpy = jest .spyOn( diff --git a/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts b/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts index 8755c3f01e..33a6a1a44a 100644 --- a/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/SilentIframeClient.spec.ts @@ -16,18 +16,21 @@ import { TEST_STATE_VALUES, TEST_TOKEN_RESPONSE, ID_TOKEN_CLAIMS, + validEarJWK, + getTestAuthenticationResult, + validEarJWE, } from "../utils/StringConstants.js"; import { AccountInfo, TokenClaims, PromptValue, - AuthorizationCodeClient, AuthenticationScheme, ServerTelemetryManager, ProtocolUtils, TenantProfile, Authority, -} from "@azure/msal-common"; + ProtocolMode, +} from "@azure/msal-common/browser"; import { createBrowserAuthError, BrowserAuthErrorMessage, @@ -36,22 +39,25 @@ import { import * as SilentHandler from "../../src/interaction_handler/SilentHandler.js"; import * as BrowserCrypto from "../../src/crypto/BrowserCrypto.js"; import * as PkceGenerator from "../../src/crypto/PkceGenerator.js"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; import { SilentIframeClient } from "../../src/interaction_client/SilentIframeClient.js"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; -import { ApiId, AuthenticationResult } from "../../src/index.js"; -import { NativeInteractionClient } from "../../src/interaction_client/NativeInteractionClient.js"; -import { NativeMessageHandler } from "../../src/broker/nativeBroker/NativeMessageHandler.js"; +import { PlatformAuthInteractionClient } from "../../src/interaction_client/PlatformAuthInteractionClient.js"; +import { PlatformAuthExtensionHandler } from "../../src/broker/nativeBroker/PlatformAuthExtensionHandler.js"; import { getDefaultPerformanceClient } from "../utils/TelemetryUtils.js"; import { InteractionHandler } from "../../src/interaction_handler/InteractionHandler.js"; import { + ApiId, BrowserConstants, InteractionType, } from "../../src/utils/BrowserConstants.js"; import { FetchClient } from "../../src/network/FetchClient.js"; import { TestTimeUtils } from "msal-test-utils"; +import { AuthenticationResult } from "../../src/response/AuthenticationResult.js"; +import { SilentRequest } from "../../src/request/SilentRequest.js"; +import { SsoSilentRequest } from "../../src/index.js"; describe("SilentIframeClient", () => { - globalThis.MessageChannel = require("worker_threads").MessageChannel; // jsdom does not include an implementation for MessageChannel let silentIframeClient: SilentIframeClient; let pca: PublicClientApplication; let browserCacheManager: BrowserCacheManager; @@ -145,8 +151,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -186,8 +192,8 @@ describe("SilentIframeClient", () => { it("Errors thrown during token acquisition are cached for telemetry and browserStorage is cleaned", (done) => { jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockRejectedValue( createBrowserAuthError( @@ -224,8 +230,8 @@ describe("SilentIframeClient", () => { it("Unexpected non-msal errors do not add correlationId and browserStorage is cleaned", (done) => { jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); const testError = { errorCode: "Unexpected error", @@ -298,8 +304,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -367,8 +373,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -407,7 +413,7 @@ describe("SilentIframeClient", () => { pca = (pca as any).controller; // @ts-ignore - const nativeMessageHandler = new NativeMessageHandler( + const nativeMessageHandler = new PlatformAuthExtensionHandler( //@ts-ignore pca.logger, 2000, @@ -478,14 +484,14 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_NATIVE_ACCOUNT_ID_SILENT ); jest.spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireToken" ).mockResolvedValue(testTokenResponse); jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ @@ -580,14 +586,14 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_NATIVE_ACCOUNT_ID_SILENT ); jest.spyOn( - NativeInteractionClient.prototype, + PlatformAuthInteractionClient.prototype, "acquireToken" ).mockResolvedValue(testTokenResponse); jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ @@ -727,8 +733,8 @@ describe("SilentIframeClient", () => { cloudGraphHostName: "", }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -779,8 +785,8 @@ describe("SilentIframeClient", () => { status: 200, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -988,8 +994,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -1098,8 +1104,8 @@ describe("SilentIframeClient", () => { tokenType: AuthenticationScheme.BEARER, }; jest.spyOn( - AuthorizationCodeClient.prototype, - "getAuthCodeUrl" + AuthorizeProtocol, + "getAuthCodeRequestUrl" ).mockResolvedValue(testNavUrl); jest.spyOn(SilentHandler, "monitorIframeForHash").mockResolvedValue( TEST_HASHES.TEST_SUCCESS_CODE_HASH_SILENT @@ -1236,6 +1242,57 @@ describe("SilentIframeClient", () => { expect(tokenKeys.refreshToken).toHaveLength(0); }); }); + + describe("EAR Flow Tests", () => { + beforeAll(() => { + jest.useFakeTimers(); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + beforeEach(async () => { + pca = new PublicClientApplication({ + auth: { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + protocolMode: ProtocolMode.EAR, + }, + }); + await pca.initialize(); + + jest.spyOn(BrowserCrypto, "generateEarKey").mockResolvedValue( + validEarJWK + ); + }); + + it("Invokes EAR flow when protocolMode is set to EAR", async () => { + const validRequest: SsoSilentRequest = { + authority: TEST_CONFIG.validAuthority, + scopes: ["openid", "profile", "offline_access"], + correlationId: TEST_CONFIG.CORRELATION_ID, + redirectUri: window.location.href, + state: TEST_STATE_VALUES.USER_STATE, + nonce: ID_TOKEN_CLAIMS.nonce, + }; + jest.spyOn(ProtocolUtils, "setRequestState").mockReturnValue( + TEST_STATE_VALUES.TEST_STATE_SILENT + ); + const earFormSpy = jest + .spyOn(SilentHandler, "initiateEarRequest") + .mockResolvedValue(document.createElement("iframe")); + jest.spyOn( + SilentHandler, + "monitorIframeForHash" + ).mockResolvedValue( + `#ear_jwe=${validEarJWE}&state=${TEST_STATE_VALUES.TEST_STATE_SILENT}` + ); + + const result = await pca.ssoSilent(validRequest); + expect(result).toEqual(getTestAuthenticationResult()); + expect(earFormSpy).toHaveBeenCalled(); + }); + }); }); describe("logout", () => { diff --git a/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts b/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts index 204e3e0b56..d532af086e 100644 --- a/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts +++ b/lib/msal-browser/test/interaction_client/StandardInteractionClient.spec.ts @@ -26,6 +26,7 @@ import { TEST_REQ_CNF_DATA, ID_TOKEN_CLAIMS, TEST_TOKENS, + RANDOM_TEST_GUID, } from "../utils/StringConstants.js"; import { AuthorizationUrlRequest } from "../../src/request/AuthorizationUrlRequest.js"; import { RedirectRequest } from "../../src/request/RedirectRequest.js"; @@ -39,10 +40,6 @@ class testStandardInteractionClient extends StandardInteractionClient { return Promise.resolve(); } - async initializeAuthorizationCodeRequest(request: AuthorizationUrlRequest) { - return super.initializeAuthorizationCodeRequest(request); - } - async initializeAuthorizationRequest( request: RedirectRequest, interactionType: InteractionType @@ -58,7 +55,7 @@ class testStandardInteractionClient extends StandardInteractionClient { } logout(request: EndSessionRequest): Promise { - return this.clearCacheOnLogout(request.account); + return this.clearCacheOnLogout(RANDOM_TEST_GUID, request.account); } } @@ -127,81 +124,6 @@ describe("StandardInteractionClient", () => { jest.restoreAllMocks(); }); - it("initializeAuthorizationCodeRequest", async () => { - const request: AuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: ["scope"], - loginHint: "AbeLi@microsoft.com", - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - }; - - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - - const authCodeRequest = - await testClient.initializeAuthorizationCodeRequest(request); - expect(request.codeChallenge).toBe(TEST_CONFIG.TEST_CHALLENGE); - expect(authCodeRequest.codeVerifier).toBe(TEST_CONFIG.TEST_VERIFIER); - expect(authCodeRequest.popKid).toBeUndefined; - }); - - it("initializeAuthorizationCodeRequest validates the request and does not influence undefined popKid param", async () => { - const request: AuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: ["scope"], - loginHint: "AbeLi@microsoft.com", - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - }; - - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - - const authCodeRequest = - await testClient.initializeAuthorizationCodeRequest(request); - expect(authCodeRequest.popKid).toBeUndefined; - }); - - it("initializeAuthorizationCodeRequest validates the request and adds reqCnf param when user defined", async () => { - const request: AuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: ["scope"], - loginHint: "AbeLi@microsoft.com", - state: TEST_STATE_VALUES.USER_STATE, - authority: TEST_CONFIG.validAuthority, - correlationId: TEST_CONFIG.CORRELATION_ID, - responseMode: TEST_CONFIG.RESPONSE_MODE as ResponseMode, - nonce: "", - authenticationScheme: - TEST_CONFIG.TOKEN_TYPE_BEARER as AuthenticationScheme, - popKid: TEST_REQ_CNF_DATA.kid, - }; - - jest.spyOn(PkceGenerator, "generatePkceCodes").mockResolvedValue({ - challenge: TEST_CONFIG.TEST_CHALLENGE, - verifier: TEST_CONFIG.TEST_VERIFIER, - }); - - const authCodeRequest = - await testClient.initializeAuthorizationCodeRequest(request); - expect(authCodeRequest.popKid).toEqual(TEST_REQ_CNF_DATA.kid); - }); - it("getDiscoveredAuthority - request authority only", async () => { const requestAuthority = TEST_CONFIG.validAuthority; diff --git a/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts b/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts index 3ff4bc34f6..c46fca2a05 100644 --- a/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts +++ b/lib/msal-browser/test/interaction_handler/InteractionHandler.spec.ts @@ -34,23 +34,14 @@ import { TEST_DATA_CLIENT_INFO, TEST_TOKENS, TEST_TOKEN_LIFETIMES, - TEST_HASHES, TEST_POP_VALUES, TEST_STATE_VALUES, RANDOM_TEST_GUID, TEST_CRYPTO_VALUES, } from "../utils/StringConstants.js"; -import { - createBrowserAuthError, - BrowserAuthErrorCodes, -} from "../../src/error/BrowserAuthError.js"; import { CryptoOps } from "../../src/crypto/CryptoOps.js"; import { TestStorageManager } from "../cache/TestStorageManager.js"; import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; -import { - TemporaryCacheKeys, - BrowserConstants, -} from "../../src/utils/BrowserConstants.js"; import { EventHandler } from "../../src/event/EventHandler.js"; import { TestTimeUtils } from "msal-test-utils"; @@ -148,8 +139,8 @@ const cryptoInterface = { signJwt: async (): Promise => { return "signedJwt"; }, - removeTokenBindingKey: async (): Promise => { - return Promise.resolve(true); + removeTokenBindingKey: async (): Promise => { + return Promise.resolve(); }, clearKeystore: async (): Promise => { return Promise.resolve(true); @@ -231,7 +222,8 @@ describe("InteractionHandler.ts Unit Tests", () => { storageInterface: new TestStorageManager( TEST_CONFIG.MSAL_CLIENT_ID, cryptoInterface, - logger + logger, + new StubPerformanceClient() ), networkInterface: { sendGetRequestAsync: async ( @@ -313,22 +305,6 @@ describe("InteractionHandler.ts Unit Tests", () => { tokenType: AuthenticationScheme.BEARER, }; testAuthCodeRequest.ccsCredential = testCcsCred; - browserStorage.setTemporaryCache( - browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ), - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - browserStorage.setTemporaryCache( - browserStorage.generateNonceKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ), - idTokenClaims.nonce - ); - browserStorage.setTemporaryCache( - TemporaryCacheKeys.CCS_CREDENTIAL, - CcsCredentialType.UPN - ); const acquireTokenSpy = jest .spyOn(AuthorizationCodeClient.prototype, "acquireToken") .mockResolvedValue(testTokenResponse); @@ -405,22 +381,6 @@ describe("InteractionHandler.ts Unit Tests", () => { state: "testState", tokenType: AuthenticationScheme.BEARER, }; - browserStorage.setTemporaryCache( - browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ), - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - browserStorage.setTemporaryCache( - browserStorage.generateNonceKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ), - idTokenClaims.nonce - ); - jest.spyOn( - AuthorizationCodeClient.prototype, - "handleFragmentResponse" - ).mockReturnValue(testCodeResponse); const updateAuthoritySpy = jest.spyOn( AuthorizationCodeClient.prototype, "updateAuthority" @@ -434,10 +394,7 @@ describe("InteractionHandler.ts Unit Tests", () => { ); await interactionHandler.initiateAuthRequest("testNavUrl"); const tokenResponse = await interactionHandler.handleCodeResponse( - { - code: "authCode", - state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, - }, + testCodeResponse, { authority: TEST_CONFIG.validAuthority, scopes: ["User.Read"], @@ -506,26 +463,6 @@ describe("InteractionHandler.ts Unit Tests", () => { tokenType: AuthenticationScheme.BEARER, }; testAuthCodeRequest.ccsCredential = testCcsCred; - browserStorage.setTemporaryCache( - browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ), - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - browserStorage.setTemporaryCache( - browserStorage.generateNonceKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ), - idTokenClaims.nonce - ); - browserStorage.setTemporaryCache( - TemporaryCacheKeys.CCS_CREDENTIAL, - CcsCredentialType.UPN - ); - jest.spyOn( - AuthorizationCodeClient.prototype, - "handleFragmentResponse" - ).mockReturnValue(testCodeResponse); const acquireTokenSpy = jest .spyOn(AuthorizationCodeClient.prototype, "acquireToken") .mockResolvedValue(testTokenResponse); @@ -535,10 +472,7 @@ describe("InteractionHandler.ts Unit Tests", () => { ); await interactionHandler.initiateAuthRequest("testNavUrl"); const tokenResponse = await interactionHandler.handleCodeResponse( - { - code: "authCode", - state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, - }, + testCodeResponse, { authority: TEST_CONFIG.validAuthority, scopes: ["User.Read"], diff --git a/lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts b/lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts deleted file mode 100644 index 6ad8f15f19..0000000000 --- a/lib/msal-browser/test/interaction_handler/RedirectHandler.spec.ts +++ /dev/null @@ -1,576 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { - PkceCodes, - NetworkRequestOptions, - AccountInfo, - AuthorityFactory, - CommonAuthorizationCodeRequest, - Constants, - AuthenticationResult, - AuthorizationCodeClient, - AuthenticationScheme, - ProtocolMode, - Logger, - LoggerOptions, - Authority, - ClientConfiguration, - AuthorizationCodePayload, - AuthorityOptions, - CcsCredential, - CcsCredentialType, - IPerformanceClient, - StubPerformanceClient, -} from "@azure/msal-common/browser"; -import { - Configuration, - buildConfiguration, -} from "../../src/config/Configuration.js"; -import { - TEST_CONFIG, - TEST_URIS, - TEST_TOKENS, - TEST_DATA_CLIENT_INFO, - RANDOM_TEST_GUID, - TEST_HASHES, - TEST_TOKEN_LIFETIMES, - TEST_STATE_VALUES, -} from "../utils/StringConstants.js"; -import { RedirectHandler } from "../../src/interaction_handler/RedirectHandler.js"; -import { - BrowserAuthErrorMessage, - BrowserAuthError, -} from "../../src/error/BrowserAuthError.js"; -import { TemporaryCacheKeys } from "../../src/utils/BrowserConstants.js"; -import { CryptoOps } from "../../src/crypto/CryptoOps.js"; -import { DatabaseStorage } from "../../src/cache/DatabaseStorage.js"; -import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; -import { NavigationClient } from "../../src/navigation/NavigationClient.js"; -import { NavigationOptions } from "../../src/navigation/NavigationOptions.js"; -import { RedirectRequest } from "../../src/request/RedirectRequest.js"; -import { EventHandler } from "../../src/event/EventHandler.js"; -import { TestTimeUtils } from "msal-test-utils"; - -const testPkceCodes = { - challenge: "TestChallenge", - verifier: "TestVerifier", -} as PkceCodes; - -const defaultTokenRequest: CommonAuthorizationCodeRequest = { - authenticationScheme: AuthenticationScheme.BEARER, - redirectUri: `${TEST_URIS.DEFAULT_INSTANCE}/`, - code: "thisIsATestCode", - scopes: TEST_CONFIG.DEFAULT_SCOPES, - codeVerifier: TEST_CONFIG.TEST_VERIFIER, - authority: `${Constants.DEFAULT_AUTHORITY}/`, - correlationId: RANDOM_TEST_GUID, -}; - -const testNetworkResult = { - testParam: "testValue", -}; - -let browserCrypto: CryptoOps; - -const networkInterface = { - sendGetRequestAsync(): T { - return {} as T; - }, - sendPostRequestAsync(): T { - return {} as T; - }, -}; - -let authorityInstance: Authority; -let authConfig: ClientConfiguration; - -describe("RedirectHandler.ts Unit Tests", () => { - let authCodeModule: AuthorizationCodeClient; - let browserRequestLogger: Logger; - let browserStorage: BrowserCacheManager; - let performanceClient: IPerformanceClient; - - beforeEach(() => { - const appConfig: Configuration = { - auth: { - clientId: TEST_CONFIG.MSAL_CLIENT_ID, - }, - }; - const configObj = buildConfiguration(appConfig, true); - const authorityOptions: AuthorityOptions = { - protocolMode: ProtocolMode.AAD, - knownAuthorities: [], - cloudDiscoveryMetadata: "", - authorityMetadata: "", - }; - const loggerOptions: LoggerOptions = { - loggerCallback: (): void => {}, - piiLoggingEnabled: true, - }; - const logger: Logger = new Logger(loggerOptions); - browserCrypto = new CryptoOps(logger); - browserStorage = new BrowserCacheManager( - TEST_CONFIG.MSAL_CLIENT_ID, - configObj.cache, - browserCrypto, - logger, - new StubPerformanceClient(), - new EventHandler() - ); - // Initialize authority after browser storage for proper use - authorityInstance = new Authority( - configObj.auth.authority, - networkInterface, - browserStorage, - authorityOptions, - logger, - TEST_CONFIG.CORRELATION_ID - ); - authConfig = { - authOptions: { - ...configObj.auth, - authority: authorityInstance, - }, - systemOptions: { - tokenRenewalOffsetSeconds: - configObj.system.tokenRenewalOffsetSeconds, - }, - cryptoInterface: new CryptoOps(new Logger({})), - storageInterface: browserStorage, - networkInterface: { - sendGetRequestAsync: async ( - url: string, - options?: NetworkRequestOptions - ): Promise => { - return testNetworkResult; - }, - sendPostRequestAsync: async ( - url: string, - options?: NetworkRequestOptions - ): Promise => { - return testNetworkResult; - }, - }, - loggerOptions: loggerOptions, - }; - authCodeModule = new AuthorizationCodeClient(authConfig); - browserRequestLogger = new Logger(authConfig.loggerOptions!); - performanceClient = { - startMeasurement: jest.fn(), - endMeasurement: jest.fn(), - discardMeasurements: jest.fn(), - removePerformanceCallback: jest.fn(), - addPerformanceCallback: jest.fn(), - emitEvents: jest.fn(), - startPerformanceMeasurement: jest.fn(), - generateId: jest.fn(), - calculateQueuedTime: jest.fn(), - addQueueMeasurement: jest.fn(), - setPreQueueTime: jest.fn(), - addFields: jest.fn(), - incrementFields: jest.fn(), - }; - }); - - afterEach(() => { - jest.restoreAllMocks(); - browserStorage.clear(); - }); - - describe("Constructor", () => { - it("creates a subclass of InteractionHandler called RedirectHandler", () => { - const redirectHandler = new RedirectHandler( - authCodeModule, - browserStorage, - defaultTokenRequest, - browserRequestLogger, - performanceClient - ); - expect(redirectHandler).toBeInstanceOf(RedirectHandler); - }); - }); - - describe("initiateAuthRequest()", () => { - it("throws error if requestUrl is empty", (done) => { - const navigationClient = new NavigationClient(); - const redirectHandler = new RedirectHandler( - authCodeModule, - browserStorage, - defaultTokenRequest, - browserRequestLogger, - performanceClient - ); - - redirectHandler - .initiateAuthRequest("", { - redirectTimeout: 3000, - redirectStartPage: "", - navigationClient, - }) - .catch((e) => { - expect(e).toBeInstanceOf(BrowserAuthError); - expect(e.errorCode).toEqual( - BrowserAuthErrorMessage.emptyNavigateUriError.code - ); - expect(e.errorMessage).toEqual( - BrowserAuthErrorMessage.emptyNavigateUriError.desc - ); - done(); - }); - }); - - it("navigates browser window to given window location", (done) => { - let dbStorage = {}; - jest.spyOn(DatabaseStorage.prototype, "open").mockImplementation( - async (): Promise => { - dbStorage = {}; - } - ); - const navigationClient = new NavigationClient(); - navigationClient.navigateExternal = ( - requestUrl: string, - options: NavigationOptions - ): Promise => { - expect(requestUrl).toEqual(TEST_URIS.TEST_ALTERNATE_REDIR_URI); - expect(options.timeout).toEqual(3000); - done(); - return Promise.resolve(true); - }; - const redirectHandler = new RedirectHandler( - authCodeModule, - browserStorage, - defaultTokenRequest, - browserRequestLogger, - performanceClient - ); - redirectHandler.initiateAuthRequest( - TEST_URIS.TEST_ALTERNATE_REDIR_URI, - { - redirectStartPage: "", - redirectTimeout: 3000, - navigationClient, - } - ); - }); - - it("doesnt navigate if onRedirectNavigate returns false", (done) => { - let dbStorage = {}; - jest.spyOn(DatabaseStorage.prototype, "open").mockImplementation( - async (): Promise => { - dbStorage = {}; - } - ); - const navigationClient = new NavigationClient(); - navigationClient.navigateExternal = ( - urlNavigate: string, - options: NavigationOptions - ): Promise => { - done( - "Navigatation should not happen if onRedirectNavigate returns false" - ); - return Promise.reject(); - }; - - const onRedirectNavigate = (url: string) => { - expect(url).toEqual(TEST_URIS.TEST_ALTERNATE_REDIR_URI); - done(); - return false; - }; - const redirectHandler = new RedirectHandler( - authCodeModule, - browserStorage, - defaultTokenRequest, - browserRequestLogger, - performanceClient - ); - redirectHandler.initiateAuthRequest( - TEST_URIS.TEST_ALTERNATE_REDIR_URI, - { - redirectTimeout: 300, - redirectStartPage: "", - onRedirectNavigate, - navigationClient, - } - ); - }); - - it("navigates if onRedirectNavigate doesnt return false", (done) => { - let dbStorage = {}; - jest.spyOn(DatabaseStorage.prototype, "open").mockImplementation( - async (): Promise => { - dbStorage = {}; - } - ); - - const navigationClient = new NavigationClient(); - navigationClient.navigateExternal = ( - requestUrl, - options - ): Promise => { - expect(requestUrl).toEqual(TEST_URIS.TEST_ALTERNATE_REDIR_URI); - done(); - return Promise.resolve(true); - }; - - const onRedirectNavigate = (url: string) => { - expect(url).toEqual(TEST_URIS.TEST_ALTERNATE_REDIR_URI); - }; - const redirectHandler = new RedirectHandler( - authCodeModule, - browserStorage, - defaultTokenRequest, - browserRequestLogger, - performanceClient - ); - redirectHandler.initiateAuthRequest( - TEST_URIS.TEST_ALTERNATE_REDIR_URI, - { - redirectTimeout: 3000, - redirectStartPage: "", - onRedirectNavigate, - navigationClient, - } - ); - }); - }); - - describe("handleCodeResponse()", () => { - it("successfully handles response", async () => { - const idTokenClaims = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: "1536361411", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; - const testCodeResponse: AuthorizationCodePayload = { - code: "authcode", - nonce: idTokenClaims.nonce, - state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, - }; - const testAccount: AccountInfo = { - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID_ENCODED, - environment: "login.windows.net", - tenantId: idTokenClaims.tid, - username: idTokenClaims.preferred_username, - }; - const testTokenResponse: AuthenticationResult = { - authority: authorityInstance.canonicalAuthority, - accessToken: TEST_TOKENS.ACCESS_TOKEN, - idToken: TEST_TOKENS.IDTOKEN_V2, - fromCache: false, - scopes: ["scope1", "scope2"], - account: testAccount, - correlationId: RANDOM_TEST_GUID, - expiresOn: TestTimeUtils.nowDateWithOffset( - TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN - ), - idTokenClaims: idTokenClaims, - tenantId: idTokenClaims.tid, - uniqueId: idTokenClaims.oid, - tokenType: AuthenticationScheme.BEARER, - }; - let dbStorage = {}; - jest.spyOn(DatabaseStorage.prototype, "open").mockImplementation( - async (): Promise => { - dbStorage = {}; - } - ); - - const testAuthCodeRequest: CommonAuthorizationCodeRequest = { - authenticationScheme: AuthenticationScheme.BEARER, - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: ["scope1", "scope2"], - code: "", - authority: authorityInstance.canonicalAuthority, - correlationId: RANDOM_TEST_GUID, - }; - browserStorage.setTemporaryCache( - browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ), - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - browserStorage.setTemporaryCache( - browserStorage.generateCacheKey( - TemporaryCacheKeys.REQUEST_PARAMS - ), - browserCrypto.base64Encode(JSON.stringify(testAuthCodeRequest)) - ); - browserStorage.setTemporaryCache( - `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, - TEST_CONFIG.MSAL_CLIENT_ID - ); - browserStorage.setTemporaryCache( - browserStorage.generateCacheKey(TemporaryCacheKeys.URL_HASH), - TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT - ); - jest.spyOn( - AuthorizationCodeClient.prototype, - "handleFragmentResponse" - ).mockReturnValue(testCodeResponse); - jest.spyOn( - AuthorizationCodeClient.prototype, - "acquireToken" - ).mockResolvedValue(testTokenResponse); - - const redirectHandler = new RedirectHandler( - authCodeModule, - browserStorage, - testAuthCodeRequest, - browserRequestLogger, - performanceClient - ); - const tokenResponse = await redirectHandler.handleCodeResponse( - { - code: "thisIsATestCode", - state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, - }, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - expect(tokenResponse).toEqual(testTokenResponse); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateCacheKey( - TemporaryCacheKeys.INTERACTION_STATUS_KEY - ) - ) - ).toBe(null); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateCacheKey(TemporaryCacheKeys.URL_HASH) - ) - ).toBe(null); - }); - - it("successfully handles response adds CCS credential to auth code request", async () => { - const idTokenClaims = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: "1536361411", - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - }; - const testCodeResponse: AuthorizationCodePayload = { - code: "authcode", - nonce: idTokenClaims.nonce, - state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, - }; - const testAccount: AccountInfo = { - homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, - localAccountId: TEST_DATA_CLIENT_INFO.TEST_UID_ENCODED, - environment: "login.windows.net", - tenantId: idTokenClaims.tid, - username: idTokenClaims.preferred_username, - }; - const testCcsCred: CcsCredential = { - credential: idTokenClaims.preferred_username || "", - type: CcsCredentialType.UPN, - }; - const testTokenResponse: AuthenticationResult = { - authority: authorityInstance.canonicalAuthority, - accessToken: TEST_TOKENS.ACCESS_TOKEN, - idToken: TEST_TOKENS.IDTOKEN_V2, - fromCache: false, - scopes: ["scope1", "scope2"], - account: testAccount, - correlationId: RANDOM_TEST_GUID, - expiresOn: TestTimeUtils.nowDateWithOffset( - TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN - ), - idTokenClaims: idTokenClaims, - tenantId: idTokenClaims.tid, - uniqueId: idTokenClaims.oid, - tokenType: AuthenticationScheme.BEARER, - }; - let dbStorage = {}; - jest.spyOn(DatabaseStorage.prototype, "open").mockImplementation( - async (): Promise => { - dbStorage = {}; - } - ); - - const testAuthCodeRequest: CommonAuthorizationCodeRequest = { - authenticationScheme: AuthenticationScheme.BEARER, - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: ["scope1", "scope2"], - code: "", - authority: authorityInstance.canonicalAuthority, - correlationId: RANDOM_TEST_GUID, - ccsCredential: testCcsCred, - }; - browserStorage.setTemporaryCache( - browserStorage.generateStateKey( - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ), - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - browserStorage.setTemporaryCache( - browserStorage.generateCacheKey( - TemporaryCacheKeys.REQUEST_PARAMS - ), - browserCrypto.base64Encode(JSON.stringify(testAuthCodeRequest)) - ); - browserStorage.setTemporaryCache( - `${Constants.CACHE_PREFIX}.${TemporaryCacheKeys.INTERACTION_STATUS_KEY}`, - TEST_CONFIG.MSAL_CLIENT_ID - ); - browserStorage.setTemporaryCache( - browserStorage.generateCacheKey(TemporaryCacheKeys.URL_HASH), - TEST_HASHES.TEST_SUCCESS_CODE_HASH_REDIRECT - ); - browserStorage.setTemporaryCache( - TemporaryCacheKeys.CCS_CREDENTIAL, - JSON.stringify(testCcsCred) - ); - jest.spyOn( - AuthorizationCodeClient.prototype, - "handleFragmentResponse" - ).mockReturnValue(testCodeResponse); - jest.spyOn( - AuthorizationCodeClient.prototype, - "acquireToken" - ).mockResolvedValue(testTokenResponse); - - const redirectHandler = new RedirectHandler( - authCodeModule, - browserStorage, - testAuthCodeRequest, - browserRequestLogger, - performanceClient - ); - const tokenResponse = await redirectHandler.handleCodeResponse( - { - code: "thisIsATestCode", - state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, - }, - TEST_STATE_VALUES.TEST_STATE_REDIRECT - ); - expect(tokenResponse).toEqual(testTokenResponse); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateCacheKey( - TemporaryCacheKeys.INTERACTION_STATUS_KEY - ) - ) - ).toBe(null); - expect( - browserStorage.getTemporaryCache( - browserStorage.generateCacheKey(TemporaryCacheKeys.URL_HASH) - ) - ).toBe(null); - }); - }); -}); diff --git a/lib/msal-browser/test/interaction_handler/SilentHandler.spec.ts b/lib/msal-browser/test/interaction_handler/SilentHandler.spec.ts index b8f2f4eb5e..6629120c5b 100644 --- a/lib/msal-browser/test/interaction_handler/SilentHandler.spec.ts +++ b/lib/msal-browser/test/interaction_handler/SilentHandler.spec.ts @@ -9,13 +9,13 @@ import { IPerformanceClient, ServerResponseType, } from "@azure/msal-common"; -import * as SilentHandler from "../../src/interaction_handler/SilentHandler"; -import { testNavUrl, RANDOM_TEST_GUID } from "../utils/StringConstants"; +import * as SilentHandler from "../../src/interaction_handler/SilentHandler.js"; +import { testNavUrl, RANDOM_TEST_GUID } from "../utils/StringConstants.js"; import { BrowserAuthError, createBrowserAuthError, BrowserAuthErrorCodes, -} from "../../src/error/BrowserAuthError"; +} from "../../src/error/BrowserAuthError.js"; const DEFAULT_IFRAME_TIMEOUT_MS = 6000; const DEFAULT_POLL_INTERVAL_MS = 30; @@ -54,7 +54,7 @@ describe("SilentHandler.ts Unit Tests", () => { describe("initiateAuthRequest()", () => { it("throws error if requestUrl is empty", async () => { await expect( - SilentHandler.initiateAuthRequest( + SilentHandler.initiateCodeRequest( "", performanceClient, browserRequestLogger, @@ -69,7 +69,7 @@ describe("SilentHandler.ts Unit Tests", () => { "Creates a frame asynchronously when created with default timeout", async () => { const startTime = Date.now(); - const authFrame = await SilentHandler.initiateAuthRequest( + const authFrame = await SilentHandler.initiateCodeRequest( testNavUrl, performanceClient, browserRequestLogger, @@ -87,7 +87,7 @@ describe("SilentHandler.ts Unit Tests", () => { it("Creates a frame synchronously when created with a timeout of 0", async () => { const startTime = Date.now(); - const authFrame = await SilentHandler.initiateAuthRequest( + const authFrame = await SilentHandler.initiateCodeRequest( testNavUrl, performanceClient, browserRequestLogger, diff --git a/lib/msal-browser/test/naa/JSRuntime.spec.ts b/lib/msal-browser/test/naa/JSRuntime.spec.ts index a66238a5ec..f6e84d209e 100644 --- a/lib/msal-browser/test/naa/JSRuntime.spec.ts +++ b/lib/msal-browser/test/naa/JSRuntime.spec.ts @@ -17,6 +17,7 @@ import { createNestablePublicClientApplication, } from "../../src/index.js"; import { InteractionRequiredAuthError } from "@azure/msal-common"; +import { BridgeRequestEnvelope } from "../../src/naa/BridgeRequestEnvelope.js"; /** * Tests Nested App Auth for JS Runtime environment @@ -105,6 +106,22 @@ describe("JS Runtime Nested App Auth", () => { expect(authResult.accessToken).toBe(TEST_TOKENS.ACCESS_TOKEN); } + // Validate claims parameter + mockBridge.addAuthResultResponse("GetToken", SILENT_TOKEN_RESPONSE); + { + const claims = `{"access_token":{"nbf":{"essential":true,"value":"1752302923"}}}`; + const authResult = await pca.acquireTokenSilent({ + scopes: ["User.Read"], + claims, + }); + expect(authResult.fromCache).toBe(false); + expect(authResult.accessToken).toBe(TEST_TOKENS.ACCESS_TOKEN); + const bridgeRequest = JSON.parse( + mockBridge.getBridgeRequests().at(-1)! + ) as BridgeRequestEnvelope; + expect(bridgeRequest.tokenParams?.claims).toBe(claims); + } + // Validate error scenario mockBridge.addErrorResponse( "GetToken", diff --git a/lib/msal-browser/test/naa/MockBridge.ts b/lib/msal-browser/test/naa/MockBridge.ts index a9f0ba3f04..dc5db3453c 100644 --- a/lib/msal-browser/test/naa/MockBridge.ts +++ b/lib/msal-browser/test/naa/MockBridge.ts @@ -12,6 +12,7 @@ import { BridgeResponseEnvelope } from "../../src/naa/BridgeResponseEnvelope.js" import { InitContext } from "../../src/naa/InitContext.js"; export class MockBridge implements AuthBridge { + private bridgeRequests: string[] = []; private listeners: { [key: string]: ((response: string) => void)[] } = {}; private responses: { [key: string]: Partial[]; @@ -28,6 +29,7 @@ export class MockBridge implements AuthBridge { } public postMessage(message: string): void { + this.bridgeRequests.push(message); const request = JSON.parse(message); if (isBridgeRequestEnvelope(request)) { const response = this.responses[request.method].pop(); @@ -94,6 +96,10 @@ export class MockBridge implements AuthBridge { public removeAllResponses(): void { this.responses = {}; } + + public getBridgeRequests(): string[] { + return this.bridgeRequests; + } } export default MockBridge; diff --git a/lib/msal-browser/test/navigation/NavigationClient.spec.ts b/lib/msal-browser/test/navigation/NavigationClient.spec.ts index 4c862302bc..e57ed28bef 100644 --- a/lib/msal-browser/test/navigation/NavigationClient.spec.ts +++ b/lib/msal-browser/test/navigation/NavigationClient.spec.ts @@ -3,8 +3,11 @@ * Licensed under the MIT License. */ +import { create } from "domain"; import { NavigationClient } from "../../src/navigation/NavigationClient.js"; import { TEST_URIS } from "../utils/StringConstants.js"; +import { BrowserAuthError, BrowserAuthErrorCodes } from "../../src/index.js"; +import { createBrowserAuthError } from "../../src/error/BrowserAuthError.js"; describe("NavigationClient.ts Unit Tests", () => { const navigationClient = new NavigationClient(); @@ -22,6 +25,7 @@ describe("NavigationClient.ts Unit Tests", () => { afterEach(() => { window = oldWindow; jest.restoreAllMocks(); + jest.useRealTimers(); }); describe("navigateInternal tests", () => { @@ -118,22 +122,32 @@ describe("NavigationClient.ts Unit Tests", () => { expect(windowReplaceSpy).toHaveBeenCalledTimes(1); }); - it("navigateExternal() logs if navigation does not take place within 30 seconds", (done) => { + it("navigateExternal() throws if navigation does not take place within specified timeout", (done) => { + jest.useRealTimers(); window.location = { ...oldWindowLocation, replace: function (url: string) { - expect(url).toBe(TEST_URIS.TEST_LOGOUT_URI); - done(); + // Do nothing }, }; const windowReplaceSpy = jest.spyOn(window.location, "replace"); - navigationClient.navigateExternal(TEST_URIS.TEST_LOGOUT_URI, { - timeout: 30000, - noHistory: true, - //@ts-ignore - apiId: 0, - }); + navigationClient + .navigateExternal(TEST_URIS.TEST_LOGOUT_URI, { + timeout: 100, + noHistory: true, + //@ts-ignore + apiId: 0, + }) + .catch((error) => { + expect(error).toEqual( + createBrowserAuthError( + BrowserAuthErrorCodes.timedOut, + "failed_to_redirect" + ) + ); + done(); + }); expect(windowReplaceSpy).toHaveBeenCalledTimes(1); }); }); diff --git a/lib/msal-browser/test/network/FetchClient.spec.ts b/lib/msal-browser/test/network/FetchClient.spec.ts index c7a024126d..c91abb9646 100644 --- a/lib/msal-browser/test/network/FetchClient.spec.ts +++ b/lib/msal-browser/test/network/FetchClient.spec.ts @@ -5,10 +5,7 @@ import { NetworkError, NetworkRequestOptions, } from "@azure/msal-common"; -import { - BrowserAuthErrorMessage, - BrowserAuthError, -} from "../../src/error/BrowserAuthError"; +import { BrowserAuthErrorMessage } from "../../src/error/BrowserAuthError"; const mockResponse: Response = { headers: new Headers(), @@ -140,10 +137,11 @@ describe("FetchClient.ts Unit Tests", () => { fetchClient .sendPostRequestAsync(targetUri, requestOptions) .catch((e) => { - expect(e).toBeInstanceOf(BrowserAuthError); - expect(e.errorCode).toBe( + expect(e).toBeInstanceOf(NetworkError); + expect(e.errorCode).toContain( BrowserAuthErrorMessage.postRequestFailed.code ); + expect(e.errorMessage).toContain(`additionalErrorInfo:`); done(); }); }); @@ -161,10 +159,11 @@ describe("FetchClient.ts Unit Tests", () => { ); fetchClient.sendGetRequestAsync(targetUri).catch((e) => { - expect(e).toBeInstanceOf(BrowserAuthError); + expect(e).toBeInstanceOf(NetworkError); expect(e.errorCode).toBe( BrowserAuthErrorMessage.getRequestFailed.code ); + expect(e.errorMessage).toContain(`additionalErrorInfo:`); done(); }); }); @@ -195,11 +194,11 @@ describe("FetchClient.ts Unit Tests", () => { .sendPostRequestAsync(targetUri, requestOptions) .catch((e) => { expect(e).toBeInstanceOf(NetworkError); - expect(e.error).toBeInstanceOf(BrowserAuthError); - expect(e.errorCode).toBe( + expect(e.errorCode).toContain( BrowserAuthErrorMessage.failedToParseNetworkResponse .code ); + expect(e.errorMessage).toContain(`additionalErrorInfo:`); done(); }); }); @@ -235,10 +234,11 @@ describe("FetchClient.ts Unit Tests", () => { fetchClient .sendPostRequestAsync(targetUri, requestOptions) .catch((e) => { - expect(e).toBeInstanceOf(BrowserAuthError); + expect(e).toBeInstanceOf(NetworkError); expect(e.errorCode).toBe( BrowserAuthErrorMessage.noNetworkConnectivity.code ); + expect(e.errorMessage).toContain(`additionalErrorInfo:`); done(); }); }); diff --git a/lib/msal-browser/test/protocol/Authorize.spec.ts b/lib/msal-browser/test/protocol/Authorize.spec.ts new file mode 100644 index 0000000000..f5878c4255 --- /dev/null +++ b/lib/msal-browser/test/protocol/Authorize.spec.ts @@ -0,0 +1,376 @@ +import { + AADServerParamKeys, + Authority, + AuthorityFactory, + AuthorityOptions, + ClientAuthError, + ClientAuthErrorCodes, + CommonAuthorizationUrlRequest, + InteractionRequiredAuthError, + Logger, + OAuthResponseType, + ProtocolMode, + ResponseMode, + StubPerformanceClient, +} from "@azure/msal-common/browser"; +import * as Authorize from "../../src/protocol/Authorize.js"; +import { buildConfiguration } from "../../src/config/Configuration.js"; +import { + generateValidEarJWE, + ID_TOKEN_CLAIMS, + getTestAuthenticationResult, + TEST_CONFIG, + TEST_STATE_VALUES, + validEarJWE, + validEarJWK, +} from "../utils/StringConstants.js"; +import { BrowserCacheManager } from "../../src/cache/BrowserCacheManager.js"; +import { CryptoOps } from "../../src/crypto/CryptoOps.js"; +import { EventHandler } from "../../src/event/EventHandler.js"; +import { ApiId, BrowserConstants } from "../../src/utils/BrowserConstants.js"; +import { version } from "../../src/packageMetadata.js"; +import { + BrowserAuthError, + BrowserAuthErrorCodes, +} from "../../src/error/BrowserAuthError.js"; +import { PlatformAuthExtensionHandler } from "../../src/broker/nativeBroker/PlatformAuthExtensionHandler.js"; +import { PlatformAuthInteractionClient } from "../../src/interaction_client/PlatformAuthInteractionClient.js"; + +describe("Authorize Protocol Tests", () => { + describe("EAR Protocol Tests", () => { + const config = buildConfiguration( + { auth: { clientId: TEST_CONFIG.MSAL_CLIENT_ID } }, + true + ); + const logger = new Logger({}); + const performanceClient = new StubPerformanceClient(); + const authorityOptions: AuthorityOptions = { + protocolMode: ProtocolMode.EAR, + knownAuthorities: [], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + }; + const eventHandler = new EventHandler(); + const cacheManager = new BrowserCacheManager( + TEST_CONFIG.MSAL_CLIENT_ID, + config.cache, + new CryptoOps(logger, performanceClient), + logger, + performanceClient, + eventHandler + ); + let authority: Authority; + const validRequest: CommonAuthorizationUrlRequest = { + authority: TEST_CONFIG.validAuthority, + scopes: ["openid", "profile", "offline_access"], + correlationId: TEST_CONFIG.CORRELATION_ID, + redirectUri: window.location.href, + state: TEST_STATE_VALUES.TEST_STATE_REDIRECT, + nonce: ID_TOKEN_CLAIMS.nonce, + responseMode: ResponseMode.FRAGMENT, + earJwk: validEarJWK, + extraQueryParameters: { + extraKey1: "extraVal1", + extraKey2: "extraVal2", + }, + }; + + beforeAll(async () => { + jest.useFakeTimers(); + authority = await AuthorityFactory.createDiscoveredInstance( + TEST_CONFIG.validAuthority, + config.system.networkClient, + cacheManager, + authorityOptions, + logger, + TEST_CONFIG.CORRELATION_ID, + performanceClient + ); + }); + + afterAll(() => { + jest.useRealTimers(); + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + + describe("getEARForm tests", () => { + it("Throws if earJwk is empty", async () => { + const { earJwk, ...request }: CommonAuthorizationUrlRequest = + validRequest; + + try { + await Authorize.getEARForm( + document, + config, + authority, + request, + logger, + performanceClient + ); + throw "Unexpected! Should throw"; + } catch (e) { + expect(e).toBeInstanceOf(BrowserAuthError); + expect((e as BrowserAuthError).errorCode).toBe( + BrowserAuthErrorCodes.earJwkEmpty + ); + } + }); + + it("Returns HTMLFormElement", async () => { + const form = await Authorize.getEARForm( + document, + config, + authority, + validRequest, + logger, + performanceClient + ); + const checkInputProperties = ( + key: string, + expectedValue: string + ): void => { + expect( + (form.elements.namedItem(key) as HTMLInputElement).value + ).toEqual(expectedValue); + expect( + (form.elements.namedItem(key) as HTMLInputElement) + .hidden + ).toEqual(true); + }; + checkInputProperties( + AADServerParamKeys.CLIENT_ID, + TEST_CONFIG.MSAL_CLIENT_ID + ); + checkInputProperties( + AADServerParamKeys.REDIRECT_URI, + validRequest.redirectUri + ); + checkInputProperties( + AADServerParamKeys.SCOPE, + validRequest.scopes.join(" ") + ); + checkInputProperties( + AADServerParamKeys.CLIENT_REQUEST_ID, + validRequest.correlationId + ); + checkInputProperties( + AADServerParamKeys.STATE, + validRequest.state + ); + checkInputProperties( + AADServerParamKeys.NONCE, + validRequest.nonce + ); + checkInputProperties( + AADServerParamKeys.RESPONSE_MODE, + validRequest.responseMode + ); + checkInputProperties( + AADServerParamKeys.RESPONSE_TYPE, + OAuthResponseType.IDTOKEN_TOKEN_REFRESHTOKEN + ); + checkInputProperties( + AADServerParamKeys.EAR_JWK, + validRequest.earJwk! + ); + checkInputProperties( + AADServerParamKeys.EAR_JWE_CRYPTO, + "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0" + ); + checkInputProperties( + AADServerParamKeys.X_CLIENT_SKU, + BrowserConstants.MSAL_SKU + ); + checkInputProperties(AADServerParamKeys.X_CLIENT_VER, version); + }); + }); + + describe("handleResponseEAR Tests", () => { + const validResponse = { + ear_jwe: validEarJWE, + state: validRequest.state, + }; + + it("Throws if earJWK in request is empty", (done) => { + const { earJwk, ...request }: CommonAuthorizationUrlRequest = + validRequest; + Authorize.handleResponseEAR( + request, + validResponse, + ApiId.acquireTokenPopup, + config, + authority, + cacheManager, + cacheManager, + eventHandler, + logger, + performanceClient + ).catch((e) => { + expect(e).toBeInstanceOf(BrowserAuthError); + expect(e.errorCode).toEqual( + BrowserAuthErrorCodes.earJwkEmpty + ); + done(); + }); + }); + + it("Throws if ear_jwe in server response is empty", (done) => { + const { ear_jwe, ...response } = validResponse; + Authorize.handleResponseEAR( + validRequest, + response, + ApiId.acquireTokenPopup, + config, + authority, + cacheManager, + cacheManager, + eventHandler, + logger, + performanceClient + ).catch((e) => { + expect(e).toBeInstanceOf(BrowserAuthError); + expect(e.errorCode).toEqual( + BrowserAuthErrorCodes.earJweEmpty + ); + done(); + }); + }); + + it("Throws if request state doesn't match response state", (done) => { + const response = { ...validResponse, state: "different-state" }; + Authorize.handleResponseEAR( + validRequest, + response, + ApiId.acquireTokenPopup, + config, + authority, + cacheManager, + cacheManager, + eventHandler, + logger, + performanceClient + ).catch((e) => { + expect(e).toBeInstanceOf(ClientAuthError); + expect(e.errorCode).toEqual( + ClientAuthErrorCodes.stateMismatch + ); + done(); + }); + }); + + it("Throws if response contains an error", (done) => { + const response = { + state: validRequest.state, + error: "interaction_required", + error_description: "Interaction is required", + }; + Authorize.handleResponseEAR( + validRequest, + response, + ApiId.acquireTokenPopup, + config, + authority, + cacheManager, + cacheManager, + eventHandler, + logger, + performanceClient + ).catch((e) => { + expect(e).toBeInstanceOf(InteractionRequiredAuthError); + expect(e.errorCode).toEqual(response.error); + done(); + }); + }); + + it("If decrypted data contains accountId invoke handleResponsePlatformBroker", async () => { + const decryptedServerResponse = { + accountId: "testAccountId", + }; + const jwe = await generateValidEarJWE( + JSON.stringify(decryptedServerResponse), + validEarJWK + ); + const response = { ...validResponse, ear_jwe: jwe }; + + const nativeMessageHandler = new PlatformAuthExtensionHandler( + logger, + 2000, + performanceClient + ); + const platformBrokerSpy = jest + .spyOn( + PlatformAuthInteractionClient.prototype, + "acquireToken" + ) + .mockResolvedValue(getTestAuthenticationResult()); + + const authResult = await Authorize.handleResponseEAR( + validRequest, + response, + ApiId.acquireTokenPopup, + config, + authority, + cacheManager, + cacheManager, + eventHandler, + logger, + performanceClient, + nativeMessageHandler + ); + expect(platformBrokerSpy).toHaveBeenCalled(); + expect(authResult).toEqual(getTestAuthenticationResult()); + }); + + it("If decrypted data contains error, throw it", async () => { + const decryptedServerResponse = { + error: "interaction_required", + error_description: "Interaction is required", + }; + const jwe = await generateValidEarJWE( + JSON.stringify(decryptedServerResponse), + validEarJWK + ); + const response = { ...validResponse, ear_jwe: jwe }; + try { + await Authorize.handleResponseEAR( + validRequest, + response, + ApiId.acquireTokenPopup, + config, + authority, + cacheManager, + cacheManager, + eventHandler, + logger, + performanceClient + ); + throw "This is unexpected! This should throw!"; + } catch (e) { + expect(e).toBeInstanceOf(InteractionRequiredAuthError); + expect( + (e as InteractionRequiredAuthError).errorCode + ).toEqual("interaction_required"); + } + }); + + it("If decrypted data contains successful response cache tokens & account & return AuthenticationResult", async () => { + const response = await Authorize.handleResponseEAR( + validRequest, + validResponse, + ApiId.acquireTokenPopup, + config, + authority, + cacheManager, + cacheManager, + eventHandler, + logger, + performanceClient + ); + expect(response).toEqual(getTestAuthenticationResult()); + }); + }); + }); +}); diff --git a/lib/msal-browser/test/utils/StringConstants.ts b/lib/msal-browser/test/utils/StringConstants.ts index 6109a4ad14..f72d60951d 100644 --- a/lib/msal-browser/test/utils/StringConstants.ts +++ b/lib/msal-browser/test/utils/StringConstants.ts @@ -4,13 +4,18 @@ */ import { + AccountInfo, AuthenticationScheme, Constants, NetworkResponse, OIDC_DEFAULT_SCOPES, ServerAuthorizationTokenResponse, + TimeUtils, } from "@azure/msal-common"; -import { version } from "../../src/packageMetadata"; +import { version } from "../../src/packageMetadata.js"; +import { base64Decode, base64DecToArr } from "../../src/encode/Base64Decode.js"; +import { urlEncodeArr } from "../../src/encode/Base64Encode.js"; +import { AuthenticationResult } from "../../src/response/AuthenticationResult.js"; /** * This file contains the string constants used by the test classes. @@ -139,6 +144,7 @@ export const ID_TOKEN_ALT_CLAIMS = { // Test Expiration Vals export const TEST_TOKEN_LIFETIMES = { DEFAULT_EXPIRES_IN: 3599, + STRING_EXPIRES_IN: "3599", TEST_ID_TOKEN_EXP: 1536361411, TEST_ACCESS_TOKEN_EXP: 1537234948, }; @@ -404,7 +410,7 @@ export const TEST_TOKEN_RESPONSE: NetworkResponse = [], + state?: string +) { + scopes.push("openid", "profile", "offline_access"); // Add default scopes + expect( + url.startsWith( + "https://login.microsoftonline.com/common/oauth2/v2.0/authorize?" + ) + ).toBe(true); + expect( + url.includes( + `client_id=${encodeURIComponent(TEST_CONFIG.MSAL_CLIENT_ID)}` + ) + ).toBe(true); + expect( + url.includes(`redirect_uri=${encodeURIComponent(window.location.href)}`) + ).toBe(true); + expect(url.includes(`scope=${scopes.join("%20")}`)).toBe(true); + expect( + url.includes( + `client-request-id=${encodeURIComponent(RANDOM_TEST_GUID)}` + ) + ); + expect(url.includes(`response_mode=fragment`)).toBe(true); + expect(url.includes("x-client-SKU=msal.js.browser")).toBe(true); + expect(url.includes(`x-client-VER=${version}`)).toBe(true); + expect(url.includes(`x-app-name=${TEST_CONFIG.applicationName}`)).toBe( + true + ); + expect(url.includes(`x-app-ver=${TEST_CONFIG.applicationVersion}`)).toBe( + true + ); + expect(url.includes("client_info=1")).toBe(true); + expect( + url.includes( + "code_challenge=JsjesZmxJwehdhNY9kvyr0QOeSMEvryY_EHZo3BKrqg" + ) + ).toBe(true); + expect(url.includes("code_challenge_method=S256")).toBe(true); + expect(url.includes(`nonce=${encodeURIComponent(RANDOM_TEST_GUID)}`)).toBe( + true + ); + expect( + url.includes(state ? `state=${encodeURIComponent(state)}` : "state=") + ).toBe(true); +} export const testLogoutUrl = `https://login.microsoftonline.com/common/oauth2/v2.0/logout?post_logout_redirect_uri=${encodeURIComponent( `${TEST_URIS.TEST_REDIR_URI}` )}`; + +export const validEarJWK = + "eyJhbGciOiJkaXIiLCJrdHkiOiJvY3QiLCJrIjoieTBPQXJYaUNvdEl5dEIxdnRmMGVUUFZFaEd4SnBQY1AyRUhHUUZaYjFCZyJ9"; +export const validEarJWE = + "eyJlbmMiOiJBMjU2R0NNIiwidHlwIjoiSldUIiwiYWxnIjoiZGlyIn0..TZmjdyAECSgPQNIm.PenCVJ3ULXstdQHr1c2x-KyQB-odDHJc1G6ofNpAhT9VDUVTPa_6rfRWG_csurU5pWTdfBVWZ6kOFk4wjqpotIy-5MMovVHVewrj5dAzsmMEf5NqLy6jqOrie6MgcxqfCyuZvOrFKI9KAhBgKiHkDuhyMN3YzM7jlzS_JCzCsFCrY8oGFcDt4yHSW33BXGWnuov30Ca7W2tGh4qmdTx95UVICDI0jpYk0DOHNAmFoekUgKHJfsJMExUZB8-PKjLAHmdeRwKOrDRLghsDe3O-Y8jpQdqYEJTj6-u59CN0EuBxGzg_D5v6DXVbZ72R17Roi7tytG0jTqmQvHqXGk0CZXYcwj_2sBhC66QnV5eAkV1m2flpsm5eRn9pI-fnKwMt4uwMyw-QkyqR9ERMwCxTr1ikneIPRcOUYGxOEfl7TQtKl5iQ-jO1mXkcfw8f9ZnPDO7G7pgKYis3a1L29ZFOWM2KnV0ZoTd3tOFEHfWc2PGBrgBdNaW8XQmTOrq-KhV66jHsoXHL8J6xeHxesj_5oXW7T2QQoMgFNIlX9dx9X8bvyR3sxTq7yDugEj3BnHDc8JK0RGDgFVO9Ek7hMAcpeNp5F_9ZoAhN3S-6lZtm-jab5J4EHq6ALbLvOYrixKfygq3SfQY6rkDTNB6eVpESFAlRpp47i3DSUh4F5kR8jLqozVKRNUaQfrcl0dI_O_3NaD6uVQGEsmQoUQIQMZ7adSxyj25IAzqtfFVWFqi-cUsOsAieNxZWGsCjbB-A1it1gBbaX7g4op5W1q2FWHPf5DEWgxUJR_PB10t4nGlvyZRJ-D6Dm1JmAh-ZjxCEsr-zO8N4MyYLwUG-jkC0qAXh1VDogWAn1ERlK_8dtvkcpZDjSOstthzNEhmMwOzsc_av3QoJhgxws4ks9t-ZKA_Yhqd-RhgZg7iyN8RzzhTpw5Vy6MhydSyW8TezBES5JlQKMy_lfnB950WPLzA757KNvKgnQxtSNz4rnYNMGKz3_ieoE9KweGmzezGdVDx2zg7WEPpXOiaKKojR9XYjNHBIsXN77G7_nBK--Nagztiw25KqCfyW-E3lL1qFKw4vwdvr3r2hBXDMLdxjm65Ibxdi53OeuOznvgyZDz7oTwLPggaDbPGVaPl-WWZRt2wOKNcL-jPILMqSNiuIiqjB4-jrWrUZ49rJPYwmO2hl0kSXnUet5pakTvkGCiRhXlm_Sr9SCZJR4aujYVoEx20mKMh6-3fWgpbCplImZ3tei5gc4ir3a_CVFnIm1jTYtFvshSDgkd3wz749XML9--YXC6VUW9Q6BkbYaHz3V84xuWAw_4MXZ5BVBVQ2aflK31HOUzV3mB0sOLF6Q_pCLDjUC_kmmvBZ96Fxl6Yt5iYPDLdh7PAt427NMDZVPGg-Eb5QuJCk-lMC0pmfmPGOAGNf1k2902hmGJOwvfCQdjq5P3-nv85whbcHzvJ5e06tZ-WRgoX5zBr9qkMXOUUZZOD61DlUnulwBsyDrrpFFAk5yUb7EVT0EPBTPxXt5XSLaLVXJ6OzyNS9hXgslLW319sH_DRxT1BhJ4h4rgXSQJuKnbHmH4UqFhrrEckjM1hxtasyeKV3oaFzU4ASmWINkN2cuwSa4NyJm-yJABOY9lL3PJ8faA5jrhw1Pj6EPa27SF-YSftGNaz3bqtAQTM6YVotTmACPj8u_328FXwA9ICzsFfVcUuSNQmiYxVKwwjQM1F3SK2H1o5SGCT_7LnLh3z5yeaRhLVYTM7fyM6wrPQEwkFqLweSdU84xVAPtaxNKgd3IIqHourV5TXisDHhw18ysc-KoCbVcE2lRCVyBszD_L-HGZa9-sCAlntLfQFxSLE3Pl_RrIO68R6d9C1_p1R8CFxQiQa5J78UK7dbpl7Weca8xWATXzux-1PRAbx9FH_qLhaEBzl8EKKzgL9hVRobKpj53ouj7XurXe4it3RWBeg6EaYry0bQ7V_75eniYRiklBo28tAjt-sqAKsEq1la7ljp3DWpyh8DjbZjxBv7oYey2R8DLSYjIWolFZ5ftbPxKMcOecrhe8OzRcD3xEeawS2CEZ-r2P9zSx-IqNC3-hpvW0UTigKLo77UuEz5ObzR_F9NiJc6rBWJE_WjrrDi2SM90fXux7DmUTvO5OzR8hxt015Y2mVMcJyaiwLdNrJ3731WW5T4QzHGO5Sq4r_Hw4plc7h2EhCboYZJPLNOrFW6E94gwGYrBMkq26eCp78mmLQqhVLaWXahgZ5XCVizTmGdRsZDZsEA-YXsZzHznDawYHRmwRI.vzeZKFIHG7nytYT5z8tzUw"; + +/** + * Use to generate a valid EAR JWE for testing using the JWK above + * @param dataToEncrypt + * @returns + */ +export async function generateValidEarJWE(dataToEncrypt: string, jwk: string) { + const b64DecodedJwk = base64Decode(jwk); + const rawKey = JSON.parse(b64DecodedJwk).k; + const keyBuffer = base64DecToArr(rawKey); + + const key = await window.crypto.subtle.importKey( + "raw", + keyBuffer, + "AES-GCM", + false, + ["encrypt"] + ); + const header = "eyJlbmMiOiJBMjU2R0NNIiwidHlwIjoiSldUIiwiYWxnIjoiZGlyIn0"; + const iv = "TZmjdyAECSgPQNIm"; + const data = new TextEncoder().encode(dataToEncrypt); + const encrypted = await window.crypto.subtle.encrypt( + { + name: "AES-GCM", + length: 256, + iv: base64DecToArr(iv), + additionalData: new TextEncoder().encode(header), + }, + key, + data + ); + const [ciphertext, tag] = [ + encrypted.slice(0, encrypted.byteLength - 16), + encrypted.slice(encrypted.byteLength - 16), + ]; + + return `${header}..${iv}.${urlEncodeArr( + new Uint8Array(ciphertext) + )}.${urlEncodeArr(new Uint8Array(tag))}`; +} + +const testTenantProfilesMap = new Map(); +testTenantProfilesMap.set(ID_TOKEN_CLAIMS.tid, { + isHomeTenant: true, + localAccountId: TEST_DATA_CLIENT_INFO.TEST_LOCAL_ACCOUNT_ID, + name: ID_TOKEN_CLAIMS.name, + tenantId: ID_TOKEN_CLAIMS.tid, +}); + +export const TEST_ACCOUNT_INFO: AccountInfo = { + authorityType: "MSSTS", + homeAccountId: TEST_DATA_CLIENT_INFO.TEST_HOME_ACCOUNT_ID, + localAccountId: TEST_DATA_CLIENT_INFO.TEST_LOCAL_ACCOUNT_ID, + environment: "login.windows.net", + tenantId: ID_TOKEN_CLAIMS.tid, + username: ID_TOKEN_CLAIMS.preferred_username, + idToken: TEST_TOKENS.IDTOKEN_V2, + idTokenClaims: ID_TOKEN_CLAIMS, + name: ID_TOKEN_CLAIMS.name, + nativeAccountId: undefined, + tenantProfiles: testTenantProfilesMap, +}; + +export function getTestAuthenticationResult(): AuthenticationResult { + return { + authority: TEST_CONFIG.validAuthority, + uniqueId: ID_TOKEN_CLAIMS.oid, + tenantId: ID_TOKEN_CLAIMS.tid, + scopes: TEST_TOKEN_RESPONSE.body.scope!.split(" "), + idToken: TEST_TOKENS.IDTOKEN_V2, + accessToken: TEST_TOKEN_RESPONSE.body.access_token!, + idTokenClaims: ID_TOKEN_CLAIMS, + fromCache: false, + expiresOn: new Date( + (TimeUtils.nowSeconds() + TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN) * + 1000 + ), + tokenType: "Bearer", + correlationId: TEST_CONFIG.CORRELATION_ID, + account: TEST_ACCOUNT_INFO, + state: TEST_STATE_VALUES.USER_STATE, + cloudGraphHostName: "", + code: undefined, + extExpiresOn: new Date( + (TimeUtils.nowSeconds() + TEST_TOKEN_LIFETIMES.DEFAULT_EXPIRES_IN) * + 1000 + ), + fromNativeBroker: false, + msGraphHost: "", + refreshOn: undefined, + requestId: "", + familyId: "", + }; +} + +export const PlatformDOMTestTokenResponseObject = { + isSuccess: true, + state: "1234state", + accessToken: "eyJ0eXAiOiJKV1QiLCJub25j…", + expiresIn: 1744768881, + account: { + userName: "idlab@msidlab4.onmicrosoft.com", + id: "test-nativeAccountId", + properties: {}, + }, + clientInfo: + "clienteyJ1aWQiOiJkMTdkMzcwNi0xZTRlLTQ2OTUtODA0OC1lZjYxOTZlOTZm", + idToken: TEST_TOKENS.IDTOKEN_V2, + scopes: "openid profile User.Read email", + proofOfPossessionPayload: "successshr", + extendedLifetimeToken: true, + properties: {}, + error: { + code: "", + description: "", + errorCode: "", + properties: {}, + status: "", + protocolError: "", + }, +}; + +export const PlatformDOMTestErrorResponseObject = { + isSuccess: false, + expiresIn: 0, + extendedLifetimeToken: false, + error: { + code: "OSError", + description: + 'Error Domain=com.apple.AuthenticationServices.AuthorizationError Code=-6000 "(null)" UserInfo={NSUnderlyingError=0x1319056b0 {Error Domain=MSALErrorDomain Code=-50000 "(null)" UserInfo={MSALErrorDescriptionKey=redirectUri host doesn\'t match sender host., MSALBrokerVersionKey=1.0, MSALInternalErrorCodeKey=-42008, MSALBrowserNativeMessageErrorStatus=PERSISTENT_ERROR}}}', + errorCode: "-6000", + properties: { + MATS: '{"x_ms_clitelem":"1,0,0,,I","ui_visible":true}', + }, + status: "PERSISTENT_ERROR", + protocolError: "", + }, +}; diff --git a/lib/msal-browser/tsconfig.custom-auth.build.json b/lib/msal-browser/tsconfig.custom-auth.build.json new file mode 100644 index 0000000000..3177289d6b --- /dev/null +++ b/lib/msal-browser/tsconfig.custom-auth.build.json @@ -0,0 +1,8 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "rootDir": "./src", + "outDir": "./dist/custom-auth-path", + }, + "include": ["src"] +} diff --git a/lib/msal-common/CHANGELOG.json b/lib/msal-common/CHANGELOG.json index 5ac03ec9be..bb84afc7c7 100644 --- a/lib/msal-common/CHANGELOG.json +++ b/lib/msal-common/CHANGELOG.json @@ -1,6 +1,447 @@ { "name": "@azure/msal-common", "entries": [ + { + "date": "Tue, 08 Jul 2025 14:21:12 GMT", + "version": "15.8.1", + "tag": "@azure/msal-common_v15.8.1", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "aab16ab730aa302042f8d68422391497816ec4b7", + "comment": "Remove RequestValidator class" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 01 Jul 2025 14:23:25 GMT", + "version": "15.8.0", + "tag": "@azure/msal-common_v15.8.0", + "comments": { + "minor": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "b2942e458324c8bf767a068cae3eccbd4863baf3", + "comment": "Add correlationIds to cache APIs #7819" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ], + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "b2942e458324c8bf767a068cae3eccbd4863baf3", + "comment": "Add lastUpdatedAt to cache entities" + } + ] + } + }, + { + "date": "Wed, 18 Jun 2025 15:21:55 GMT", + "version": "15.7.1", + "tag": "@azure/msal-common_v15.7.1", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "32c6a1bd1f2a8d2da6a1b0ad69a5fd9c7e4107c4", + "comment": "remove access tokens synchronously" + }, + { + "author": "lalimasharda@microsoft.com", + "package": "@azure/msal-common", + "commit": "32c6a1bd1f2a8d2da6a1b0ad69a5fd9c7e4107c4", + "comment": "added UX_NOT_ALLOWED suberror to InteractionRequired error type #7834" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Fri, 30 May 2025 22:36:44 GMT", + "version": "15.7.0", + "tag": "@azure/msal-common_v15.7.0", + "comments": { + "none": [ + { + "author": "shylasummers@microsoft.com", + "package": "@azure/msal-common", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "Bump version" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Fri, 30 May 2025 17:59:15 GMT", + "version": "15.7.0", + "tag": "@azure/msal-common_v15.7.0", + "comments": { + "none": [ + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-common", + "commit": "2a979a886ff80b97e35e36fc8cd27eafdf3752ea", + "comment": "Added Constants for Encoding Types #7766" + }, + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-common", + "commit": "fdb4b18bd1f024b77faa5bd1ccee53b49b3a7551", + "comment": "Removed duplicate http status enum #7767" + } + ], + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "8f6c61c7efe9204d3b73e9f67b707ba39611b105", + "comment": "Upgrade/rollback telemetry #7738" + } + ], + "minor": [ + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-common", + "commit": "4abedb3d38ec7d9f3c9937476dd1ccc9fcaf5c0a", + "comment": "Added token revocation functionality to Managed Identity's App Service and Service Fabric Sources #7679" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 06 May 2025 22:47:43 GMT", + "version": "15.6.0", + "tag": "@azure/msal-common_v15.6.0", + "comments": { + "minor": [ + { + "author": "shylasummers@microsoft.com", + "package": "@azure/msal-common", + "commit": "b6098464c80f3db124ca3db145446ddb71be936d", + "comment": "Multi-instance detection" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ], + "none": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "b6098464c80f3db124ca3db145446ddb71be936d", + "comment": "New dev dependency" + } + ] + } + }, + { + "date": "Tue, 29 Apr 2025 20:25:47 GMT", + "version": "15.5.2", + "tag": "@azure/msal-common_v15.5.2", + "comments": { + "patch": [ + { + "author": "sameera.gajjarapu@microsoft.com", + "package": "@azure/msal-common", + "commit": "2b6112d62495ac4e3245d8a0d9be6b541ffb206c", + "comment": "Add optional parameter to NetworkError for error specifics #7721" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "2b6112d62495ac4e3245d8a0d9be6b541ffb206c", + "comment": "Fix circular type imports" + }, + { + "author": "joarroyo@microsoft.com", + "package": "@azure/msal-common", + "commit": "2b6112d62495ac4e3245d8a0d9be6b541ffb206c", + "comment": "Deprecate cache options #7707" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 15 Apr 2025 23:34:14 GMT", + "version": "15.5.1", + "tag": "@azure/msal-common_v15.5.1", + "comments": { + "patch": [ + { + "author": "shylasummers@microsoft.com", + "package": "@azure/msal-common", + "commit": "66a6372ce1e4f80992e49d2c153628ad0415b71f", + "comment": "Add config option to not encode extra params" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 08 Apr 2025 16:56:06 GMT", + "version": "15.5.0", + "tag": "@azure/msal-common_v15.5.0", + "comments": { + "minor": [ + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-common", + "commit": "fabf962dcb69611b4516342107bc7bcc364c4e68", + "comment": "Implemented a Retry Policy for the IMDS Managed Identity Source #7614" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 25 Mar 2025 22:29:34 GMT", + "version": "15.4.0", + "tag": "@azure/msal-common_v15.4.0", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "8bbed7012616d9106c923d08b5c4e408a741577f", + "comment": "Additional PerformanceEvents" + } + ], + "minor": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "8bbed7012616d9106c923d08b5c4e408a741577f", + "comment": "EAR protocol request support" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Thu, 20 Mar 2025 22:09:04 GMT", + "version": "15.3.0", + "tag": "@azure/msal-common_v15.3.0", + "comments": { + "minor": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "0de0f206568d7f075d12177172d108efc01c9fab", + "comment": "Refactor /authorize request generation" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "0de0f206568d7f075d12177172d108efc01c9fab", + "comment": "refactor RequestParameterBuilder" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-common", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ], + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-common", + "commit": "0de0f206568d7f075d12177172d108efc01c9fab", + "comment": "encodeURIComponent during QS generation" + } + ] + } + }, { "date": "Tue, 11 Mar 2025 18:51:25 GMT", "version": "15.2.1", diff --git a/lib/msal-common/CHANGELOG.md b/lib/msal-common/CHANGELOG.md index e581e043b1..a07ccd4bbe 100644 --- a/lib/msal-common/CHANGELOG.md +++ b/lib/msal-common/CHANGELOG.md @@ -1,9 +1,139 @@ # Change Log - @azure/msal-common - + +## 15.8.1 + +Tue, 08 Jul 2025 14:21:12 GMT + +### Patches + +- Remove RequestValidator class (thomas.norling@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 15.8.0 + +Tue, 01 Jul 2025 14:23:25 GMT + +### Minor changes + +- Add correlationIds to cache APIs #7819 (thomas.norling@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +### Patches + +- Add lastUpdatedAt to cache entities (thomas.norling@microsoft.com) + +## 15.7.1 + +Wed, 18 Jun 2025 15:21:55 GMT + +### Patches + +- remove access tokens synchronously (thomas.norling@microsoft.com) +- added UX_NOT_ALLOWED suberror to InteractionRequired error type #7834 (lalimasharda@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 15.7.0 + +Fri, 30 May 2025 17:59:15 GMT + +### Minor changes + +- Added token revocation functionality to Managed Identity's App Service and Service Fabric Sources #7679 (rginsburg@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +### Patches + +- Upgrade/rollback telemetry #7738 (thomas.norling@microsoft.com) + +## 15.6.0 + +Tue, 06 May 2025 22:47:43 GMT + +### Minor changes + +- Multi-instance detection (shylasummers@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 15.5.2 + +Tue, 29 Apr 2025 20:25:47 GMT + +### Patches + +- Add optional parameter to NetworkError for error specifics #7721 (sameera.gajjarapu@microsoft.com) +- Fix circular type imports (thomas.norling@microsoft.com) +- Deprecate cache options #7707 (joarroyo@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 15.5.1 + +Tue, 15 Apr 2025 23:34:14 GMT + +### Patches + +- Add config option to not encode extra params (shylasummers@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 15.5.0 + +Tue, 08 Apr 2025 16:56:06 GMT + +### Minor changes + +- Implemented a Retry Policy for the IMDS Managed Identity Source #7614 (rginsburg@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +## 15.4.0 + +Tue, 25 Mar 2025 22:29:34 GMT + +### Minor changes + +- EAR protocol request support (thomas.norling@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +### Patches + +- Additional PerformanceEvents (thomas.norling@microsoft.com) + +## 15.3.0 + +Thu, 20 Mar 2025 22:09:04 GMT + +### Minor changes + +- Refactor /authorize request generation (thomas.norling@microsoft.com) +- refactor RequestParameterBuilder (thomas.norling@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 +- Bump rollup-msal to v0.0.0 + +### Patches + +- encodeURIComponent during QS generation (thomas.norling@microsoft.com) + ## 15.2.1 Tue, 11 Mar 2025 18:51:25 GMT diff --git a/lib/msal-common/apiReview/msal-common.api.md b/lib/msal-common/apiReview/msal-common.api.md index deb6d13f15..afaa461a77 100644 --- a/lib/msal-common/apiReview/msal-common.api.md +++ b/lib/msal-common/apiReview/msal-common.api.md @@ -76,7 +76,9 @@ declare namespace AADServerParamKeys { X_CLIENT_EXTRA_SKU, BROKER_CLIENT_ID, BROKER_REDIRECT_URI, - INSTANCE_AWARE + INSTANCE_AWARE, + EAR_JWK, + EAR_JWE_CRYPTO } } export { AADServerParamKeys } @@ -171,6 +173,8 @@ export class AccountEntity { // (undocumented) lastModificationTime?: string; // (undocumented) + lastUpdatedAt?: string; + // (undocumented) localAccountId: string; // (undocumented) msGraphHost?: string; @@ -226,6 +230,7 @@ export type ActiveAccountFilters = { homeAccountId: string; localAccountId: string; tenantId?: string; + lastUpdatedAt?: string; }; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -302,7 +307,7 @@ function addClientSecret(parameters: Map, clientSecret: string): // Warning: (ae-missing-release-tag) "addCodeChallengeParams" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function addCodeChallengeParams(parameters: Map, codeChallenge: string, codeChallengeMethod: string): void; +function addCodeChallengeParams(parameters: Map, codeChallenge?: string, codeChallengeMethod?: string): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "addCodeVerifier" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -328,6 +333,13 @@ function addDeviceCode(parameters: Map, code: string): void; // @public function addDomainHint(parameters: Map, domainHint: string): void; +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-missing-release-tag) "addEARParameters" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function addEARParameters(parameters: Map, jwk: string): void; + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "addExtraQueryParameters" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -434,15 +446,12 @@ function addRequestTokenUse(parameters: Map, tokenUse: string): // @public function addResponseMode(parameters: Map, responseMode?: ResponseMode): void; -// Warning: (ae-missing-release-tag) "addResponseTypeCode" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -function addResponseTypeCode(parameters: Map): void; - -// Warning: (ae-missing-release-tag) "addResponseTypeForTokenAndIdToken" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-missing-release-tag) "addResponseType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function addResponseTypeForTokenAndIdToken(parameters: Map): void; +function addResponseType(parameters: Map, responseType: OAuthResponseType): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -622,6 +631,7 @@ export type AuthOptions = { azureCloudOptions?: AzureCloudOptions; skipAuthorityMetadataCache?: boolean; instanceAware?: boolean; + encodeExtraQueryParams?: boolean; }; // Warning: (ae-internal-missing-underscore) The name "Authority" should be prefixed with an underscore because the declaration is marked as @internal @@ -759,11 +769,7 @@ export class AuthorizationCodeClient extends BaseClient { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen acquireToken(request: CommonAuthorizationCodeRequest, authCodePayload?: AuthorizationCodePayload): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - getAuthCodeUrl(request: CommonAuthorizationUrlRequest): Promise; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen getLogoutUri(logoutRequest: CommonEndSessionRequest): string; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - handleFragmentResponse(serverParams: ServerAuthorizationCodeResponse, cachedState: string): AuthorizationCodePayload; // (undocumented) protected includeRedirectUri: boolean; } @@ -787,6 +793,38 @@ export type AuthorizationCodePayload = { client_info?: string; }; +declare namespace AuthorizeProtocol { + export { + getStandardAuthorizeRequestParameters, + getAuthorizeUrl, + getAuthorizationCodePayload, + validateAuthorizationResponse + } +} + +// Warning: (ae-missing-release-tag) "AuthorizeResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export type AuthorizeResponse = { + code?: string; + ear_jwe?: string; + client_info?: string; + state?: string; + cloud_instance_name?: string; + cloud_instance_host_name?: string; + cloud_graph_host_name?: string; + msgraph_host?: string; + error?: string; + error_uri?: string; + error_description?: string; + suberror?: string; + timestamp?: string; + trace_id?: string; + correlation_id?: string; + claims?: string; + accountId?: string; +}; + // Warning: (ae-missing-release-tag) "authTimeNotFound" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -927,7 +965,7 @@ const BROKER_REDIRECT_URI = "brk_redirect_uri"; // Warning: (ae-missing-release-tag) "buildAccountToCache" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export function buildAccountToCache(cacheStorage: CacheManager, authority: Authority, homeAccountId: string, base64Decode: (input: string) => string, idTokenClaims?: TokenClaims, clientInfo?: string, environment?: string, claimsTenantId?: string | null, authCodePayload?: AuthorizationCodePayload, nativeAccountId?: string, logger?: Logger): AccountEntity; +export function buildAccountToCache(cacheStorage: CacheManager, authority: Authority, homeAccountId: string, base64Decode: (input: string) => string, correlationId: string, idTokenClaims?: TokenClaims, clientInfo?: string, environment?: string, claimsTenantId?: string | null, authCodePayload?: AuthorizationCodePayload, nativeAccountId?: string, logger?: Logger): AccountEntity; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -969,7 +1007,7 @@ export type CacheAccountType = (typeof CacheAccountType)[keyof typeof CacheAccou // Warning: (ae-missing-release-tag) "CacheError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export class CacheError extends Error { +export class CacheError extends AuthError { constructor(errorCode: string, errorMessage?: string); errorCode: string; errorMessage: string; @@ -977,12 +1015,17 @@ export class CacheError extends Error { declare namespace CacheErrorCodes { export { - cacheQuotaExceededErrorCode, - cacheUnknownErrorCode + cacheQuotaExceeded, + cacheErrorUnknown } } export { CacheErrorCodes } +// Warning: (ae-missing-release-tag) "cacheErrorUnknown" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const cacheErrorUnknown = "cache_error_unknown"; + declare namespace CacheHelpers { export { generateCredentialKey, @@ -1010,7 +1053,7 @@ export { CacheHelpers } // // @internal export abstract class CacheManager implements ICacheManager { - constructor(clientId: string, cryptoImpl: ICrypto, logger: Logger, staticAuthorityOptions?: StaticAuthorityOptions); + constructor(clientId: string, cryptoImpl: ICrypto, logger: Logger, performanceClient: IPerformanceClient, staticAuthorityOptions?: StaticAuthorityOptions); // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -1033,17 +1076,17 @@ export abstract class CacheManager implements ICacheManager { // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' - getAccessToken(account: AccountInfo, request: BaseAuthRequest, tokenKeys?: TokenKeys, targetRealm?: string, performanceClient?: IPerformanceClient, correlationId?: string): AccessTokenEntity | null; + getAccessToken(account: AccountInfo, request: BaseAuthRequest, tokenKeys?: TokenKeys, targetRealm?: string): AccessTokenEntity | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - abstract getAccessTokenCredential(accessTokenKey: string): AccessTokenEntity | null; + abstract getAccessTokenCredential(accessTokenKey: string, correlationId: string): AccessTokenEntity | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - getAccessTokensByFilter(filter: CredentialFilter): AccessTokenEntity[]; + getAccessTokensByFilter(filter: CredentialFilter, correlationId: string): AccessTokenEntity[]; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - abstract getAccount(accountKey: string, logger?: Logger): AccountEntity | null; - getAccountInfoFilteredBy(accountFilter: AccountFilter): AccountInfo | null; + abstract getAccount(accountKey: string, correlationId: string): AccountEntity | null; + getAccountInfoFilteredBy(accountFilter: AccountFilter, correlationId: string): AccountInfo | null; abstract getAccountKeys(): string[]; - getAccountsFilteredBy(accountFilter: AccountFilter): AccountEntity[]; - getAllAccounts(accountFilter?: AccountFilter): AccountInfo[]; + getAccountsFilteredBy(accountFilter: AccountFilter, correlationId: string): AccountEntity[]; + getAllAccounts(accountFilter: AccountFilter, correlationId: string): AccountInfo[]; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen abstract getAppMetadata(appMetadataKey: string): AppMetadataEntity | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -1056,7 +1099,7 @@ export abstract class CacheManager implements ICacheManager { // (undocumented) abstract getAuthorityMetadataKeys(): Array; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - getBaseAccountInfo(accountFilter: AccountFilter): AccountInfo | null; + getBaseAccountInfo(accountFilter: AccountFilter, correlationId: string): AccountInfo | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -1067,11 +1110,11 @@ export abstract class CacheManager implements ICacheManager { // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' - getIdToken(account: AccountInfo, tokenKeys?: TokenKeys, targetRealm?: string, performanceClient?: IPerformanceClient, correlationId?: string): IdTokenEntity | null; + getIdToken(account: AccountInfo, correlationId: string, tokenKeys?: TokenKeys, targetRealm?: string, performanceClient?: IPerformanceClient): IdTokenEntity | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - abstract getIdTokenCredential(idTokenKey: string): IdTokenEntity | null; + abstract getIdTokenCredential(idTokenKey: string, correlationId: string): IdTokenEntity | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - getIdTokensByFilter(filter: CredentialFilter, tokenKeys?: TokenKeys): Map; + getIdTokensByFilter(filter: CredentialFilter, correlationId: string, tokenKeys?: TokenKeys): Map; abstract getKeys(): string[]; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' @@ -1083,9 +1126,9 @@ export abstract class CacheManager implements ICacheManager { // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' - getRefreshToken(account: AccountInfo, familyRT: boolean, tokenKeys?: TokenKeys, performanceClient?: IPerformanceClient, correlationId?: string): RefreshTokenEntity | null; + getRefreshToken(account: AccountInfo, familyRT: boolean, correlationId: string, tokenKeys?: TokenKeys, performanceClient?: IPerformanceClient): RefreshTokenEntity | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - abstract getRefreshTokenCredential(refreshTokenKey: string): RefreshTokenEntity | null; + abstract getRefreshTokenCredential(refreshTokenKey: string, correlationId: string): RefreshTokenEntity | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen abstract getServerTelemetry(serverTelemetryKey: string): ServerTelemetryEntity | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -1105,26 +1148,29 @@ export abstract class CacheManager implements ICacheManager { protected isAuthorityMetadata(key: string): boolean; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen isCredentialKey(key: string): boolean; + // (undocumented) + protected performanceClient: IPerformanceClient; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - readAccountFromCache(account: AccountInfo): AccountEntity | null; + readAccountFromCache(account: AccountInfo, correlationId: string): AccountEntity | null; readAppMetadataFromCache(environment: string): AppMetadataEntity | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen refreshTokenKeyMatchesFilter(inputKey: string, filter: CredentialFilter): boolean; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - removeAccessToken(key: string): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - removeAccount(accountKey: string): Promise; + removeAccessToken(key: string, correlationId: string): void; + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen + removeAccount(accountKey: string, correlationId: string): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - removeAccountContext(account: AccountEntity): Promise; - removeAllAccounts(): Promise; - removeAppMetadata(): boolean; + removeAccountContext(account: AccountEntity, correlationId: string): void; + removeAllAccounts(correlationId: string): void; + removeAppMetadata(correlationId: string): boolean; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - removeIdToken(key: string): void; + removeIdToken(key: string, correlationId: string): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - abstract removeItem(key: string): void; + abstract removeItem(key: string, correlationId: string): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - removeRefreshToken(key: string): void; + removeRefreshToken(key: string, correlationId: string): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -1139,7 +1185,7 @@ export abstract class CacheManager implements ICacheManager { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen abstract setAccount(account: AccountEntity, correlationId: string): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - abstract setAppMetadata(appMetadata: AppMetadataEntity): void; + abstract setAppMetadata(appMetadata: AppMetadataEntity, correlationId: string): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen abstract setAuthorityMetadata(key: string, value: AuthorityMetadataEntity): void; @@ -1151,10 +1197,10 @@ export abstract class CacheManager implements ICacheManager { abstract setRefreshTokenCredential(refreshToken: RefreshTokenEntity, correlationId: string): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - abstract setServerTelemetry(serverTelemetryKey: string, serverTelemetry: ServerTelemetryEntity): void; + abstract setServerTelemetry(serverTelemetryKey: string, serverTelemetry: ServerTelemetryEntity, correlationId: string): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - abstract setThrottlingCache(throttlingCacheKey: string, throttlingCache: ThrottlingEntity): void; + abstract setThrottlingCache(throttlingCacheKey: string, throttlingCache: ThrottlingEntity, correlationId: string): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen static toObject(obj: T, json: object): T; @@ -1182,10 +1228,10 @@ export const CacheOutcome: { // @public (undocumented) export type CacheOutcome = (typeof CacheOutcome)[keyof typeof CacheOutcome]; -// Warning: (ae-missing-release-tag) "cacheQuotaExceededErrorCode" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "cacheQuotaExceeded" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -const cacheQuotaExceededErrorCode = "cache_quota_exceeded"; +const cacheQuotaExceeded = "cache_quota_exceeded"; // Warning: (ae-internal-missing-underscore) The name "CacheRecord" should be prefixed with an underscore because the declaration is marked as @internal // @@ -1217,11 +1263,6 @@ export const CacheType: { // @public (undocumented) export type CacheType = (typeof CacheType)[keyof typeof CacheType]; -// Warning: (ae-missing-release-tag) "cacheUnknownErrorCode" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -const cacheUnknownErrorCode = "cache_error_unknown"; - // Warning: (ae-missing-release-tag) "cannotAllowPlatformBroker" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1621,7 +1662,6 @@ declare namespace ClientConfigurationErrorCodes { urlParseError, urlEmptyError, emptyInputScopesError, - invalidPromptValue, invalidClaims, tokenRequestEmpty, logoutRequestEmpty, @@ -1669,10 +1709,6 @@ export const ClientConfigurationErrorMessage: { code: string; desc: string; }; - invalidPrompt: { - code: string; - desc: string; - }; invalidClaimsRequest: { code: string; desc: string; @@ -1800,16 +1836,17 @@ export type CommonAuthorizationUrlRequest = BaseAuthRequest & { redirectUri: string; responseMode: ResponseMode; account?: AccountInfo; + earJwk?: string; codeChallenge?: string; codeChallengeMethod?: string; domainHint?: string; extraQueryParameters?: StringDict; extraScopesToConsent?: Array; loginHint?: string; - nonce?: string; + nonce: string; prompt?: string; sid?: string; - state?: string; + state: string; platformBroker?: boolean; }; @@ -1909,10 +1946,8 @@ export const Constants: { PROFILE_SCOPE: string; OFFLINE_ACCESS_SCOPE: string; EMAIL_SCOPE: string; - CODE_RESPONSE_TYPE: string; CODE_GRANT_TYPE: string; RT_GRANT_TYPE: string; - FRAGMENT_RESPONSE_MODE: string; S256_CODE_CHALLENGE_METHOD: string; URL_FORM_CONTENT_TYPE: string; AUTHORIZATION_PENDING: string; @@ -1927,8 +1962,6 @@ export const Constants: { AZURE_REGION_AUTO_DISCOVER_FLAG: string; REGIONAL_AUTH_PUBLIC_CLOUD_SUFFIX: string; KNOWN_PUBLIC_CLOUDS: string[]; - TOKEN_RESPONSE_TYPE: string; - ID_TOKEN_RESPONSE_TYPE: string; SHR_NONCE_VALIDITY: number; INVALID_INSTANCE: string; }; @@ -1951,6 +1984,12 @@ function createAccessTokenEntity(homeAccountId: string, environment: string, acc // @public (undocumented) export function createAuthError(code: string, additionalMessage?: string): AuthError; +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-missing-release-tag) "createCacheError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export function createCacheError(e: unknown): CacheError; + // Warning: (ae-missing-release-tag) "createClientAuthError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -1985,7 +2024,7 @@ export function createInteractionRequiredAuthError(errorCode: string): Interacti // Warning: (ae-missing-release-tag) "createNetworkError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -export function createNetworkError(error: AuthError, httpStatus?: number, responseHeaders?: Record): NetworkError; +export function createNetworkError(error: AuthError, httpStatus?: number, responseHeaders?: Record, additionalError?: Error): NetworkError; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -2012,6 +2051,7 @@ export type CredentialEntity = { tokenType?: AuthenticationScheme; keyId?: string; requestedClaimsHash?: string; + lastUpdatedAt?: string; }; // Warning: (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag @@ -2164,6 +2204,16 @@ const deviceCodeUnknownError = "device_code_unknown_error"; // @public (undocumented) const DOMAIN_HINT = "domain_hint"; +// Warning: (ae-missing-release-tag) "EAR_JWE_CRYPTO" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const EAR_JWE_CRYPTO = "ear_jwe_crypto"; + +// Warning: (ae-missing-release-tag) "EAR_JWK" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const EAR_JWK = "ear_jwk"; + // Warning: (ae-missing-release-tag) "emptyInputScopesError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2174,6 +2224,19 @@ const emptyInputScopesError = "empty_input_scopes_error"; // @public (undocumented) const emptyInputScopeSet = "empty_input_scopeset"; +// Warning: (ae-missing-release-tag) "EncodingTypes" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "EncodingTypes" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +export const EncodingTypes: { + readonly BASE64: "base64"; + readonly HEX: "hex"; + readonly UTF8: "utf-8"; +}; + +// @public (undocumented) +export type EncodingTypes = (typeof EncodingTypes)[keyof typeof EncodingTypes]; + // Warning: (ae-missing-release-tag) "endpointResolutionError" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2257,6 +2320,22 @@ function generateAuthorityMetadataExpiresAt(): number; // @public function generateCredentialKey(credentialEntity: CredentialEntity): string; +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-missing-release-tag) "getAuthorizationCodePayload" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function getAuthorizationCodePayload(serverParams: AuthorizeResponse, cachedState: string): AuthorizationCodePayload; + +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-incompatible-release-tags) The symbol "getAuthorizeUrl" is marked as @public, but its signature references "Authority" which is marked as @internal +// Warning: (ae-incompatible-release-tags) The symbol "getAuthorizeUrl" is marked as @public, but its signature references "Authority" which is marked as @internal +// Warning: (ae-missing-release-tag) "getAuthorizeUrl" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function getAuthorizeUrl(authority: Authority, requestParameters: Map, encodeParams?: boolean, extraQueryParameters?: StringDict | undefined): string; + // Warning: (ae-missing-release-tag) "getClientAssertion" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2265,7 +2344,7 @@ export function getClientAssertion(clientAssertion: string | ClientAssertionCall // Warning: (ae-missing-release-tag) "getDeserializedResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function getDeserializedResponse(responseString: string): ServerAuthorizationCodeResponse | null; +function getDeserializedResponse(responseString: string): AuthorizeResponse | null; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "getJWSPayload" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) @@ -2278,6 +2357,17 @@ function getJWSPayload(authToken: string): string; // @public (undocumented) export function getRequestThumbprint(clientId: string, request: BaseAuthRequest, homeAccountId?: string): RequestThumbprint; +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-incompatible-release-tags) The symbol "getStandardAuthorizeRequestParameters" is marked as @public, but its signature references "AuthOptions" which is marked as @internal +// Warning: (ae-incompatible-release-tags) The symbol "getStandardAuthorizeRequestParameters" is marked as @public, but its signature references "AuthOptions" which is marked as @internal +// Warning: (ae-missing-release-tag) "getStandardAuthorizeRequestParameters" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function getStandardAuthorizeRequestParameters(authOptions: AuthOptions, request: CommonAuthorizationUrlRequest, logger: Logger, performanceClient?: IPerformanceClient): Map; + // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "getTenantIdFromIdTokenClaims" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -2344,6 +2434,7 @@ export const HttpStatus: { readonly UNAUTHORIZED: 401; readonly NOT_FOUND: 404; readonly REQUEST_TIMEOUT: 408; + readonly GONE: 410; readonly TOO_MANY_REQUESTS: 429; readonly CLIENT_ERROR_RANGE_END: 499; readonly SERVER_ERROR: 500; @@ -2393,7 +2484,7 @@ export interface ICrypto { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen hashString(plainText: string): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - removeTokenBindingKey(kid: string): Promise; + removeTokenBindingKey(kid: string): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen signJwt(payload: SignedHttpRequest, kid: string, shrOptions?: ShrOptions, correlationId?: string): Promise; } @@ -2520,6 +2611,7 @@ declare namespace InteractionRequiredAuthErrorCodes { noTokensFound, nativeAccountUnavailable, refreshTokenExpired, + uxNotAllowed, interactionRequired, consentRequired, loginRequired, @@ -2596,11 +2688,6 @@ const invalidCloudDiscoveryMetadata = "invalid_cloud_discovery_metadata"; // @public (undocumented) const invalidCodeChallengeMethod = "invalid_code_challenge_method"; -// Warning: (ae-missing-release-tag) "invalidPromptValue" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public (undocumented) -const invalidPromptValue = "invalid_prompt_value"; - // Warning: (ae-missing-release-tag) "invalidState" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -2880,7 +2967,7 @@ const logoutRequestEmpty = "logout_request_empty"; // Warning: (ae-missing-release-tag) "mapToQueryString" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public -function mapToQueryString(parameters: Map): string; +function mapToQueryString(parameters: Map, encodeExtraParams?: boolean, extraQueryParameters?: StringDict): string; // Warning: (ae-missing-release-tag) "maxAgeTranspired" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // @@ -3053,6 +3140,19 @@ function nowSeconds(): number; // @public (undocumented) const nullOrEmptyToken = "null_or_empty_token"; +// Warning: (ae-missing-release-tag) "OAuthResponseType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// Warning: (ae-missing-release-tag) "OAuthResponseType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +export const OAuthResponseType: { + readonly CODE: "code"; + readonly IDTOKEN_TOKEN: "id_token token"; + readonly IDTOKEN_TOKEN_REFRESHTOKEN: "id_token token refresh_token"; +}; + +// @public (undocumented) +export type OAuthResponseType = (typeof OAuthResponseType)[keyof typeof OAuthResponseType]; + // Warning: (ae-missing-release-tag) "OBO_ASSERTION" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) @@ -3286,6 +3386,7 @@ export type PerformanceEvent = { serverErrorNo?: string; libraryName: string; libraryVersion: string; + previousLibraryVersion?: string; isNativeBroker?: boolean; requestId?: string; cacheLookupPolicy?: number | undefined; @@ -3350,6 +3451,8 @@ export type PerformanceEvent = { domainHintFromRequest?: boolean; prompt?: string; usePreGeneratedPkce?: boolean; + msalInstanceCount?: number; + sameClientIdInstanceCount?: number; }; // Warning: (tsdoc-undefined-tag) The TSDoc tag "@export" is not defined in this configuration @@ -3406,15 +3509,17 @@ export const PerformanceEvents: { readonly StandardInteractionClientCreateAuthCodeClient: "standardInteractionClientCreateAuthCodeClient"; readonly StandardInteractionClientGetClientConfiguration: "standardInteractionClientGetClientConfiguration"; readonly StandardInteractionClientInitializeAuthorizationRequest: "standardInteractionClientInitializeAuthorizationRequest"; - readonly StandardInteractionClientInitializeAuthorizationCodeRequest: "standardInteractionClientInitializeAuthorizationCodeRequest"; readonly GetAuthCodeUrl: "getAuthCodeUrl"; + readonly GetStandardParams: "getStandardParams"; readonly HandleCodeResponseFromServer: "handleCodeResponseFromServer"; readonly HandleCodeResponse: "handleCodeResponse"; + readonly HandleResponseEar: "handleResponseEar"; + readonly HandleResponsePlatformBroker: "handleResponsePlatformBroker"; + readonly HandleResponseCode: "handleResponseCode"; readonly UpdateTokenEndpointAuthority: "updateTokenEndpointAuthority"; readonly AuthClientAcquireToken: "authClientAcquireToken"; readonly AuthClientExecuteTokenRequest: "authClientExecuteTokenRequest"; readonly AuthClientCreateTokenRequestBody: "authClientCreateTokenRequestBody"; - readonly AuthClientCreateQueryString: "authClientCreateQueryString"; readonly PopTokenGenerateCnf: "popTokenGenerateCnf"; readonly PopTokenGenerateKid: "popTokenGenerateKid"; readonly HandleServerTokenResponse: "handleServerTokenResponse"; @@ -3456,6 +3561,8 @@ export const PerformanceEvents: { readonly UrlEncodeArr: "urlEncodeArr"; readonly Encrypt: "encrypt"; readonly Decrypt: "decrypt"; + readonly GenerateEarKey: "generateEarKey"; + readonly DecryptEarResponse: "decryptEarResponse"; }; // @public (undocumented) @@ -3567,6 +3674,7 @@ export const PromptValue: { export const ProtocolMode: { readonly AAD: "AAD"; readonly OIDC: "OIDC"; + readonly EAR: "EAR"; }; // @public (undocumented) @@ -3662,8 +3770,7 @@ const REQUESTED_TOKEN_USE = "requested_token_use"; declare namespace RequestParameterBuilder { export { instrumentBrokerParams, - addResponseTypeCode, - addResponseTypeForTokenAndIdToken, + addResponseType, addResponseMode, addNativeBroker, addScopes, @@ -3705,7 +3812,8 @@ declare namespace RequestParameterBuilder { addServerTelemetry, addThrottling, addLogoutHint, - addBrokerParameters + addBrokerParameters, + addEARParameters } } @@ -3763,10 +3871,6 @@ export class ResponseHandler { handleServerTokenResponse(serverTokenResponse: ServerAuthorizationTokenResponse, authority: Authority, reqTimestamp: number, request: BaseAuthRequest, authCodePayload?: AuthorizationCodePayload, userAssertionHash?: string, handlingRefreshTokenResponse?: boolean, forceCacheRefreshTokenResponse?: boolean, serverRequestId?: string): Promise; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - validateServerAuthorizationCodeResponse(serverResponse: ServerAuthorizationCodeResponse, requestState: string): void; - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen validateTokenResponse(serverResponse: ServerAuthorizationTokenResponse, refreshAccessToken?: boolean): void; } @@ -3775,9 +3879,9 @@ export class ResponseHandler { // // @public export const ResponseMode: { - readonly FORM_POST: "form_post"; readonly QUERY: "query"; readonly FRAGMENT: "fragment"; + readonly FORM_POST: "form_post"; }; // @public (undocumented) @@ -3826,28 +3930,6 @@ export class ScopeSet { unionScopeSets(otherScopes: ScopeSet): Set; } -// Warning: (ae-missing-release-tag) "ServerAuthorizationCodeResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) -// -// @public -export type ServerAuthorizationCodeResponse = { - code?: string; - client_info?: string; - state?: string; - cloud_instance_name?: string; - cloud_instance_host_name?: string; - cloud_graph_host_name?: string; - msgraph_host?: string; - error?: string; - error_uri?: string; - error_description?: string; - suberror?: string; - timestamp?: string; - trace_id?: string; - correlation_id?: string; - claims?: string; - accountId?: string; -}; - // Warning: (ae-missing-release-tag) "ServerAuthorizationTokenResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public @@ -3901,7 +3983,7 @@ export class ServerError extends AuthError { // Warning: (ae-missing-release-tag) "ServerResponseType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // Warning: (ae-missing-release-tag) "ServerResponseType" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // -// @public +// @public @deprecated export const ServerResponseType: { readonly QUERY: "query"; readonly FRAGMENT: "fragment"; @@ -4189,10 +4271,10 @@ export class ThrottlingUtils { // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - static postProcess(cacheManager: CacheManager, thumbprint: RequestThumbprint, response: NetworkResponse): void; + static postProcess(cacheManager: CacheManager, thumbprint: RequestThumbprint, response: NetworkResponse, correlationId: string): void; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen - static preProcess(cacheManager: CacheManager, thumbprint: RequestThumbprint): void; + static preProcess(cacheManager: CacheManager, thumbprint: RequestThumbprint, correlationId: string): void; // (undocumented) static removeThrottle(cacheManager: CacheManager, clientId: string, request: BaseAuthRequest, homeAccountIdentifier?: string): void; } @@ -4403,6 +4485,18 @@ const userCanceled = "user_canceled"; // @public (undocumented) const userTimeoutReached = "user_timeout_reached"; +// Warning: (ae-missing-release-tag) "uxNotAllowed" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public (undocumented) +const uxNotAllowed = "ux_not_allowed"; + +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// Warning: (ae-missing-release-tag) "validateAuthorizationResponse" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) +// +// @public +function validateAuthorizationResponse(serverResponse: AuthorizeResponse, requestState: string): void; + // Warning: (ae-internal-missing-underscore) The name "ValidCacheType" should be prefixed with an underscore because the declaration is marked as @internal // // @internal @@ -4416,7 +4510,7 @@ export type ValidCredentialType = IdTokenEntity | AccessTokenEntity | RefreshTok // Warning: (ae-missing-release-tag) "version" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const version = "15.2.1"; +export const version = "15.8.1"; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen @@ -4494,156 +4588,154 @@ const X_MS_LIB_CAPABILITY = "x-ms-lib-capability"; // src/authority/Authority.ts:818:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/authority/Authority.ts:1009:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/authority/AuthorityOptions.ts:26:5 - (ae-forgotten-export) The symbol "CloudInstanceDiscoveryResponse" needs to be exported by the entry point index.d.ts -// src/cache/CacheManager.ts:289:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:290:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:572:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1559:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1560:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1574:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1575:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:316:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:317:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:582:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/cache/CacheManager.ts:1595:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/cache/CacheManager.ts:1596:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1605:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1606:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1622:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1623:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1637:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1638:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1671:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1672:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1686:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1687:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1698:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1699:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1710:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1711:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1610:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1611:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1631:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1632:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1641:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1642:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1658:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1659:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1673:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1674:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1707:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1708:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/cache/CacheManager.ts:1722:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/cache/CacheManager.ts:1723:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1740:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1741:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1765:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1766:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1785:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1786:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1805:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1806:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1817:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1818:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/cache/CacheManager.ts:1826:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1734:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1735:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1746:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1747:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1758:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1759:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1776:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1777:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1801:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1802:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1821:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1822:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1841:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1842:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1853:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1854:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/cache/CacheManager.ts:1862:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/cache/utils/CacheTypes.ts:94:53 - (tsdoc-escape-greater-than) The ">" character should be escaped using a backslash to avoid confusion with an HTML tag // src/cache/utils/CacheTypes.ts:94:43 - (tsdoc-malformed-html-name) Invalid HTML element: An HTML name must be an ASCII letter followed by zero or more letters, digits, or hyphens -// src/client/AuthorizationCodeClient.ts:229:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/AuthorizationCodeClient.ts:230:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/AuthorizationCodeClient.ts:299:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/AuthorizationCodeClient.ts:531:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/AuthorizationCodeClient.ts:844:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/AuthorizationCodeClient.ts:899:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/AuthorizationCodeClient.ts:157:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/AuthorizationCodeClient.ts:158:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/AuthorizationCodeClient.ts:227:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/AuthorizationCodeClient.ts:463:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/client/RefreshTokenClient.ts:194:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/RefreshTokenClient.ts:287:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/RefreshTokenClient.ts:288:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/RefreshTokenClient.ts:339:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/client/SilentFlowClient.ts:172:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/config/ClientConfiguration.ts:50:5 - (ae-forgotten-export) The symbol "ClientCredentials" needs to be exported by the entry point index.d.ts -// src/config/ClientConfiguration.ts:52:5 - (ae-forgotten-export) The symbol "TelemetryOptions" needs to be exported by the entry point index.d.ts +// src/client/RefreshTokenClient.ts:290:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/RefreshTokenClient.ts:291:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/RefreshTokenClient.ts:342:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/client/SilentFlowClient.ts:173:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/config/ClientConfiguration.ts:51:5 - (ae-forgotten-export) The symbol "ClientCredentials" needs to be exported by the entry point index.d.ts +// src/config/ClientConfiguration.ts:53:5 - (ae-forgotten-export) The symbol "TelemetryOptions" needs to be exported by the entry point index.d.ts // src/index.ts:8:12 - (tsdoc-characters-after-block-tag) The token "@azure" looks like a TSDoc tag but contains an invalid character "/"; if it is not a tag, use a backslash to escape the "@" // src/index.ts:8:4 - (tsdoc-undefined-tag) The TSDoc tag "@module" is not defined in this configuration // src/request/AuthenticationHeaderParser.ts:74:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/response/ResponseHandler.ts:431:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/response/ResponseHandler.ts:432:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen -// src/response/ResponseHandler.ts:433:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/response/ResponseHandler.ts:336:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/response/ResponseHandler.ts:337:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen +// src/response/ResponseHandler.ts:338:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/telemetry/performance/PerformanceClient.ts:916:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/telemetry/performance/PerformanceClient.ts:916:15 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // src/telemetry/performance/PerformanceClient.ts:928:8 - (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // src/telemetry/performance/PerformanceClient.ts:928:27 - (tsdoc-param-tag-with-invalid-type) The @param block should not include a JSDoc-style '{type}' // src/telemetry/performance/PerformanceClient.ts:929:24 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag // src/telemetry/performance/PerformanceClient.ts:929:17 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:586:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:586:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:586:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:593:37 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:593:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:593:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:600:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:600:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:600:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:607:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:607:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:607:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:614:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:614:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:614:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:620:8 - (tsdoc-undefined-tag) The TSDoc tag "@date" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:622:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:622:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:622:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:630:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:630:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:630:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:637:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:637:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:637:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:644:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:644:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:644:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:652:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:652:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:652:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:659:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:659:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:659:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:666:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:666:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:666:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:673:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:673:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:673:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:680:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:680:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:680:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:692:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:692:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:692:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:699:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:699:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:699:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:706:23 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:706:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:706:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:713:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:713:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:713:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:720:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:720:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:720:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:726:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:726:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:726:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:733:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:733:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:733:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:752:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:752:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:752:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:758:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:758:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:758:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:765:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:765:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:765:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:589:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:589:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:589:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:596:37 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:596:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:596:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:603:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:603:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:603:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:610:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:610:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:610:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:617:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:617:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:617:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:623:8 - (tsdoc-undefined-tag) The TSDoc tag "@date" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:625:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:625:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:625:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:633:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:633:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:633:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:640:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:640:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:640:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:647:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:647:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:647:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:655:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:655:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:655:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:662:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:662:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:662:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:669:31 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:669:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:669:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:676:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:676:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:676:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:683:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:683:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:683:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:695:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:695:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:695:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:702:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:702:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:702:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:714:23 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:714:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:714:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:721:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:721:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:721:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:728:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:728:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:728:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:734:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:734:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:734:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:741:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:741:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:741:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:760:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:760:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:760:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:766:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:766:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:766:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration // src/telemetry/performance/PerformanceEvent.ts:773:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag // src/telemetry/performance/PerformanceEvent.ts:773:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" // src/telemetry/performance/PerformanceEvent.ts:773:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:782:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:782:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:782:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:790:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:781:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:781:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:781:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:790:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag // src/telemetry/performance/PerformanceEvent.ts:790:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" // src/telemetry/performance/PerformanceEvent.ts:790:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:797:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:797:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:797:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration -// src/telemetry/performance/PerformanceEvent.ts:867:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag -// src/telemetry/performance/PerformanceEvent.ts:867:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" -// src/telemetry/performance/PerformanceEvent.ts:867:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:798:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:798:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:798:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:805:22 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:805:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:805:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration +// src/telemetry/performance/PerformanceEvent.ts:875:21 - (tsdoc-escape-right-brace) The "}" character should be escaped using a backslash to avoid confusion with a TSDoc inline tag +// src/telemetry/performance/PerformanceEvent.ts:875:14 - (tsdoc-malformed-inline-tag) Expecting a TSDoc tag starting with "{@" +// src/telemetry/performance/PerformanceEvent.ts:875:8 - (tsdoc-undefined-tag) The TSDoc tag "@type" is not defined in this configuration ``` diff --git a/lib/msal-common/package.json b/lib/msal-common/package.json index e1394b3ae4..025660927f 100644 --- a/lib/msal-common/package.json +++ b/lib/msal-common/package.json @@ -10,7 +10,7 @@ "type": "git", "url": "https://github.com/AzureAD/microsoft-authentication-library-for-js.git" }, - "version": "15.2.1", + "version": "15.8.1", "description": "Microsoft Authentication Library for js", "keywords": [ "implicit", @@ -109,6 +109,7 @@ "@types/node": "^20.3.1", "eslint-config-msal": "file:../../shared-configs/eslint-config-msal", "jest": "^29.5.0", + "jest-junit": "^16.0.0", "lodash": "^4.17.21", "msal-test-utils": "file:../../shared-test-utils", "prettier": "2.8.7", diff --git a/lib/msal-common/src/account/AccountInfo.ts b/lib/msal-common/src/account/AccountInfo.ts index 5ebcdceac8..428a33731d 100644 --- a/lib/msal-common/src/account/AccountInfo.ts +++ b/lib/msal-common/src/account/AccountInfo.ts @@ -56,6 +56,7 @@ export type ActiveAccountFilters = { homeAccountId: string; localAccountId: string; tenantId?: string; + lastUpdatedAt?: string; }; /** diff --git a/lib/msal-common/src/authority/Authority.ts b/lib/msal-common/src/authority/Authority.ts index a3acc6e42a..30623e0ee6 100644 --- a/lib/msal-common/src/authority/Authority.ts +++ b/lib/msal-common/src/authority/Authority.ts @@ -51,7 +51,7 @@ import { CloudDiscoveryMetadata } from "./CloudDiscoveryMetadata.js"; import { RegionDiscovery } from "./RegionDiscovery.js"; import { RegionDiscoveryMetadata } from "./RegionDiscoveryMetadata.js"; import { ImdsOptions } from "./ImdsOptions.js"; -import { AzureCloudOptions } from "../config/ClientConfiguration.js"; +import type { AzureCloudOptions } from "../config/ClientConfiguration.js"; import { Logger } from "../logger/Logger.js"; import { AuthError } from "../error/AuthError.js"; import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js"; @@ -311,7 +311,7 @@ export class Authority { authorityUri.PathSegments[0] ) && this.getAuthorityType(authorityUri) === AuthorityType.Default && - this.protocolMode === ProtocolMode.AAD + this.protocolMode !== ProtocolMode.OIDC ); } @@ -378,7 +378,7 @@ export class Authority { if ( this.canonicalAuthority.endsWith("v2.0/") || this.authorityType === AuthorityType.Adfs || - (this.protocolMode !== ProtocolMode.AAD && + (this.protocolMode === ProtocolMode.OIDC && !this.isAliasOfKnownMicrosoftAuthority(canonicalAuthorityHost)) ) { return `${this.canonicalAuthority}.well-known/openid-configuration`; diff --git a/lib/msal-common/src/authority/ProtocolMode.ts b/lib/msal-common/src/authority/ProtocolMode.ts index c36e8438fe..5fd1375172 100644 --- a/lib/msal-common/src/authority/ProtocolMode.ts +++ b/lib/msal-common/src/authority/ProtocolMode.ts @@ -7,7 +7,18 @@ * Protocol modes supported by MSAL. */ export const ProtocolMode = { + /** + * Auth Code + PKCE with Entra ID (formerly AAD) specific optimizations and features + */ AAD: "AAD", + /** + * Auth Code + PKCE without Entra ID specific optimizations and features. For use only with non-Microsoft owned authorities. + * Support is limited for this mode. + */ OIDC: "OIDC", + /** + * Encrypted Authorize Response (EAR) with Entra ID specific optimizations and features + */ + EAR: "EAR", } as const; export type ProtocolMode = (typeof ProtocolMode)[keyof typeof ProtocolMode]; diff --git a/lib/msal-common/src/authority/RegionDiscovery.ts b/lib/msal-common/src/authority/RegionDiscovery.ts index 39af480bc4..cf999ec3df 100644 --- a/lib/msal-common/src/authority/RegionDiscovery.ts +++ b/lib/msal-common/src/authority/RegionDiscovery.ts @@ -8,8 +8,8 @@ import { NetworkResponse } from "../network/NetworkResponse.js"; import { IMDSBadResponse } from "../response/IMDSBadResponse.js"; import { Constants, + HttpStatus, RegionDiscoverySources, - ResponseCodes, } from "../utils/Constants.js"; import { RegionDiscoveryMetadata } from "./RegionDiscoveryMetadata.js"; import { ImdsOptions } from "./ImdsOptions.js"; @@ -75,10 +75,7 @@ export class RegionDiscovery { this.performanceClient, this.correlationId )(Constants.IMDS_VERSION, options); - if ( - localIMDSVersionResponse.status === - ResponseCodes.httpSuccess - ) { + if (localIMDSVersionResponse.status === HttpStatus.SUCCESS) { autodetectedRegionName = localIMDSVersionResponse.body; regionDiscoveryMetadata.region_source = RegionDiscoverySources.IMDS; @@ -86,8 +83,7 @@ export class RegionDiscovery { // If the response using the local IMDS version failed, try to fetch the current version of IMDS and retry. if ( - localIMDSVersionResponse.status === - ResponseCodes.httpBadRequest + localIMDSVersionResponse.status === HttpStatus.BAD_REQUEST ) { const currentIMDSVersion = await invokeAsync( this.getCurrentVersion.bind(this), @@ -110,8 +106,7 @@ export class RegionDiscovery { this.correlationId )(currentIMDSVersion, options); if ( - currentIMDSVersionResponse.status === - ResponseCodes.httpSuccess + currentIMDSVersionResponse.status === HttpStatus.SUCCESS ) { autodetectedRegionName = currentIMDSVersionResponse.body; @@ -180,7 +175,7 @@ export class RegionDiscovery { // When IMDS endpoint is called without the api version query param, bad request response comes back with latest version. if ( - response.status === ResponseCodes.httpBadRequest && + response.status === HttpStatus.BAD_REQUEST && response.body && response.body["newest-versions"] && response.body["newest-versions"].length > 0 diff --git a/lib/msal-common/src/cache/CacheManager.ts b/lib/msal-common/src/cache/CacheManager.ts index 2b95feaf49..80a78d7a58 100644 --- a/lib/msal-common/src/cache/CacheManager.ts +++ b/lib/msal-common/src/cache/CacheManager.ts @@ -52,7 +52,8 @@ import { getAliasesFromStaticSources } from "../authority/AuthorityMetadata.js"; import { StaticAuthorityOptions } from "../authority/AuthorityOptions.js"; import { TokenClaims } from "../account/TokenClaims.js"; import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js"; -import { CacheError, CacheErrorCodes } from "../error/CacheError.js"; +import { createCacheError } from "../error/CacheError.js"; +import { AuthError } from "../error/AuthError.js"; /** * Interface class which implement cache storage functions used by MSAL to perform validity checks, and store tokens. @@ -64,17 +65,20 @@ export abstract class CacheManager implements ICacheManager { // Instance of logger for functions defined in the msal-common layer private commonLogger: Logger; private staticAuthorityOptions?: StaticAuthorityOptions; + protected performanceClient: IPerformanceClient; constructor( clientId: string, cryptoImpl: ICrypto, logger: Logger, + performanceClient: IPerformanceClient, staticAuthorityOptions?: StaticAuthorityOptions ) { this.clientId = clientId; this.cryptoImpl = cryptoImpl; this.commonLogger = logger.clone(name, version); this.staticAuthorityOptions = staticAuthorityOptions; + this.performanceClient = performanceClient; } /** @@ -83,7 +87,7 @@ export abstract class CacheManager implements ICacheManager { */ abstract getAccount( accountKey: string, - logger?: Logger + correlationId: string ): AccountEntity | null; /** @@ -100,7 +104,10 @@ export abstract class CacheManager implements ICacheManager { * fetch the idToken entity from the platform cache * @param idTokenKey */ - abstract getIdTokenCredential(idTokenKey: string): IdTokenEntity | null; + abstract getIdTokenCredential( + idTokenKey: string, + correlationId: string + ): IdTokenEntity | null; /** * set idToken entity to the platform cache @@ -117,7 +124,8 @@ export abstract class CacheManager implements ICacheManager { * @param accessTokenKey */ abstract getAccessTokenCredential( - accessTokenKey: string + accessTokenKey: string, + correlationId: string ): AccessTokenEntity | null; /** @@ -135,7 +143,8 @@ export abstract class CacheManager implements ICacheManager { * @param refreshTokenKey */ abstract getRefreshTokenCredential( - refreshTokenKey: string + refreshTokenKey: string, + correlationId: string ): RefreshTokenEntity | null; /** @@ -158,7 +167,10 @@ export abstract class CacheManager implements ICacheManager { * set appMetadata entity to the platform cache * @param appMetadata */ - abstract setAppMetadata(appMetadata: AppMetadataEntity): void; + abstract setAppMetadata( + appMetadata: AppMetadataEntity, + correlationId: string + ): void; /** * fetch server telemetry entity from the platform cache @@ -175,7 +187,8 @@ export abstract class CacheManager implements ICacheManager { */ abstract setServerTelemetry( serverTelemetryKey: string, - serverTelemetry: ServerTelemetryEntity + serverTelemetry: ServerTelemetryEntity, + correlationId: string ): void; /** @@ -214,14 +227,15 @@ export abstract class CacheManager implements ICacheManager { */ abstract setThrottlingCache( throttlingCacheKey: string, - throttlingCache: ThrottlingEntity + throttlingCache: ThrottlingEntity, + correlationId: string ): void; /** * Function to remove an item from cache given its key. * @param key */ - abstract removeItem(key: string): void; + abstract removeItem(key: string, correlationId: string): void; /** * Function which retrieves all current keys from the cache. @@ -243,9 +257,13 @@ export abstract class CacheManager implements ICacheManager { * @param accountFilter - (Optional) filter to narrow down the accounts returned * @returns Array of AccountInfo objects in cache */ - getAllAccounts(accountFilter?: AccountFilter): AccountInfo[] { + getAllAccounts( + accountFilter: AccountFilter, + correlationId: string + ): AccountInfo[] { return this.buildTenantProfiles( - this.getAccountsFilteredBy(accountFilter || {}), + this.getAccountsFilteredBy(accountFilter, correlationId), + correlationId, accountFilter ); } @@ -253,8 +271,11 @@ export abstract class CacheManager implements ICacheManager { /** * Gets first tenanted AccountInfo object found based on provided filters */ - getAccountInfoFilteredBy(accountFilter: AccountFilter): AccountInfo | null { - const allAccounts = this.getAllAccounts(accountFilter); + getAccountInfoFilteredBy( + accountFilter: AccountFilter, + correlationId: string + ): AccountInfo | null { + const allAccounts = this.getAllAccounts(accountFilter, correlationId); if (allAccounts.length > 1) { // If one or more accounts are found, prioritize accounts that have an ID token const sortedAccounts = allAccounts.sort((account) => { @@ -274,8 +295,14 @@ export abstract class CacheManager implements ICacheManager { * @param accountFilter * @returns */ - getBaseAccountInfo(accountFilter: AccountFilter): AccountInfo | null { - const accountEntities = this.getAccountsFilteredBy(accountFilter); + getBaseAccountInfo( + accountFilter: AccountFilter, + correlationId: string + ): AccountInfo | null { + const accountEntities = this.getAccountsFilteredBy( + accountFilter, + correlationId + ); if (accountEntities.length > 0) { return accountEntities[0].getAccountInfo(); } else { @@ -292,11 +319,13 @@ export abstract class CacheManager implements ICacheManager { */ private buildTenantProfiles( cachedAccounts: AccountEntity[], + correlationId: string, accountFilter?: AccountFilter ): AccountInfo[] { return cachedAccounts.flatMap((accountEntity) => { return this.getTenantProfilesFromAccountEntity( accountEntity, + correlationId, accountFilter?.tenantId, accountFilter ); @@ -307,6 +336,7 @@ export abstract class CacheManager implements ICacheManager { accountInfo: AccountInfo, tokenKeys: TokenKeys, tenantProfile: TenantProfile, + correlationId: string, tenantProfileFilter?: TenantProfileFilter ): AccountInfo | null { let tenantedAccountInfo: AccountInfo | null = null; @@ -325,6 +355,7 @@ export abstract class CacheManager implements ICacheManager { const idToken = this.getIdToken( accountInfo, + correlationId, tokenKeys, tenantProfile.tenantId ); @@ -359,6 +390,7 @@ export abstract class CacheManager implements ICacheManager { private getTenantProfilesFromAccountEntity( accountEntity: AccountEntity, + correlationId: string, targetTenantId?: string, tenantProfileFilter?: TenantProfileFilter ): AccountInfo[] { @@ -387,6 +419,7 @@ export abstract class CacheManager implements ICacheManager { accountInfo, tokenKeys, tenantProfile, + correlationId, tenantProfileFilter ); if (tenantedAccountInfo) { @@ -532,37 +565,14 @@ export abstract class CacheManager implements ICacheManager { } if (!!cacheRecord.appMetadata) { - this.setAppMetadata(cacheRecord.appMetadata); + this.setAppMetadata(cacheRecord.appMetadata, correlationId); } } catch (e: unknown) { this.commonLogger?.error(`CacheManager.saveCacheRecord: failed`); - if (e instanceof Error) { - this.commonLogger?.errorPii( - `CacheManager.saveCacheRecord: ${e.message}`, - correlationId - ); - - if ( - e.name === "QuotaExceededError" || - e.name === "NS_ERROR_DOM_QUOTA_REACHED" || - e.message.includes("exceeded the quota") - ) { - this.commonLogger?.error( - `CacheManager.saveCacheRecord: exceeded storage quota`, - correlationId - ); - throw new CacheError( - CacheErrorCodes.cacheQuotaExceededErrorCode - ); - } else { - throw new CacheError(e.name, e.message); - } + if (e instanceof AuthError) { + throw e; } else { - this.commonLogger?.errorPii( - `CacheManager.saveCacheRecord: ${e}`, - correlationId - ); - throw new CacheError(CacheErrorCodes.cacheUnknownErrorCode); + throw createCacheError(e); } } } @@ -588,7 +598,6 @@ export abstract class CacheManager implements ICacheManager { const tokenKeys = this.getTokenKeys(); const currentScopes = ScopeSet.fromString(credential.target); - const removedAccessTokens: Array> = []; tokenKeys.accessToken.forEach((key) => { if ( !this.accessTokenKeyMatchesFilter(key, accessTokenFilter, false) @@ -596,7 +605,10 @@ export abstract class CacheManager implements ICacheManager { return; } - const tokenEntity = this.getAccessTokenCredential(key); + const tokenEntity = this.getAccessTokenCredential( + key, + correlationId + ); if ( tokenEntity && @@ -604,11 +616,10 @@ export abstract class CacheManager implements ICacheManager { ) { const tokenScopeSet = ScopeSet.fromString(tokenEntity.target); if (tokenScopeSet.intersectingScopeSets(currentScopes)) { - removedAccessTokens.push(this.removeAccessToken(key)); + this.removeAccessToken(key, correlationId); } } }); - await Promise.all(removedAccessTokens); await this.setAccessTokenCredential(credential, correlationId); } @@ -617,7 +628,10 @@ export abstract class CacheManager implements ICacheManager { * Not checking for casing as keys are all generated in lower case, remember to convert to lower case if object properties are compared * @param accountFilter - An object containing Account properties to filter by */ - getAccountsFilteredBy(accountFilter: AccountFilter): AccountEntity[] { + getAccountsFilteredBy( + accountFilter: AccountFilter, + correlationId: string + ): AccountEntity[] { const allAccountKeys = this.getAccountKeys(); const matchingAccounts: AccountEntity[] = []; allAccountKeys.forEach((cacheKey) => { @@ -628,7 +642,7 @@ export abstract class CacheManager implements ICacheManager { const entity: AccountEntity | null = this.getAccount( cacheKey, - this.commonLogger + correlationId ); // Match base account fields @@ -959,103 +973,102 @@ export abstract class CacheManager implements ICacheManager { /** * Removes all accounts and related tokens from cache. */ - async removeAllAccounts(): Promise { + removeAllAccounts(correlationId: string): void { const allAccountKeys = this.getAccountKeys(); - const removedAccounts: Array> = []; allAccountKeys.forEach((cacheKey) => { - removedAccounts.push(this.removeAccount(cacheKey)); + this.removeAccount(cacheKey, correlationId); }); - - await Promise.all(removedAccounts); } /** * Removes the account and related tokens for a given account key * @param account */ - async removeAccount(accountKey: string): Promise { - const account = this.getAccount(accountKey, this.commonLogger); + removeAccount(accountKey: string, correlationId: string): void { + const account = this.getAccount(accountKey, correlationId); if (!account) { return; } - await this.removeAccountContext(account); - this.removeItem(accountKey); + this.removeAccountContext(account, correlationId); + this.removeItem(accountKey, correlationId); } /** * Removes credentials associated with the provided account * @param account */ - async removeAccountContext(account: AccountEntity): Promise { + removeAccountContext(account: AccountEntity, correlationId: string): void { const allTokenKeys = this.getTokenKeys(); const accountId = account.generateAccountId(); - const removedCredentials: Array> = []; allTokenKeys.idToken.forEach((key) => { if (key.indexOf(accountId) === 0) { - this.removeIdToken(key); + this.removeIdToken(key, correlationId); } }); allTokenKeys.accessToken.forEach((key) => { if (key.indexOf(accountId) === 0) { - removedCredentials.push(this.removeAccessToken(key)); + this.removeAccessToken(key, correlationId); } }); allTokenKeys.refreshToken.forEach((key) => { if (key.indexOf(accountId) === 0) { - this.removeRefreshToken(key); + this.removeRefreshToken(key, correlationId); } }); - - await Promise.all(removedCredentials); } /** - * returns a boolean if the given credential is removed - * @param credential + * Removes accessToken from the cache + * @param key + * @param correlationId */ - async removeAccessToken(key: string): Promise { - const credential = this.getAccessTokenCredential(key); - if (!credential) { - return; - } + removeAccessToken(key: string, correlationId: string): void { + const credential = this.getAccessTokenCredential(key, correlationId); + this.removeItem(key, correlationId); + this.performanceClient.incrementFields( + { accessTokensRemoved: 1 }, + correlationId + ); - // Remove Token Binding Key from key store for PoP Tokens Credentials if ( - credential.credentialType.toLowerCase() === - CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME.toLowerCase() + !credential || + credential.credentialType.toLowerCase() !== + CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME.toLowerCase() || + credential.tokenType !== AuthenticationScheme.POP ) { - if (credential.tokenType === AuthenticationScheme.POP) { - const accessTokenWithAuthSchemeEntity = - credential as AccessTokenEntity; - const kid = accessTokenWithAuthSchemeEntity.keyId; - - if (kid) { - try { - await this.cryptoImpl.removeTokenBindingKey(kid); - } catch (error) { - throw createClientAuthError( - ClientAuthErrorCodes.bindingKeyNotRemoved - ); - } - } - } + // If the credential is not a PoP token, we can return + return; } - return this.removeItem(key); + // Remove Token Binding Key from key store for PoP Tokens Credentials + const kid = credential.keyId; + + if (kid) { + void this.cryptoImpl.removeTokenBindingKey(kid).catch(() => { + this.commonLogger.error( + `Failed to remove token binding key ${kid}`, + correlationId + ); + this.performanceClient?.incrementFields( + { removeTokenBindingKeyFailure: 1 }, + correlationId + ); + }); + } } /** * Removes all app metadata objects from cache. */ - removeAppMetadata(): boolean { + removeAppMetadata(correlationId: string): boolean { const allCacheKeys = this.getKeys(); allCacheKeys.forEach((cacheKey) => { if (this.isAppMetadata(cacheKey)) { - this.removeItem(cacheKey); + this.removeItem(cacheKey, correlationId); } }); @@ -1066,10 +1079,13 @@ export abstract class CacheManager implements ICacheManager { * Retrieve AccountEntity from cache * @param account */ - readAccountFromCache(account: AccountInfo): AccountEntity | null { + readAccountFromCache( + account: AccountInfo, + correlationId: string + ): AccountEntity | null { const accountKey: string = AccountEntity.generateAccountCacheKey(account); - return this.getAccount(accountKey, this.commonLogger); + return this.getAccount(accountKey, correlationId); } /** @@ -1082,10 +1098,10 @@ export abstract class CacheManager implements ICacheManager { */ getIdToken( account: AccountInfo, + correlationId: string, tokenKeys?: TokenKeys, targetRealm?: string, - performanceClient?: IPerformanceClient, - correlationId?: string + performanceClient?: IPerformanceClient ): IdTokenEntity | null { this.commonLogger.trace("CacheManager - getIdToken called"); const idTokenFilter: CredentialFilter = { @@ -1098,6 +1114,7 @@ export abstract class CacheManager implements ICacheManager { const idTokenMap: Map = this.getIdTokensByFilter( idTokenFilter, + correlationId, tokenKeys ); @@ -1140,7 +1157,7 @@ export abstract class CacheManager implements ICacheManager { "CacheManager:getIdToken - Multiple matching ID tokens found, clearing them" ); tokensToBeRemoved.forEach((idToken, key) => { - this.removeIdToken(key); + this.removeIdToken(key, correlationId); }); if (performanceClient && correlationId) { performanceClient.addFields( @@ -1162,6 +1179,7 @@ export abstract class CacheManager implements ICacheManager { */ getIdTokensByFilter( filter: CredentialFilter, + correlationId: string, tokenKeys?: TokenKeys ): Map { const idTokenKeys = @@ -1180,7 +1198,7 @@ export abstract class CacheManager implements ICacheManager { ) { return; } - const idToken = this.getIdTokenCredential(key); + const idToken = this.getIdTokenCredential(key, correlationId); if (idToken && this.credentialMatchesFilter(idToken, filter)) { idTokens.set(key, idToken); } @@ -1221,35 +1239,37 @@ export abstract class CacheManager implements ICacheManager { * Removes idToken from the cache * @param key */ - removeIdToken(key: string): void { - this.removeItem(key); + removeIdToken(key: string, correlationId: string): void { + this.removeItem(key, correlationId); } /** * Removes refresh token from the cache * @param key */ - removeRefreshToken(key: string): void { - this.removeItem(key); + removeRefreshToken(key: string, correlationId: string): void { + this.removeItem(key, correlationId); } /** * Retrieve AccessTokenEntity from cache * @param account {AccountInfo} * @param request {BaseAuthRequest} + * @param correlationId {?string} * @param tokenKeys {?TokenKeys} * @param performanceClient {?IPerformanceClient} - * @param correlationId {?string} */ getAccessToken( account: AccountInfo, request: BaseAuthRequest, tokenKeys?: TokenKeys, - targetRealm?: string, - performanceClient?: IPerformanceClient, - correlationId?: string + targetRealm?: string ): AccessTokenEntity | null { - this.commonLogger.trace("CacheManager - getAccessToken called"); + const correlationId = request.correlationId; + this.commonLogger.trace( + "CacheManager - getAccessToken called", + correlationId + ); const scopes = ScopeSet.createSearchScopes(request.scopes); const authScheme = request.authenticationScheme || AuthenticationScheme.BEARER; @@ -1286,7 +1306,10 @@ export abstract class CacheManager implements ICacheManager { if ( this.accessTokenKeyMatchesFilter(key, accessTokenFilter, true) ) { - const accessToken = this.getAccessTokenCredential(key); + const accessToken = this.getAccessTokenCredential( + key, + correlationId + ); // Validate value if ( @@ -1301,27 +1324,31 @@ export abstract class CacheManager implements ICacheManager { const numAccessTokens = accessTokens.length; if (numAccessTokens < 1) { this.commonLogger.info( - "CacheManager:getAccessToken - No token found" + "CacheManager:getAccessToken - No token found", + correlationId ); return null; } else if (numAccessTokens > 1) { this.commonLogger.info( - "CacheManager:getAccessToken - Multiple access tokens found, clearing them" + "CacheManager:getAccessToken - Multiple access tokens found, clearing them", + correlationId ); accessTokens.forEach((accessToken) => { - void this.removeAccessToken(generateCredentialKey(accessToken)); - }); - if (performanceClient && correlationId) { - performanceClient.addFields( - { multiMatchedAT: accessTokens.length }, + this.removeAccessToken( + generateCredentialKey(accessToken), correlationId ); - } + }); + this.performanceClient.addFields( + { multiMatchedAT: accessTokens.length }, + correlationId + ); return null; } this.commonLogger.info( - "CacheManager:getAccessToken - Returning access token" + "CacheManager:getAccessToken - Returning access token", + correlationId ); return accessTokens[0]; } @@ -1391,7 +1418,10 @@ export abstract class CacheManager implements ICacheManager { * @param filter * @returns */ - getAccessTokensByFilter(filter: CredentialFilter): AccessTokenEntity[] { + getAccessTokensByFilter( + filter: CredentialFilter, + correlationId: string + ): AccessTokenEntity[] { const tokenKeys = this.getTokenKeys(); const accessTokens: AccessTokenEntity[] = []; @@ -1400,7 +1430,10 @@ export abstract class CacheManager implements ICacheManager { return; } - const accessToken = this.getAccessTokenCredential(key); + const accessToken = this.getAccessTokenCredential( + key, + correlationId + ); if ( accessToken && this.credentialMatchesFilter(accessToken, filter) @@ -1416,16 +1449,16 @@ export abstract class CacheManager implements ICacheManager { * Helper to retrieve the appropriate refresh token from cache * @param account {AccountInfo} * @param familyRT {boolean} + * @param correlationId {?string} * @param tokenKeys {?TokenKeys} * @param performanceClient {?IPerformanceClient} - * @param correlationId {?string} */ getRefreshToken( account: AccountInfo, familyRT: boolean, + correlationId: string, tokenKeys?: TokenKeys, - performanceClient?: IPerformanceClient, - correlationId?: string + performanceClient?: IPerformanceClient ): RefreshTokenEntity | null { this.commonLogger.trace("CacheManager - getRefreshToken called"); const id = familyRT ? THE_FAMILY_ID : undefined; @@ -1445,7 +1478,10 @@ export abstract class CacheManager implements ICacheManager { refreshTokenKeys.forEach((key) => { // Validate key if (this.refreshTokenKeyMatchesFilter(key, refreshTokenFilter)) { - const refreshToken = this.getRefreshTokenCredential(key); + const refreshToken = this.getRefreshTokenCredential( + key, + correlationId + ); // Validate value if ( refreshToken && diff --git a/lib/msal-common/src/cache/entities/AccountEntity.ts b/lib/msal-common/src/cache/entities/AccountEntity.ts index 4f0cd5c79d..230e7119d7 100644 --- a/lib/msal-common/src/cache/entities/AccountEntity.ts +++ b/lib/msal-common/src/cache/entities/AccountEntity.ts @@ -4,7 +4,7 @@ */ import { CacheAccountType, Separators } from "../../utils/Constants.js"; -import { Authority } from "../../authority/Authority.js"; +import type { Authority } from "../../authority/Authority.js"; import { ICrypto } from "../../crypto/ICrypto.js"; import { ClientInfo, buildClientInfo } from "../../account/ClientInfo.js"; import { @@ -62,6 +62,7 @@ export class AccountEntity { msGraphHost?: string; nativeAccountId?: string; tenantProfiles?: Array; + lastUpdatedAt?: string; /** * Generate Account Id key component as per the schema: - @@ -150,10 +151,10 @@ export class AccountEntity { if (authority.authorityType === AuthorityType.Adfs) { account.authorityType = CacheAccountType.ADFS_ACCOUNT_TYPE; - } else if (authority.protocolMode === ProtocolMode.AAD) { - account.authorityType = CacheAccountType.MSSTS_ACCOUNT_TYPE; - } else { + } else if (authority.protocolMode === ProtocolMode.OIDC) { account.authorityType = CacheAccountType.GENERIC_ACCOUNT_TYPE; + } else { + account.authorityType = CacheAccountType.MSSTS_ACCOUNT_TYPE; } let clientInfo: ClientInfo | undefined; diff --git a/lib/msal-common/src/cache/entities/CredentialEntity.ts b/lib/msal-common/src/cache/entities/CredentialEntity.ts index eab89c9086..618d2b9163 100644 --- a/lib/msal-common/src/cache/entities/CredentialEntity.ts +++ b/lib/msal-common/src/cache/entities/CredentialEntity.ts @@ -33,4 +33,6 @@ export type CredentialEntity = { keyId?: string; /** Matches the SHA 256 hash of the claims object included in the token request */ requestedClaimsHash?: string; + /** Timestamp when the entry was last updated */ + lastUpdatedAt?: string; }; diff --git a/lib/msal-common/src/cache/interface/ICacheManager.ts b/lib/msal-common/src/cache/interface/ICacheManager.ts index 2d1c879d8d..5ec4b7dea5 100644 --- a/lib/msal-common/src/cache/interface/ICacheManager.ts +++ b/lib/msal-common/src/cache/interface/ICacheManager.ts @@ -21,7 +21,7 @@ export interface ICacheManager { * fetch the account entity from the platform cache * @param accountKey */ - getAccount(accountKey: string): AccountEntity | null; + getAccount(accountKey: string, correlationId: string): AccountEntity | null; /** * set account entity in the platform cache @@ -46,7 +46,10 @@ export interface ICacheManager { * fetch the idToken entity from the platform cache * @param idTokenKey */ - getIdTokenCredential(idTokenKey: string): IdTokenEntity | null; + getIdTokenCredential( + idTokenKey: string, + correlationId: string + ): IdTokenEntity | null; /** * set idToken entity to the platform cache @@ -61,7 +64,10 @@ export interface ICacheManager { * fetch the idToken entity from the platform cache * @param accessTokenKey */ - getAccessTokenCredential(accessTokenKey: string): AccessTokenEntity | null; + getAccessTokenCredential( + accessTokenKey: string, + correlationId: string + ): AccessTokenEntity | null; /** * set idToken entity to the platform cache @@ -77,7 +83,8 @@ export interface ICacheManager { * @param refreshTokenKey */ getRefreshTokenCredential( - refreshTokenKey: string + refreshTokenKey: string, + correlationId: string ): RefreshTokenEntity | null; /** @@ -99,7 +106,7 @@ export interface ICacheManager { * set appMetadata entity to the platform cache * @param appMetadata */ - setAppMetadata(appMetadata: AppMetadataEntity): void; + setAppMetadata(appMetadata: AppMetadataEntity, correlationId: string): void; /** * fetch server telemetry entity from the platform cache @@ -116,7 +123,8 @@ export interface ICacheManager { */ setServerTelemetry( serverTelemetryKey: string, - serverTelemetry: ServerTelemetryEntity + serverTelemetry: ServerTelemetryEntity, + correlationId: string ): void; /** @@ -162,13 +170,17 @@ export interface ICacheManager { */ setThrottlingCache( throttlingCacheKey: string, - throttlingCache: ThrottlingEntity + throttlingCache: ThrottlingEntity, + correlationId: string ): void; /** * Returns all accounts in cache */ - getAllAccounts(): AccountInfo[]; + getAllAccounts( + accountFilter: AccountFilter, + correlationId: string + ): AccountInfo[]; /** * saves a cache record @@ -186,43 +198,49 @@ export interface ICacheManager { * @param environment * @param realm */ - getAccountsFilteredBy(filter: AccountFilter): AccountEntity[]; + getAccountsFilteredBy( + filter: AccountFilter, + correlationId: string + ): AccountEntity[]; /** * Get AccountInfo object based on provided filters * @param filter */ - getAccountInfoFilteredBy(filter: AccountFilter): AccountInfo | null; + getAccountInfoFilteredBy( + filter: AccountFilter, + correlationId: string + ): AccountInfo | null; /** * Removes all accounts and related tokens from cache. */ - removeAllAccounts(): Promise; + removeAllAccounts(correlationId: string): void; /** * returns a boolean if the given account is removed * @param account */ - removeAccount(accountKey: string): Promise; + removeAccount(accountKey: string, correlationId: string): void; /** * returns a boolean if the given account is removed * @param account */ - removeAccountContext(account: AccountEntity): Promise; + removeAccountContext(account: AccountEntity, correlationId: string): void; /** * @param key */ - removeIdToken(key: string): void; + removeIdToken(key: string, correlationId: string): void; /** * @param key */ - removeAccessToken(key: string): Promise; + removeAccessToken(key: string, correlationId: string): void; /** * @param key */ - removeRefreshToken(key: string): void; + removeRefreshToken(key: string, correlationId: string): void; } diff --git a/lib/msal-common/src/client/AuthorizationCodeClient.ts b/lib/msal-common/src/client/AuthorizationCodeClient.ts index 1e40e7b0c0..7a830754fd 100644 --- a/lib/msal-common/src/client/AuthorizationCodeClient.ts +++ b/lib/msal-common/src/client/AuthorizationCodeClient.ts @@ -4,7 +4,6 @@ */ import { BaseClient } from "./BaseClient.js"; -import { CommonAuthorizationUrlRequest } from "../request/CommonAuthorizationUrlRequest.js"; import { CommonAuthorizationCodeRequest } from "../request/CommonAuthorizationCodeRequest.js"; import { Authority } from "../authority/Authority.js"; import * as RequestParameterBuilder from "../request/RequestParameterBuilder.js"; @@ -12,7 +11,6 @@ import * as UrlUtils from "../utils/UrlUtils.js"; import { GrantType, AuthenticationScheme, - PromptValue, Separators, HeaderNames, } from "../utils/Constants.js"; @@ -31,12 +29,10 @@ import { createClientAuthError, } from "../error/ClientAuthError.js"; import { UrlString } from "../url/UrlString.js"; -import { ServerAuthorizationCodeResponse } from "../response/ServerAuthorizationCodeResponse.js"; import { CommonEndSessionRequest } from "../request/CommonEndSessionRequest.js"; import { PopTokenGenerator } from "../crypto/PopTokenGenerator.js"; import { AuthorizationCodePayload } from "../response/AuthorizationCodePayload.js"; import * as TimeUtils from "../utils/TimeUtils.js"; -import { AccountInfo } from "../account/AccountInfo.js"; import { buildClientInfoFromHomeAccountId, buildClientInfo, @@ -46,7 +42,6 @@ import { createClientConfigurationError, ClientConfigurationErrorCodes, } from "../error/ClientConfigurationError.js"; -import { RequestValidator } from "../request/RequestValidator.js"; import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js"; import { PerformanceEvents } from "../telemetry/performance/PerformanceEvent.js"; import { invokeAsync } from "../utils/FunctionWrappers.js"; @@ -72,38 +67,6 @@ export class AuthorizationCodeClient extends BaseClient { this.config.authOptions.authority.options.OIDCOptions?.defaultScopes; } - /** - * Creates the URL of the authorization request letting the user input credentials and consent to the - * application. The URL target the /authorize endpoint of the authority configured in the - * application object. - * - * Once the user inputs their credentials and consents, the authority will send a response to the redirect URI - * sent in the request and should contain an authorization code, which can then be used to acquire tokens via - * acquireToken(AuthorizationCodeRequest) - * @param request - */ - async getAuthCodeUrl( - request: CommonAuthorizationUrlRequest - ): Promise { - this.performanceClient?.addQueueMeasurement( - PerformanceEvents.GetAuthCodeUrl, - request.correlationId - ); - - const queryString = await invokeAsync( - this.createAuthCodeUrlQueryString.bind(this), - PerformanceEvents.AuthClientCreateQueryString, - this.logger, - this.performanceClient, - request.correlationId - )(request); - - return UrlString.appendQueryString( - this.authority.authorizationEndpoint, - queryString - ); - } - /** * API to acquire a token in exchange of 'authorization_code` acquired by the user in the first leg of the * authorization_code_grant @@ -168,41 +131,6 @@ export class AuthorizationCodeClient extends BaseClient { ); } - /** - * Handles the hash fragment response from public client code request. Returns a code response used by - * the client to exchange for a token in acquireToken. - * @param hashFragment - */ - handleFragmentResponse( - serverParams: ServerAuthorizationCodeResponse, - cachedState: string - ): AuthorizationCodePayload { - // Handle responses. - const responseHandler = new ResponseHandler( - this.config.authOptions.clientId, - this.cacheManager, - this.cryptoUtils, - this.logger, - null, - null - ); - - // Get code response - responseHandler.validateServerAuthorizationCodeResponse( - serverParams, - cachedState - ); - - // throw when there is no auth code in the response - if (!serverParams.code) { - throw createClientAuthError( - ClientAuthErrorCodes.authorizationCodeMissingFromServerResponse - ); - } - - return serverParams as AuthorizationCodePayload; - } - /** * Used to log out the current user, and redirect the user to the postLogoutRedirectUri. * Default behaviour is to redirect the user to `window.location.href`. @@ -321,7 +249,11 @@ export class AuthorizationCodeClient extends BaseClient { */ if (!this.includeRedirectUri) { // Just validate - RequestValidator.validateRedirectUri(request.redirectUri); + if (!request.redirectUri) { + throw createClientConfigurationError( + ClientConfigurationErrorCodes.redirectUriEmpty + ); + } } else { // Validate and include redirect uri RequestParameterBuilder.addRedirectUri( @@ -526,319 +458,6 @@ export class AuthorizationCodeClient extends BaseClient { return UrlUtils.mapToQueryString(parameters); } - /** - * This API validates the `AuthorizationCodeUrlRequest` and creates a URL - * @param request - */ - private async createAuthCodeUrlQueryString( - request: CommonAuthorizationUrlRequest - ): Promise { - // generate the correlationId if not set by the user and add - const correlationId = - request.correlationId || - this.config.cryptoInterface.createNewGuid(); - - this.performanceClient?.addQueueMeasurement( - PerformanceEvents.AuthClientCreateQueryString, - correlationId - ); - - const parameters = new Map(); - - RequestParameterBuilder.addClientId( - parameters, - request.embeddedClientId || - request.extraQueryParameters?.[AADServerParamKeys.CLIENT_ID] || - this.config.authOptions.clientId - ); - - const requestScopes = [ - ...(request.scopes || []), - ...(request.extraScopesToConsent || []), - ]; - RequestParameterBuilder.addScopes( - parameters, - requestScopes, - true, - this.oidcDefaultScopes - ); - - // validate the redirectUri (to be a non null value) - RequestParameterBuilder.addRedirectUri(parameters, request.redirectUri); - - RequestParameterBuilder.addCorrelationId(parameters, correlationId); - - // add response_mode. If not passed in it defaults to query. - RequestParameterBuilder.addResponseMode( - parameters, - request.responseMode - ); - - // add response_type = code - RequestParameterBuilder.addResponseTypeCode(parameters); - - // add library info parameters - RequestParameterBuilder.addLibraryInfo( - parameters, - this.config.libraryInfo - ); - if (!isOidcProtocolMode(this.config)) { - RequestParameterBuilder.addApplicationTelemetry( - parameters, - this.config.telemetry.application - ); - } - - // add client_info=1 - RequestParameterBuilder.addClientInfo(parameters); - - if (request.codeChallenge && request.codeChallengeMethod) { - RequestParameterBuilder.addCodeChallengeParams( - parameters, - request.codeChallenge, - request.codeChallengeMethod - ); - } - - if (request.prompt) { - RequestParameterBuilder.addPrompt(parameters, request.prompt); - } - - if (request.domainHint) { - RequestParameterBuilder.addDomainHint( - parameters, - request.domainHint - ); - this.performanceClient?.addFields( - { domainHintFromRequest: true }, - correlationId - ); - } - - this.performanceClient?.addFields( - { prompt: request.prompt }, - correlationId - ); - - // Add sid or loginHint with preference for login_hint claim (in request) -> sid -> loginHint (upn/email) -> username of AccountInfo object - if (request.prompt !== PromptValue.SELECT_ACCOUNT) { - // AAD will throw if prompt=select_account is passed with an account hint - if (request.sid && request.prompt === PromptValue.NONE) { - // SessionID is only used in silent calls - this.logger.verbose( - "createAuthCodeUrlQueryString: Prompt is none, adding sid from request" - ); - RequestParameterBuilder.addSid(parameters, request.sid); - this.performanceClient?.addFields( - { sidFromRequest: true }, - correlationId - ); - } else if (request.account) { - const accountSid = this.extractAccountSid(request.account); - let accountLoginHintClaim = this.extractLoginHint( - request.account - ); - - if (accountLoginHintClaim && request.domainHint) { - this.logger.warning( - `AuthorizationCodeClient.createAuthCodeUrlQueryString: "domainHint" param is set, skipping opaque "login_hint" claim. Please consider not passing domainHint` - ); - accountLoginHintClaim = null; - } - - // If login_hint claim is present, use it over sid/username - if (accountLoginHintClaim) { - this.logger.verbose( - "createAuthCodeUrlQueryString: login_hint claim present on account" - ); - RequestParameterBuilder.addLoginHint( - parameters, - accountLoginHintClaim - ); - this.performanceClient?.addFields( - { loginHintFromClaim: true }, - correlationId - ); - try { - const clientInfo = buildClientInfoFromHomeAccountId( - request.account.homeAccountId - ); - RequestParameterBuilder.addCcsOid( - parameters, - clientInfo - ); - } catch (e) { - this.logger.verbose( - "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" - ); - } - } else if (accountSid && request.prompt === PromptValue.NONE) { - /* - * If account and loginHint are provided, we will check account first for sid before adding loginHint - * SessionId is only used in silent calls - */ - this.logger.verbose( - "createAuthCodeUrlQueryString: Prompt is none, adding sid from account" - ); - RequestParameterBuilder.addSid(parameters, accountSid); - this.performanceClient?.addFields( - { sidFromClaim: true }, - correlationId - ); - try { - const clientInfo = buildClientInfoFromHomeAccountId( - request.account.homeAccountId - ); - RequestParameterBuilder.addCcsOid( - parameters, - clientInfo - ); - } catch (e) { - this.logger.verbose( - "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" - ); - } - } else if (request.loginHint) { - this.logger.verbose( - "createAuthCodeUrlQueryString: Adding login_hint from request" - ); - RequestParameterBuilder.addLoginHint( - parameters, - request.loginHint - ); - RequestParameterBuilder.addCcsUpn( - parameters, - request.loginHint - ); - this.performanceClient?.addFields( - { loginHintFromRequest: true }, - correlationId - ); - } else if (request.account.username) { - // Fallback to account username if provided - this.logger.verbose( - "createAuthCodeUrlQueryString: Adding login_hint from account" - ); - RequestParameterBuilder.addLoginHint( - parameters, - request.account.username - ); - this.performanceClient?.addFields( - { loginHintFromUpn: true }, - correlationId - ); - try { - const clientInfo = buildClientInfoFromHomeAccountId( - request.account.homeAccountId - ); - RequestParameterBuilder.addCcsOid( - parameters, - clientInfo - ); - } catch (e) { - this.logger.verbose( - "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" - ); - } - } - } else if (request.loginHint) { - this.logger.verbose( - "createAuthCodeUrlQueryString: No account, adding login_hint from request" - ); - RequestParameterBuilder.addLoginHint( - parameters, - request.loginHint - ); - RequestParameterBuilder.addCcsUpn( - parameters, - request.loginHint - ); - this.performanceClient?.addFields( - { loginHintFromRequest: true }, - correlationId - ); - } - } else { - this.logger.verbose( - "createAuthCodeUrlQueryString: Prompt is select_account, ignoring account hints" - ); - } - - if (request.nonce) { - RequestParameterBuilder.addNonce(parameters, request.nonce); - } - - if (request.state) { - RequestParameterBuilder.addState(parameters, request.state); - } - - if ( - request.claims || - (this.config.authOptions.clientCapabilities && - this.config.authOptions.clientCapabilities.length > 0) - ) { - RequestParameterBuilder.addClaims( - parameters, - request.claims, - this.config.authOptions.clientCapabilities - ); - } - - if (request.embeddedClientId) { - RequestParameterBuilder.addBrokerParameters( - parameters, - this.config.authOptions.clientId, - this.config.authOptions.redirectUri - ); - } - - if (request.extraQueryParameters) { - RequestParameterBuilder.addExtraQueryParameters( - parameters, - request.extraQueryParameters - ); - } - - if (this.config.authOptions.instanceAware) { - RequestParameterBuilder.addInstanceAware(parameters); - } - - if (request.platformBroker) { - // signal ests that this is a WAM call - RequestParameterBuilder.addNativeBroker(parameters); - - // pass the req_cnf for POP - if (request.authenticationScheme === AuthenticationScheme.POP) { - const popTokenGenerator = new PopTokenGenerator( - this.cryptoUtils - ); - - // req_cnf is always sent as a string for SPAs - let reqCnfData; - if (!request.popKid) { - const generatedReqCnfData = await invokeAsync( - popTokenGenerator.generateCnf.bind(popTokenGenerator), - PerformanceEvents.PopTokenGenerateCnf, - this.logger, - this.performanceClient, - request.correlationId - )(request, this.logger); - reqCnfData = generatedReqCnfData.reqCnfString; - } else { - reqCnfData = this.cryptoUtils.encodeKid(request.popKid); - } - RequestParameterBuilder.addPopToken(parameters, reqCnfData); - } - } - - RequestParameterBuilder.instrumentBrokerParams( - parameters, - request.correlationId, - this.performanceClient - ); - return UrlUtils.mapToQueryString(parameters); - } - /** * This API validates the `EndSessionRequest` and creates a URL * @param request @@ -891,18 +510,10 @@ export class AuthorizationCodeClient extends BaseClient { RequestParameterBuilder.addInstanceAware(parameters); } - return UrlUtils.mapToQueryString(parameters); - } - - /** - * Helper to get sid from account. Returns null if idTokenClaims are not present or sid is not present. - * @param account - */ - private extractAccountSid(account: AccountInfo): string | null { - return account.idTokenClaims?.sid || null; - } - - private extractLoginHint(account: AccountInfo): string | null { - return account.idTokenClaims?.login_hint || null; + return UrlUtils.mapToQueryString( + parameters, + this.config.authOptions.encodeExtraQueryParams, + request.extraQueryParameters + ); } } diff --git a/lib/msal-common/src/client/BaseClient.ts b/lib/msal-common/src/client/BaseClient.ts index e60aa39943..e9c10d4e6e 100644 --- a/lib/msal-common/src/client/BaseClient.ts +++ b/lib/msal-common/src/client/BaseClient.ts @@ -187,7 +187,11 @@ export abstract class BaseClient { options: NetworkRequestOptions, correlationId: string ): Promise> { - ThrottlingUtils.preProcess(this.cacheManager, thumbprint); + ThrottlingUtils.preProcess( + this.cacheManager, + thumbprint, + correlationId + ); let response; try { @@ -244,7 +248,12 @@ export abstract class BaseClient { } } - ThrottlingUtils.postProcess(this.cacheManager, thumbprint, response); + ThrottlingUtils.postProcess( + this.cacheManager, + thumbprint, + response, + correlationId + ); return response; } diff --git a/lib/msal-common/src/client/RefreshTokenClient.ts b/lib/msal-common/src/client/RefreshTokenClient.ts index 1da926717e..f0bb91d022 100644 --- a/lib/msal-common/src/client/RefreshTokenClient.ts +++ b/lib/msal-common/src/client/RefreshTokenClient.ts @@ -212,9 +212,9 @@ export class RefreshTokenClient extends BaseClient { )( request.account, foci, + request.correlationId, undefined, - this.performanceClient, - request.correlationId + this.performanceClient ); if (!refreshToken) { @@ -274,7 +274,10 @@ export class RefreshTokenClient extends BaseClient { ); const badRefreshTokenKey = generateCredentialKey(refreshToken); - this.cacheManager.removeRefreshToken(badRefreshTokenKey); + this.cacheManager.removeRefreshToken( + badRefreshTokenKey, + request.correlationId + ); } } diff --git a/lib/msal-common/src/client/SilentFlowClient.ts b/lib/msal-common/src/client/SilentFlowClient.ts index 0801c88ee0..afe0ee9c89 100644 --- a/lib/msal-common/src/client/SilentFlowClient.ts +++ b/lib/msal-common/src/client/SilentFlowClient.ts @@ -75,9 +75,7 @@ export class SilentFlowClient extends BaseClient { request.account, request, tokenKeys, - requestTenantId, - this.performanceClient, - request.correlationId + requestTenantId ); if (!cachedAccessToken) { @@ -117,14 +115,17 @@ export class SilentFlowClient extends BaseClient { const environment = request.authority || this.authority.getPreferredCache(); const cacheRecord: CacheRecord = { - account: this.cacheManager.readAccountFromCache(request.account), + account: this.cacheManager.readAccountFromCache( + request.account, + request.correlationId + ), accessToken: cachedAccessToken, idToken: this.cacheManager.getIdToken( request.account, + request.correlationId, tokenKeys, requestTenantId, - this.performanceClient, - request.correlationId + this.performanceClient ), refreshToken: null, appMetadata: diff --git a/lib/msal-common/src/config/ClientConfiguration.ts b/lib/msal-common/src/config/ClientConfiguration.ts index e3eecd1206..06be508c2a 100644 --- a/lib/msal-common/src/config/ClientConfiguration.ts +++ b/lib/msal-common/src/config/ClientConfiguration.ts @@ -11,7 +11,7 @@ import { DEFAULT_TOKEN_RENEWAL_OFFSET_SEC, } from "../utils/Constants.js"; import { version } from "../packageMetadata.js"; -import { Authority } from "../authority/Authority.js"; +import type { Authority } from "../authority/Authority.js"; import { AzureCloudInstance } from "../authority/AuthorityOptions.js"; import { CacheManager, DefaultStorageClass } from "../cache/CacheManager.js"; import { ServerTelemetryManager } from "../telemetry/server/ServerTelemetryManager.js"; @@ -23,6 +23,7 @@ import { ClientAuthErrorCodes, createClientAuthError, } from "../error/ClientAuthError.js"; +import { StubPerformanceClient } from "../telemetry/performance/StubPerformanceClient.js"; /** * Use the configuration object to configure MSAL Modules and initialize the base interfaces for MSAL. @@ -83,6 +84,7 @@ export type CommonClientConfiguration = { * - skipAuthorityMetadataCache - A flag to choose whether to use or not use the local metadata cache during authority initialization. Defaults to false. * - instanceAware - A flag of whether the STS will send back additional parameters to specify where the tokens should be retrieved from. * - redirectUri - The redirect URI where authentication responses can be received by your application. It must exactly match one of the redirect URIs registered in the Azure portal. + * - encodeExtraQueryParams - A flag to choose whether to encode the extra query parameters or not. Defaults to false. * @internal */ export type AuthOptions = { @@ -93,6 +95,10 @@ export type AuthOptions = { azureCloudOptions?: AzureCloudOptions; skipAuthorityMetadataCache?: boolean; instanceAware?: boolean; + /** + * @deprecated This flag is deprecated and will be removed in the next major version where all extra query params will be encoded by default. + */ + encodeExtraQueryParams?: boolean; }; /** @@ -126,6 +132,9 @@ export type LoggerOptions = { * - claimsBasedCachingEnabled - Sets whether tokens should be cached based on the claims hash. Default is false. */ export type CacheOptions = { + /** + * @deprecated claimsBasedCachingEnabled is deprecated and will be removed in the next major version. + */ claimsBasedCachingEnabled?: boolean; }; @@ -252,7 +261,8 @@ export function buildClientConfiguration({ new DefaultStorageClass( userAuthOptions.clientId, DEFAULT_CRYPTO_IMPLEMENTATION, - new Logger(loggerOptions) + new Logger(loggerOptions), + new StubPerformanceClient() ), networkInterface: networkImplementation || DEFAULT_NETWORK_IMPLEMENTATION, @@ -276,6 +286,7 @@ function buildAuthOptions(authOptions: AuthOptions): Required { azureCloudOptions: DEFAULT_AZURE_CLOUD_OPTIONS, skipAuthorityMetadataCache: false, instanceAware: false, + encodeExtraQueryParams: false, ...authOptions, }; } diff --git a/lib/msal-common/src/constants/AADServerParamKeys.ts b/lib/msal-common/src/constants/AADServerParamKeys.ts index b62edfe780..4310d4924b 100644 --- a/lib/msal-common/src/constants/AADServerParamKeys.ts +++ b/lib/msal-common/src/constants/AADServerParamKeys.ts @@ -59,3 +59,5 @@ export const X_CLIENT_EXTRA_SKU = "x-client-xtra-sku"; export const BROKER_CLIENT_ID = "brk_client_id"; export const BROKER_REDIRECT_URI = "brk_redirect_uri"; export const INSTANCE_AWARE = "instance_aware"; +export const EAR_JWK = "ear_jwk"; +export const EAR_JWE_CRYPTO = "ear_jwe_crypto"; diff --git a/lib/msal-common/src/crypto/ICrypto.ts b/lib/msal-common/src/crypto/ICrypto.ts index 4728cc8029..4f9f98e1cd 100644 --- a/lib/msal-common/src/crypto/ICrypto.ts +++ b/lib/msal-common/src/crypto/ICrypto.ts @@ -7,8 +7,8 @@ import { ClientAuthErrorCodes, createClientAuthError, } from "../error/ClientAuthError.js"; -import { BaseAuthRequest } from "../request/BaseAuthRequest.js"; -import { ShrOptions, SignedHttpRequest } from "./SignedHttpRequest.js"; +import type { BaseAuthRequest } from "../request/BaseAuthRequest.js"; +import type { ShrOptions, SignedHttpRequest } from "./SignedHttpRequest.js"; /** * The PkceCodes type describes the structure @@ -70,7 +70,7 @@ export interface ICrypto { * Removes cryptographic keypair from key store matching the keyId passed in * @param kid */ - removeTokenBindingKey(kid: string): Promise; + removeTokenBindingKey(kid: string): Promise; /** * Removes all cryptographic keys from IndexedDB storage */ @@ -111,7 +111,7 @@ export const DEFAULT_CRYPTO_IMPLEMENTATION: ICrypto = { async getPublicKeyThumbprint(): Promise { throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented); }, - async removeTokenBindingKey(): Promise { + async removeTokenBindingKey(): Promise { throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented); }, async clearKeystore(): Promise { diff --git a/lib/msal-common/src/error/CacheError.ts b/lib/msal-common/src/error/CacheError.ts index 0ac74e8e37..4b93ae2b77 100644 --- a/lib/msal-common/src/error/CacheError.ts +++ b/lib/msal-common/src/error/CacheError.ts @@ -3,20 +3,20 @@ * Licensed under the MIT License. */ +import { AuthError } from "./AuthError.js"; import * as CacheErrorCodes from "./CacheErrorCodes.js"; export { CacheErrorCodes }; export const CacheErrorMessages = { - [CacheErrorCodes.cacheQuotaExceededErrorCode]: - "Exceeded cache storage capacity.", - [CacheErrorCodes.cacheUnknownErrorCode]: + [CacheErrorCodes.cacheQuotaExceeded]: "Exceeded cache storage capacity.", + [CacheErrorCodes.cacheErrorUnknown]: "Unexpected error occurred when using cache storage.", }; /** * Error thrown when there is an error with the cache */ -export class CacheError extends Error { +export class CacheError extends AuthError { /** * Short string denoting error */ @@ -32,7 +32,7 @@ export class CacheError extends Error { errorMessage || (CacheErrorMessages[errorCode] ? CacheErrorMessages[errorCode] - : CacheErrorMessages[CacheErrorCodes.cacheUnknownErrorCode]); + : CacheErrorMessages[CacheErrorCodes.cacheErrorUnknown]); super(`${errorCode}: ${message}`); Object.setPrototypeOf(this, CacheError.prototype); @@ -42,3 +42,24 @@ export class CacheError extends Error { this.errorMessage = message; } } + +/** + * Helper function to wrap browser errors in a CacheError object + * @param e + * @returns + */ +export function createCacheError(e: unknown): CacheError { + if (!(e instanceof Error)) { + return new CacheError(CacheErrorCodes.cacheErrorUnknown); + } + + if ( + e.name === "QuotaExceededError" || + e.name === "NS_ERROR_DOM_QUOTA_REACHED" || + e.message.includes("exceeded the quota") + ) { + return new CacheError(CacheErrorCodes.cacheQuotaExceeded); + } else { + return new CacheError(e.name, e.message); + } +} diff --git a/lib/msal-common/src/error/CacheErrorCodes.ts b/lib/msal-common/src/error/CacheErrorCodes.ts index a90eb3a15c..c64bdce4c0 100644 --- a/lib/msal-common/src/error/CacheErrorCodes.ts +++ b/lib/msal-common/src/error/CacheErrorCodes.ts @@ -3,5 +3,5 @@ * Licensed under the MIT License. */ -export const cacheQuotaExceededErrorCode = "cache_quota_exceeded"; -export const cacheUnknownErrorCode = "cache_error_unknown"; +export const cacheQuotaExceeded = "cache_quota_exceeded"; +export const cacheErrorUnknown = "cache_error_unknown"; diff --git a/lib/msal-common/src/error/ClientConfigurationError.ts b/lib/msal-common/src/error/ClientConfigurationError.ts index c5f560d29e..00ab9ae81c 100644 --- a/lib/msal-common/src/error/ClientConfigurationError.ts +++ b/lib/msal-common/src/error/ClientConfigurationError.ts @@ -19,8 +19,6 @@ export const ClientConfigurationErrorMessages = { [ClientConfigurationErrorCodes.urlEmptyError]: "URL was empty or null.", [ClientConfigurationErrorCodes.emptyInputScopesError]: "Scopes cannot be passed as null, undefined or empty array because they are required to obtain an access token.", - [ClientConfigurationErrorCodes.invalidPromptValue]: - "Please see here for valid configuration options: https://azuread.github.io/microsoft-authentication-library-for-js/ref/modules/_azure_msal_common.html#commonauthorizationurlrequest", [ClientConfigurationErrorCodes.invalidClaims]: "Given claims parameter must be a stringified JSON object.", [ClientConfigurationErrorCodes.tokenRequestEmpty]: @@ -94,12 +92,6 @@ export const ClientConfigurationErrorMessage = { ClientConfigurationErrorCodes.emptyInputScopesError ], }, - invalidPrompt: { - code: ClientConfigurationErrorCodes.invalidPromptValue, - desc: ClientConfigurationErrorMessages[ - ClientConfigurationErrorCodes.invalidPromptValue - ], - }, invalidClaimsRequest: { code: ClientConfigurationErrorCodes.invalidClaims, desc: ClientConfigurationErrorMessages[ diff --git a/lib/msal-common/src/error/ClientConfigurationErrorCodes.ts b/lib/msal-common/src/error/ClientConfigurationErrorCodes.ts index 2be49e6781..47bbec4386 100644 --- a/lib/msal-common/src/error/ClientConfigurationErrorCodes.ts +++ b/lib/msal-common/src/error/ClientConfigurationErrorCodes.ts @@ -9,7 +9,6 @@ export const authorityUriInsecure = "authority_uri_insecure"; export const urlParseError = "url_parse_error"; export const urlEmptyError = "empty_url_error"; export const emptyInputScopesError = "empty_input_scopes_error"; -export const invalidPromptValue = "invalid_prompt_value"; export const invalidClaims = "invalid_claims"; export const tokenRequestEmpty = "token_request_empty"; export const logoutRequestEmpty = "logout_request_empty"; diff --git a/lib/msal-common/src/error/InteractionRequiredAuthError.ts b/lib/msal-common/src/error/InteractionRequiredAuthError.ts index a3b4a4314f..05242a0acc 100644 --- a/lib/msal-common/src/error/InteractionRequiredAuthError.ts +++ b/lib/msal-common/src/error/InteractionRequiredAuthError.ts @@ -16,6 +16,7 @@ export const InteractionRequiredServerErrorMessage = [ InteractionRequiredAuthErrorCodes.consentRequired, InteractionRequiredAuthErrorCodes.loginRequired, InteractionRequiredAuthErrorCodes.badToken, + InteractionRequiredAuthErrorCodes.uxNotAllowed, ]; export const InteractionRequiredAuthSubErrorMessage = [ @@ -36,6 +37,8 @@ const InteractionRequiredAuthErrorMessages = { "Refresh token has expired.", [InteractionRequiredAuthErrorCodes.badToken]: "Identity provider returned bad_token due to an expired or invalid refresh token. Please invoke an interactive API to resolve.", + [InteractionRequiredAuthErrorCodes.uxNotAllowed]: + "`canShowUI` flag in Edge was set to false. User interaction required on web page. Please invoke an interactive API to resolve.", }; /** diff --git a/lib/msal-common/src/error/InteractionRequiredAuthErrorCodes.ts b/lib/msal-common/src/error/InteractionRequiredAuthErrorCodes.ts index c01e038b77..1c0ef88d35 100644 --- a/lib/msal-common/src/error/InteractionRequiredAuthErrorCodes.ts +++ b/lib/msal-common/src/error/InteractionRequiredAuthErrorCodes.ts @@ -7,6 +7,7 @@ export const noTokensFound = "no_tokens_found"; export const nativeAccountUnavailable = "native_account_unavailable"; export const refreshTokenExpired = "refresh_token_expired"; +export const uxNotAllowed = "ux_not_allowed"; // Codes potentially returned by server export const interactionRequired = "interaction_required"; diff --git a/lib/msal-common/src/error/NetworkError.ts b/lib/msal-common/src/error/NetworkError.ts index 0b4c9f5aa2..60b64a351f 100644 --- a/lib/msal-common/src/error/NetworkError.ts +++ b/lib/msal-common/src/error/NetworkError.ts @@ -38,7 +38,9 @@ export class NetworkError extends AuthError { export function createNetworkError( error: AuthError, httpStatus?: number, - responseHeaders?: Record + responseHeaders?: Record, + additionalError?: Error ): NetworkError { + error.errorMessage = `${error.errorMessage}, additionalErrorInfo: error.name:${additionalError?.name}, error.message:${additionalError?.message}`; return new NetworkError(error, httpStatus, responseHeaders); } diff --git a/lib/msal-common/src/exports-common.ts b/lib/msal-common/src/exports-common.ts index 6c3b191e41..01f537ba16 100644 --- a/lib/msal-common/src/exports-common.ts +++ b/lib/msal-common/src/exports-common.ts @@ -105,6 +105,8 @@ export { DEFAULT_CRYPTO_IMPLEMENTATION, SignedHttpRequestParameters, } from "./crypto/ICrypto.js"; + +export * as AuthorizeProtocol from "./protocol/Authorize.js"; export { BaseAuthRequest } from "./request/BaseAuthRequest.js"; export { CommonAuthorizationUrlRequest } from "./request/CommonAuthorizationUrlRequest.js"; export { CommonAuthorizationCodeRequest } from "./request/CommonAuthorizationCodeRequest.js"; @@ -117,7 +119,7 @@ export { AzureRegion } from "./authority/AzureRegion.js"; export { AzureRegionConfiguration } from "./authority/AzureRegionConfiguration.js"; export { AuthenticationResult } from "./response/AuthenticationResult.js"; export { AuthorizationCodePayload } from "./response/AuthorizationCodePayload.js"; -export { ServerAuthorizationCodeResponse } from "./response/ServerAuthorizationCodeResponse.js"; +export { AuthorizeResponse } from "./response/AuthorizeResponse.js"; export { ServerAuthorizationTokenResponse } from "./response/ServerAuthorizationTokenResponse.js"; export { ResponseHandler, @@ -140,7 +142,11 @@ export { } from "./error/AuthError.js"; export { ServerError } from "./error/ServerError.js"; export { NetworkError, createNetworkError } from "./error/NetworkError.js"; -export { CacheError, CacheErrorCodes } from "./error/CacheError.js"; +export { + CacheError, + CacheErrorCodes, + createCacheError, +} from "./error/CacheError.js"; export { ClientAuthError, ClientAuthErrorMessage, @@ -158,6 +164,7 @@ export { OIDC_DEFAULT_SCOPES, PromptValue, PersistentCacheKeys, + OAuthResponseType, ServerResponseType, ResponseMode, CacheOutcome, @@ -178,6 +185,7 @@ export { HttpStatus, DEFAULT_TOKEN_RENEWAL_OFFSET_SEC, JsonWebTokenTypes, + EncodingTypes, } from "./utils/Constants.js"; export { StringUtils } from "./utils/StringUtils.js"; export { StringDict } from "./utils/MsalTypes.js"; diff --git a/lib/msal-common/src/logger/Logger.ts b/lib/msal-common/src/logger/Logger.ts index ca4f0900f0..1dbab86ed6 100644 --- a/lib/msal-common/src/logger/Logger.ts +++ b/lib/msal-common/src/logger/Logger.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { LoggerOptions } from "../config/ClientConfiguration.js"; +import type { LoggerOptions } from "../config/ClientConfiguration.js"; import { Constants } from "../utils/Constants.js"; /** diff --git a/lib/msal-common/src/network/ThrottlingUtils.ts b/lib/msal-common/src/network/ThrottlingUtils.ts index 8c5f8db35f..a91bbc2f27 100644 --- a/lib/msal-common/src/network/ThrottlingUtils.ts +++ b/lib/msal-common/src/network/ThrottlingUtils.ts @@ -38,14 +38,15 @@ export class ThrottlingUtils { */ static preProcess( cacheManager: CacheManager, - thumbprint: RequestThumbprint + thumbprint: RequestThumbprint, + correlationId: string ): void { const key = ThrottlingUtils.generateThrottlingStorageKey(thumbprint); const value = cacheManager.getThrottlingCache(key); if (value) { if (value.throttleTime < Date.now()) { - cacheManager.removeItem(key); + cacheManager.removeItem(key, correlationId); return; } throw new ServerError( @@ -65,7 +66,8 @@ export class ThrottlingUtils { static postProcess( cacheManager: CacheManager, thumbprint: RequestThumbprint, - response: NetworkResponse + response: NetworkResponse, + correlationId: string ): void { if ( ThrottlingUtils.checkResponseStatus(response) || @@ -82,7 +84,8 @@ export class ThrottlingUtils { }; cacheManager.setThrottlingCache( ThrottlingUtils.generateThrottlingStorageKey(thumbprint), - thumbprintValue + thumbprintValue, + correlationId ); } } @@ -146,6 +149,6 @@ export class ThrottlingUtils { homeAccountIdentifier ); const key = this.generateThrottlingStorageKey(thumbprint); - cacheManager.removeItem(key); + cacheManager.removeItem(key, request.correlationId); } } diff --git a/lib/msal-common/src/packageMetadata.ts b/lib/msal-common/src/packageMetadata.ts index f6a518d9df..ba1fa95072 100644 --- a/lib/msal-common/src/packageMetadata.ts +++ b/lib/msal-common/src/packageMetadata.ts @@ -1,3 +1,3 @@ /* eslint-disable header/header */ export const name = "@azure/msal-common"; -export const version = "15.2.1"; +export const version = "15.8.1"; diff --git a/lib/msal-common/src/protocol/Authorize.ts b/lib/msal-common/src/protocol/Authorize.ts new file mode 100644 index 0000000000..8d17420684 --- /dev/null +++ b/lib/msal-common/src/protocol/Authorize.ts @@ -0,0 +1,415 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { CommonAuthorizationUrlRequest } from "../request/CommonAuthorizationUrlRequest.js"; +import * as RequestParameterBuilder from "../request/RequestParameterBuilder.js"; +import { IPerformanceClient } from "../telemetry/performance/IPerformanceClient.js"; +import * as AADServerParamKeys from "../constants/AADServerParamKeys.js"; +import { AuthOptions } from "../config/ClientConfiguration.js"; +import { PromptValue } from "../utils/Constants.js"; +import { AccountInfo } from "../account/AccountInfo.js"; +import { Logger } from "../logger/Logger.js"; +import { buildClientInfoFromHomeAccountId } from "../account/ClientInfo.js"; +import { Authority } from "../authority/Authority.js"; +import { mapToQueryString } from "../utils/UrlUtils.js"; +import { UrlString } from "../url/UrlString.js"; +import { AuthorizationCodePayload } from "../response/AuthorizationCodePayload.js"; +import { AuthorizeResponse } from "../response/AuthorizeResponse.js"; +import { + ClientAuthErrorCodes, + createClientAuthError, +} from "../error/ClientAuthError.js"; +import { + InteractionRequiredAuthError, + isInteractionRequiredError, +} from "../error/InteractionRequiredAuthError.js"; +import { ServerError } from "../error/ServerError.js"; +import { StringDict } from "../utils/MsalTypes.js"; + +/** + * Returns map of parameters that are applicable to all calls to /authorize whether using PKCE or EAR + * @param config + * @param request + * @param logger + * @param performanceClient + * @returns + */ +export function getStandardAuthorizeRequestParameters( + authOptions: AuthOptions, + request: CommonAuthorizationUrlRequest, + logger: Logger, + performanceClient?: IPerformanceClient +): Map { + // generate the correlationId if not set by the user and add + const correlationId = request.correlationId; + + const parameters = new Map(); + + RequestParameterBuilder.addClientId( + parameters, + request.embeddedClientId || + request.extraQueryParameters?.[AADServerParamKeys.CLIENT_ID] || + authOptions.clientId + ); + + const requestScopes = [ + ...(request.scopes || []), + ...(request.extraScopesToConsent || []), + ]; + RequestParameterBuilder.addScopes( + parameters, + requestScopes, + true, + authOptions.authority.options.OIDCOptions?.defaultScopes + ); + + RequestParameterBuilder.addRedirectUri(parameters, request.redirectUri); + + RequestParameterBuilder.addCorrelationId(parameters, correlationId); + + // add response_mode. If not passed in it defaults to query. + RequestParameterBuilder.addResponseMode(parameters, request.responseMode); + + // add client_info=1 + RequestParameterBuilder.addClientInfo(parameters); + + if (request.prompt) { + RequestParameterBuilder.addPrompt(parameters, request.prompt); + performanceClient?.addFields({ prompt: request.prompt }, correlationId); + } + + if (request.domainHint) { + RequestParameterBuilder.addDomainHint(parameters, request.domainHint); + performanceClient?.addFields( + { domainHintFromRequest: true }, + correlationId + ); + } + + // Add sid or loginHint with preference for login_hint claim (in request) -> sid -> loginHint (upn/email) -> username of AccountInfo object + if (request.prompt !== PromptValue.SELECT_ACCOUNT) { + // AAD will throw if prompt=select_account is passed with an account hint + if (request.sid && request.prompt === PromptValue.NONE) { + // SessionID is only used in silent calls + logger.verbose( + "createAuthCodeUrlQueryString: Prompt is none, adding sid from request" + ); + RequestParameterBuilder.addSid(parameters, request.sid); + performanceClient?.addFields( + { sidFromRequest: true }, + correlationId + ); + } else if (request.account) { + const accountSid = extractAccountSid(request.account); + let accountLoginHintClaim = extractLoginHint(request.account); + + if (accountLoginHintClaim && request.domainHint) { + logger.warning( + `AuthorizationCodeClient.createAuthCodeUrlQueryString: "domainHint" param is set, skipping opaque "login_hint" claim. Please consider not passing domainHint` + ); + accountLoginHintClaim = null; + } + + // If login_hint claim is present, use it over sid/username + if (accountLoginHintClaim) { + logger.verbose( + "createAuthCodeUrlQueryString: login_hint claim present on account" + ); + RequestParameterBuilder.addLoginHint( + parameters, + accountLoginHintClaim + ); + performanceClient?.addFields( + { loginHintFromClaim: true }, + correlationId + ); + try { + const clientInfo = buildClientInfoFromHomeAccountId( + request.account.homeAccountId + ); + RequestParameterBuilder.addCcsOid(parameters, clientInfo); + } catch (e) { + logger.verbose( + "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" + ); + } + } else if (accountSid && request.prompt === PromptValue.NONE) { + /* + * If account and loginHint are provided, we will check account first for sid before adding loginHint + * SessionId is only used in silent calls + */ + logger.verbose( + "createAuthCodeUrlQueryString: Prompt is none, adding sid from account" + ); + RequestParameterBuilder.addSid(parameters, accountSid); + performanceClient?.addFields( + { sidFromClaim: true }, + correlationId + ); + try { + const clientInfo = buildClientInfoFromHomeAccountId( + request.account.homeAccountId + ); + RequestParameterBuilder.addCcsOid(parameters, clientInfo); + } catch (e) { + logger.verbose( + "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" + ); + } + } else if (request.loginHint) { + logger.verbose( + "createAuthCodeUrlQueryString: Adding login_hint from request" + ); + RequestParameterBuilder.addLoginHint( + parameters, + request.loginHint + ); + RequestParameterBuilder.addCcsUpn( + parameters, + request.loginHint + ); + performanceClient?.addFields( + { loginHintFromRequest: true }, + correlationId + ); + } else if (request.account.username) { + // Fallback to account username if provided + logger.verbose( + "createAuthCodeUrlQueryString: Adding login_hint from account" + ); + RequestParameterBuilder.addLoginHint( + parameters, + request.account.username + ); + performanceClient?.addFields( + { loginHintFromUpn: true }, + correlationId + ); + try { + const clientInfo = buildClientInfoFromHomeAccountId( + request.account.homeAccountId + ); + RequestParameterBuilder.addCcsOid(parameters, clientInfo); + } catch (e) { + logger.verbose( + "createAuthCodeUrlQueryString: Could not parse home account ID for CCS Header" + ); + } + } + } else if (request.loginHint) { + logger.verbose( + "createAuthCodeUrlQueryString: No account, adding login_hint from request" + ); + RequestParameterBuilder.addLoginHint(parameters, request.loginHint); + RequestParameterBuilder.addCcsUpn(parameters, request.loginHint); + performanceClient?.addFields( + { loginHintFromRequest: true }, + correlationId + ); + } + } else { + logger.verbose( + "createAuthCodeUrlQueryString: Prompt is select_account, ignoring account hints" + ); + } + + if (request.nonce) { + RequestParameterBuilder.addNonce(parameters, request.nonce); + } + + if (request.state) { + RequestParameterBuilder.addState(parameters, request.state); + } + + if ( + request.claims || + (authOptions.clientCapabilities && + authOptions.clientCapabilities.length > 0) + ) { + RequestParameterBuilder.addClaims( + parameters, + request.claims, + authOptions.clientCapabilities + ); + } + + if (request.embeddedClientId) { + RequestParameterBuilder.addBrokerParameters( + parameters, + authOptions.clientId, + authOptions.redirectUri + ); + } + + // If extraQueryParameters includes instance_aware its value will be added when extraQueryParameters are added + if ( + authOptions.instanceAware && + (!request.extraQueryParameters || + !Object.keys(request.extraQueryParameters).includes( + AADServerParamKeys.INSTANCE_AWARE + )) + ) { + RequestParameterBuilder.addInstanceAware(parameters); + } + + return parameters; +} + +/** + * Returns authorize endpoint with given request parameters in the query string + * @param authority + * @param requestParameters + * @returns + */ +export function getAuthorizeUrl( + authority: Authority, + requestParameters: Map, + encodeParams?: boolean, + extraQueryParameters?: StringDict | undefined +): string { + const queryString = mapToQueryString( + requestParameters, + encodeParams, + extraQueryParameters + ); + return UrlString.appendQueryString( + authority.authorizationEndpoint, + queryString + ); +} + +/** + * Handles the hash fragment response from public client code request. Returns a code response used by + * the client to exchange for a token in acquireToken. + * @param serverParams + * @param cachedState + */ +export function getAuthorizationCodePayload( + serverParams: AuthorizeResponse, + cachedState: string +): AuthorizationCodePayload { + // Get code response + validateAuthorizationResponse(serverParams, cachedState); + + // throw when there is no auth code in the response + if (!serverParams.code) { + throw createClientAuthError( + ClientAuthErrorCodes.authorizationCodeMissingFromServerResponse + ); + } + + return serverParams as AuthorizationCodePayload; +} + +/** + * Function which validates server authorization code response. + * @param serverResponseHash + * @param requestState + */ +export function validateAuthorizationResponse( + serverResponse: AuthorizeResponse, + requestState: string +): void { + if (!serverResponse.state || !requestState) { + throw serverResponse.state + ? createClientAuthError( + ClientAuthErrorCodes.stateNotFound, + "Cached State" + ) + : createClientAuthError( + ClientAuthErrorCodes.stateNotFound, + "Server State" + ); + } + + let decodedServerResponseState: string; + let decodedRequestState: string; + + try { + decodedServerResponseState = decodeURIComponent(serverResponse.state); + } catch (e) { + throw createClientAuthError( + ClientAuthErrorCodes.invalidState, + serverResponse.state + ); + } + + try { + decodedRequestState = decodeURIComponent(requestState); + } catch (e) { + throw createClientAuthError( + ClientAuthErrorCodes.invalidState, + serverResponse.state + ); + } + + if (decodedServerResponseState !== decodedRequestState) { + throw createClientAuthError(ClientAuthErrorCodes.stateMismatch); + } + + // Check for error + if ( + serverResponse.error || + serverResponse.error_description || + serverResponse.suberror + ) { + const serverErrorNo = parseServerErrorNo(serverResponse); + if ( + isInteractionRequiredError( + serverResponse.error, + serverResponse.error_description, + serverResponse.suberror + ) + ) { + throw new InteractionRequiredAuthError( + serverResponse.error || "", + serverResponse.error_description, + serverResponse.suberror, + serverResponse.timestamp || "", + serverResponse.trace_id || "", + serverResponse.correlation_id || "", + serverResponse.claims || "", + serverErrorNo + ); + } + + throw new ServerError( + serverResponse.error || "", + serverResponse.error_description, + serverResponse.suberror, + serverErrorNo + ); + } +} + +/** + * Get server error No from the error_uri + * @param serverResponse + * @returns + */ +function parseServerErrorNo( + serverResponse: AuthorizeResponse +): string | undefined { + const errorCodePrefix = "code="; + const errorCodePrefixIndex = + serverResponse.error_uri?.lastIndexOf(errorCodePrefix); + return errorCodePrefixIndex && errorCodePrefixIndex >= 0 + ? serverResponse.error_uri?.substring( + errorCodePrefixIndex + errorCodePrefix.length + ) + : undefined; +} + +/** + * Helper to get sid from account. Returns null if idTokenClaims are not present or sid is not present. + * @param account + */ +function extractAccountSid(account: AccountInfo): string | null { + return account.idTokenClaims?.sid || null; +} + +function extractLoginHint(account: AccountInfo): string | null { + return account.idTokenClaims?.login_hint || null; +} diff --git a/lib/msal-common/src/request/BaseAuthRequest.ts b/lib/msal-common/src/request/BaseAuthRequest.ts index fcaf8c9d44..7669281c99 100644 --- a/lib/msal-common/src/request/BaseAuthRequest.ts +++ b/lib/msal-common/src/request/BaseAuthRequest.ts @@ -4,7 +4,7 @@ */ import { AuthenticationScheme } from "../utils/Constants.js"; -import { AzureCloudOptions } from "../config/ClientConfiguration.js"; +import type { AzureCloudOptions } from "../config/ClientConfiguration.js"; import { StringDict } from "../utils/MsalTypes.js"; import { StoreInCache } from "./StoreInCache.js"; import { ShrOptions } from "../crypto/SignedHttpRequest.js"; diff --git a/lib/msal-common/src/request/CommonAuthorizationUrlRequest.ts b/lib/msal-common/src/request/CommonAuthorizationUrlRequest.ts index 97039539ce..27cc217316 100644 --- a/lib/msal-common/src/request/CommonAuthorizationUrlRequest.ts +++ b/lib/msal-common/src/request/CommonAuthorizationUrlRequest.ts @@ -42,15 +42,16 @@ export type CommonAuthorizationUrlRequest = BaseAuthRequest & { redirectUri: string; responseMode: ResponseMode; account?: AccountInfo; + earJwk?: string; codeChallenge?: string; codeChallengeMethod?: string; domainHint?: string; extraQueryParameters?: StringDict; extraScopesToConsent?: Array; loginHint?: string; - nonce?: string; + nonce: string; prompt?: string; sid?: string; - state?: string; + state: string; platformBroker?: boolean; }; diff --git a/lib/msal-common/src/request/RequestParameterBuilder.ts b/lib/msal-common/src/request/RequestParameterBuilder.ts index ee91695e67..70555833b4 100644 --- a/lib/msal-common/src/request/RequestParameterBuilder.ts +++ b/lib/msal-common/src/request/RequestParameterBuilder.ts @@ -4,7 +4,6 @@ */ import { - Constants, ResponseMode, CLIENT_INFO, AuthenticationScheme, @@ -13,6 +12,7 @@ import { OIDC_DEFAULT_SCOPES, ThrottlingConstants, HeaderNames, + OAuthResponseType, } from "../utils/Constants.js"; import * as AADServerParamKeys from "../constants/AADServerParamKeys.js"; import { ScopeSet } from "./ScopeSet.js"; @@ -53,27 +53,15 @@ export function instrumentBrokerParams( } /** - * add response_type = code + * Add the given response_type + * @param parameters + * @param responseType */ -export function addResponseTypeCode(parameters: Map): void { - parameters.set( - AADServerParamKeys.RESPONSE_TYPE, - encodeURIComponent(Constants.CODE_RESPONSE_TYPE) - ); -} - -/** - * add response_type = token id_token - */ -export function addResponseTypeForTokenAndIdToken( - parameters: Map +export function addResponseType( + parameters: Map, + responseType: OAuthResponseType ): void { - parameters.set( - AADServerParamKeys.RESPONSE_TYPE, - encodeURIComponent( - `${Constants.TOKEN_RESPONSE_TYPE} ${Constants.ID_TOKEN_RESPONSE_TYPE}` - ) - ); + parameters.set(AADServerParamKeys.RESPONSE_TYPE, responseType); } /** @@ -86,7 +74,7 @@ export function addResponseMode( ): void { parameters.set( AADServerParamKeys.RESPONSE_MODE, - encodeURIComponent(responseMode ? responseMode : ResponseMode.QUERY) + responseMode ? responseMode : ResponseMode.QUERY ); } @@ -94,7 +82,7 @@ export function addResponseMode( * Add flag to indicate STS should attempt to use WAM if available */ export function addNativeBroker(parameters: Map): void { - parameters.set(AADServerParamKeys.NATIVE_BROKER, encodeURIComponent("1")); + parameters.set(AADServerParamKeys.NATIVE_BROKER, "1"); } /** @@ -120,10 +108,7 @@ export function addScopes( ? [...(scopes || []), ...defaultScopes] : scopes || []; const scopeSet = new ScopeSet(requestScopes); - parameters.set( - AADServerParamKeys.SCOPE, - encodeURIComponent(scopeSet.printScopes()) - ); + parameters.set(AADServerParamKeys.SCOPE, scopeSet.printScopes()); } /** @@ -134,7 +119,7 @@ export function addClientId( parameters: Map, clientId: string ): void { - parameters.set(AADServerParamKeys.CLIENT_ID, encodeURIComponent(clientId)); + parameters.set(AADServerParamKeys.CLIENT_ID, clientId); } /** @@ -145,10 +130,7 @@ export function addRedirectUri( parameters: Map, redirectUri: string ): void { - parameters.set( - AADServerParamKeys.REDIRECT_URI, - encodeURIComponent(redirectUri) - ); + parameters.set(AADServerParamKeys.REDIRECT_URI, redirectUri); } /** @@ -159,10 +141,7 @@ export function addPostLogoutRedirectUri( parameters: Map, redirectUri: string ): void { - parameters.set( - AADServerParamKeys.POST_LOGOUT_URI, - encodeURIComponent(redirectUri) - ); + parameters.set(AADServerParamKeys.POST_LOGOUT_URI, redirectUri); } /** @@ -173,10 +152,7 @@ export function addIdTokenHint( parameters: Map, idTokenHint: string ): void { - parameters.set( - AADServerParamKeys.ID_TOKEN_HINT, - encodeURIComponent(idTokenHint) - ); + parameters.set(AADServerParamKeys.ID_TOKEN_HINT, idTokenHint); } /** @@ -187,10 +163,7 @@ export function addDomainHint( parameters: Map, domainHint: string ): void { - parameters.set( - AADServerParamKeys.DOMAIN_HINT, - encodeURIComponent(domainHint) - ); + parameters.set(AADServerParamKeys.DOMAIN_HINT, domainHint); } /** @@ -201,10 +174,7 @@ export function addLoginHint( parameters: Map, loginHint: string ): void { - parameters.set( - AADServerParamKeys.LOGIN_HINT, - encodeURIComponent(loginHint) - ); + parameters.set(AADServerParamKeys.LOGIN_HINT, loginHint); } /** @@ -215,10 +185,7 @@ export function addCcsUpn( parameters: Map, loginHint: string ): void { - parameters.set( - HeaderNames.CCS_HEADER, - encodeURIComponent(`UPN:${loginHint}`) - ); + parameters.set(HeaderNames.CCS_HEADER, `UPN:${loginHint}`); } /** @@ -231,7 +198,7 @@ export function addCcsOid( ): void { parameters.set( HeaderNames.CCS_HEADER, - encodeURIComponent(`Oid:${clientInfo.uid}@${clientInfo.utid}`) + `Oid:${clientInfo.uid}@${clientInfo.utid}` ); } @@ -240,7 +207,7 @@ export function addCcsOid( * @param sid */ export function addSid(parameters: Map, sid: string): void { - parameters.set(AADServerParamKeys.SID, encodeURIComponent(sid)); + parameters.set(AADServerParamKeys.SID, sid); } /** @@ -263,7 +230,7 @@ export function addClaims( ClientConfigurationErrorCodes.invalidClaims ); } - parameters.set(AADServerParamKeys.CLAIMS, encodeURIComponent(mergedClaims)); + parameters.set(AADServerParamKeys.CLAIMS, mergedClaims); } /** @@ -274,10 +241,7 @@ export function addCorrelationId( parameters: Map, correlationId: string ): void { - parameters.set( - AADServerParamKeys.CLIENT_REQUEST_ID, - encodeURIComponent(correlationId) - ); + parameters.set(AADServerParamKeys.CLIENT_REQUEST_ID, correlationId); } /** @@ -324,7 +288,7 @@ export function addPrompt( parameters: Map, prompt: string ): void { - parameters.set(`${AADServerParamKeys.PROMPT}`, encodeURIComponent(prompt)); + parameters.set(AADServerParamKeys.PROMPT, prompt); } /** @@ -333,7 +297,7 @@ export function addPrompt( */ export function addState(parameters: Map, state: string): void { if (state) { - parameters.set(AADServerParamKeys.STATE, encodeURIComponent(state)); + parameters.set(AADServerParamKeys.STATE, state); } } @@ -342,7 +306,7 @@ export function addState(parameters: Map, state: string): void { * @param nonce */ export function addNonce(parameters: Map, nonce: string): void { - parameters.set(AADServerParamKeys.NONCE, encodeURIComponent(nonce)); + parameters.set(AADServerParamKeys.NONCE, nonce); } /** @@ -353,17 +317,14 @@ export function addNonce(parameters: Map, nonce: string): void { */ export function addCodeChallengeParams( parameters: Map, - codeChallenge: string, - codeChallengeMethod: string + codeChallenge?: string, + codeChallengeMethod?: string ): void { if (codeChallenge && codeChallengeMethod) { - parameters.set( - AADServerParamKeys.CODE_CHALLENGE, - encodeURIComponent(codeChallenge) - ); + parameters.set(AADServerParamKeys.CODE_CHALLENGE, codeChallenge); parameters.set( AADServerParamKeys.CODE_CHALLENGE_METHOD, - encodeURIComponent(codeChallengeMethod) + codeChallengeMethod ); } else { throw createClientConfigurationError( @@ -380,7 +341,7 @@ export function addAuthorizationCode( parameters: Map, code: string ): void { - parameters.set(AADServerParamKeys.CODE, encodeURIComponent(code)); + parameters.set(AADServerParamKeys.CODE, code); } /** @@ -391,7 +352,7 @@ export function addDeviceCode( parameters: Map, code: string ): void { - parameters.set(AADServerParamKeys.DEVICE_CODE, encodeURIComponent(code)); + parameters.set(AADServerParamKeys.DEVICE_CODE, code); } /** @@ -402,10 +363,7 @@ export function addRefreshToken( parameters: Map, refreshToken: string ): void { - parameters.set( - AADServerParamKeys.REFRESH_TOKEN, - encodeURIComponent(refreshToken) - ); + parameters.set(AADServerParamKeys.REFRESH_TOKEN, refreshToken); } /** @@ -416,10 +374,7 @@ export function addCodeVerifier( parameters: Map, codeVerifier: string ): void { - parameters.set( - AADServerParamKeys.CODE_VERIFIER, - encodeURIComponent(codeVerifier) - ); + parameters.set(AADServerParamKeys.CODE_VERIFIER, codeVerifier); } /** @@ -430,10 +385,7 @@ export function addClientSecret( parameters: Map, clientSecret: string ): void { - parameters.set( - AADServerParamKeys.CLIENT_SECRET, - encodeURIComponent(clientSecret) - ); + parameters.set(AADServerParamKeys.CLIENT_SECRET, clientSecret); } /** @@ -445,10 +397,7 @@ export function addClientAssertion( clientAssertion: string ): void { if (clientAssertion) { - parameters.set( - AADServerParamKeys.CLIENT_ASSERTION, - encodeURIComponent(clientAssertion) - ); + parameters.set(AADServerParamKeys.CLIENT_ASSERTION, clientAssertion); } } @@ -463,7 +412,7 @@ export function addClientAssertionType( if (clientAssertionType) { parameters.set( AADServerParamKeys.CLIENT_ASSERTION_TYPE, - encodeURIComponent(clientAssertionType) + clientAssertionType ); } } @@ -476,10 +425,7 @@ export function addOboAssertion( parameters: Map, oboAssertion: string ): void { - parameters.set( - AADServerParamKeys.OBO_ASSERTION, - encodeURIComponent(oboAssertion) - ); + parameters.set(AADServerParamKeys.OBO_ASSERTION, oboAssertion); } /** @@ -490,10 +436,7 @@ export function addRequestTokenUse( parameters: Map, tokenUse: string ): void { - parameters.set( - AADServerParamKeys.REQUESTED_TOKEN_USE, - encodeURIComponent(tokenUse) - ); + parameters.set(AADServerParamKeys.REQUESTED_TOKEN_USE, tokenUse); } /** @@ -504,10 +447,7 @@ export function addGrantType( parameters: Map, grantType: string ): void { - parameters.set( - AADServerParamKeys.GRANT_TYPE, - encodeURIComponent(grantType) - ); + parameters.set(AADServerParamKeys.GRANT_TYPE, grantType); } /** @@ -582,10 +522,7 @@ export function addUsername( parameters: Map, username: string ): void { - parameters.set( - PasswordGrantConstants.username, - encodeURIComponent(username) - ); + parameters.set(PasswordGrantConstants.username, username); } /** @@ -596,10 +533,7 @@ export function addPassword( parameters: Map, password: string ): void { - parameters.set( - PasswordGrantConstants.password, - encodeURIComponent(password) - ); + parameters.set(PasswordGrantConstants.password, password); } /** @@ -612,10 +546,7 @@ export function addPopToken( ): void { if (cnfString) { parameters.set(AADServerParamKeys.TOKEN_TYPE, AuthenticationScheme.POP); - parameters.set( - AADServerParamKeys.REQ_CNF, - encodeURIComponent(cnfString) - ); + parameters.set(AADServerParamKeys.REQ_CNF, cnfString); } } @@ -628,10 +559,7 @@ export function addSshJwk( ): void { if (sshJwkString) { parameters.set(AADServerParamKeys.TOKEN_TYPE, AuthenticationScheme.SSH); - parameters.set( - AADServerParamKeys.REQ_CNF, - encodeURIComponent(sshJwkString) - ); + parameters.set(AADServerParamKeys.REQ_CNF, sshJwkString); } } @@ -670,10 +598,7 @@ export function addLogoutHint( parameters: Map, logoutHint: string ): void { - parameters.set( - AADServerParamKeys.LOGOUT_HINT, - encodeURIComponent(logoutHint) - ); + parameters.set(AADServerParamKeys.LOGOUT_HINT, logoutHint); } export function addBrokerParameters( @@ -682,15 +607,28 @@ export function addBrokerParameters( brokerRedirectUri: string ): void { if (!parameters.has(AADServerParamKeys.BROKER_CLIENT_ID)) { - parameters.set( - AADServerParamKeys.BROKER_CLIENT_ID, - encodeURIComponent(brokerClientId) - ); + parameters.set(AADServerParamKeys.BROKER_CLIENT_ID, brokerClientId); } if (!parameters.has(AADServerParamKeys.BROKER_REDIRECT_URI)) { parameters.set( AADServerParamKeys.BROKER_REDIRECT_URI, - encodeURIComponent(brokerRedirectUri) + brokerRedirectUri ); } } + +/** + * Add EAR (Encrypted Authorize Response) request parameters + * @param parameters + * @param jwk + */ +export function addEARParameters( + parameters: Map, + jwk: string +): void { + parameters.set(AADServerParamKeys.EAR_JWK, encodeURIComponent(jwk)); + + // ear_jwe_crypto will always have value: {"alg":"dir","enc":"A256GCM"} so we can hardcode this + const jweCryptoB64Encoded = "eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIn0"; + parameters.set(AADServerParamKeys.EAR_JWE_CRYPTO, jweCryptoB64Encoded); +} diff --git a/lib/msal-common/src/request/RequestValidator.ts b/lib/msal-common/src/request/RequestValidator.ts deleted file mode 100644 index 96b471129d..0000000000 --- a/lib/msal-common/src/request/RequestValidator.ts +++ /dev/null @@ -1,90 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { - createClientConfigurationError, - ClientConfigurationErrorCodes, -} from "../error/ClientConfigurationError.js"; -import { PromptValue, CodeChallengeMethodValues } from "../utils/Constants.js"; - -/** - * Validates server consumable params from the "request" objects - */ -export class RequestValidator { - /** - * Utility to check if the `redirectUri` in the request is a non-null value - * @param redirectUri - */ - static validateRedirectUri(redirectUri: string): void { - if (!redirectUri) { - throw createClientConfigurationError( - ClientConfigurationErrorCodes.redirectUriEmpty - ); - } - } - - /** - * Utility to validate prompt sent by the user in the request - * @param prompt - */ - static validatePrompt(prompt: string): void { - const promptValues = []; - - for (const value in PromptValue) { - promptValues.push(PromptValue[value]); - } - - if (promptValues.indexOf(prompt) < 0) { - throw createClientConfigurationError( - ClientConfigurationErrorCodes.invalidPromptValue - ); - } - } - - static validateClaims(claims: string): void { - try { - JSON.parse(claims); - } catch (e) { - throw createClientConfigurationError( - ClientConfigurationErrorCodes.invalidClaims - ); - } - } - - /** - * Utility to validate code_challenge and code_challenge_method - * @param codeChallenge - * @param codeChallengeMethod - */ - static validateCodeChallengeParams( - codeChallenge: string, - codeChallengeMethod: string - ): void { - if (!codeChallenge || !codeChallengeMethod) { - throw createClientConfigurationError( - ClientConfigurationErrorCodes.pkceParamsMissing - ); - } else { - this.validateCodeChallengeMethod(codeChallengeMethod); - } - } - - /** - * Utility to validate code_challenge_method - * @param codeChallengeMethod - */ - static validateCodeChallengeMethod(codeChallengeMethod: string): void { - if ( - [ - CodeChallengeMethodValues.PLAIN, - CodeChallengeMethodValues.S256, - ].indexOf(codeChallengeMethod) < 0 - ) { - throw createClientConfigurationError( - ClientConfigurationErrorCodes.invalidCodeChallengeMethod - ); - } - } -} diff --git a/lib/msal-common/src/response/AuthorizeResponse.ts b/lib/msal-common/src/response/AuthorizeResponse.ts new file mode 100644 index 0000000000..ebf8234d24 --- /dev/null +++ b/lib/msal-common/src/response/AuthorizeResponse.ts @@ -0,0 +1,80 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * Response properties that may be returned by the /authorize endpoint + */ +export type AuthorizeResponse = { + /** + * Authorization Code to be exchanged for tokens + */ + code?: string; + /** + * Encrypted Authorize Response (EAR) JWE + */ + ear_jwe?: string; + /** + * Client info object containing UserId and TenantId + */ + client_info?: string; + /** + * State string, should match what was sent on request + */ + state?: string; + /** + * Cloud instance returned when application is instance aware + */ + cloud_instance_name?: string; + /** + * Cloud instance hostname returned when application is instance aware + */ + cloud_instance_host_name?: string; + /** + * AAD Graph hostname returned when application is instance aware + * https://docs.microsoft.com/en-us/azure/active-directory/develop/active-directory-graph-api + */ + cloud_graph_host_name?: string; + /** + * Microsoft Graph hostname returned when application is instance aware + * https://docs.microsoft.com/en-us/graph/overview + */ + msgraph_host?: string; + /** + * Server error code + */ + error?: string; + /** + * Server error URI + */ + error_uri?: string; + /** + * Server error description + */ + error_description?: string; + /** + * Server Sub-Error + */ + suberror?: string; + /** + * Timestamp of request + */ + timestamp?: string; + /** + * Trace Id used to look up request in logs + */ + trace_id?: string; + /** + * Correlation ID use to look up request in logs + */ + correlation_id?: string; + /** + * Claims + */ + claims?: string; + /** + * AccountId for the user, returned when platform broker is available to use + */ + accountId?: string; +}; diff --git a/lib/msal-common/src/response/ResponseHandler.ts b/lib/msal-common/src/response/ResponseHandler.ts index ff4b1a18ae..f01df79ba9 100644 --- a/lib/msal-common/src/response/ResponseHandler.ts +++ b/lib/msal-common/src/response/ResponseHandler.ts @@ -9,7 +9,6 @@ import { ClientAuthErrorCodes, createClientAuthError, } from "../error/ClientAuthError.js"; -import { ServerAuthorizationCodeResponse } from "./ServerAuthorizationCodeResponse.js"; import { Logger } from "../logger/Logger.js"; import { ServerError } from "../error/ServerError.js"; import { ScopeSet } from "../request/ScopeSet.js"; @@ -54,19 +53,6 @@ import { import * as CacheHelpers from "../cache/utils/CacheHelpers.js"; import * as TimeUtils from "../utils/TimeUtils.js"; -function parseServerErrorNo( - serverResponse: ServerAuthorizationCodeResponse -): string | undefined { - const errorCodePrefix = "code="; - const errorCodePrefixIndex = - serverResponse.error_uri?.lastIndexOf(errorCodePrefix); - return errorCodePrefixIndex && errorCodePrefixIndex >= 0 - ? serverResponse.error_uri?.substring( - errorCodePrefixIndex + errorCodePrefix.length - ) - : undefined; -} - /** * Class that handles response parsing. * @internal @@ -99,90 +85,6 @@ export class ResponseHandler { this.performanceClient = performanceClient; } - /** - * Function which validates server authorization code response. - * @param serverResponseHash - * @param requestState - * @param cryptoObj - */ - validateServerAuthorizationCodeResponse( - serverResponse: ServerAuthorizationCodeResponse, - requestState: string - ): void { - if (!serverResponse.state || !requestState) { - throw serverResponse.state - ? createClientAuthError( - ClientAuthErrorCodes.stateNotFound, - "Cached State" - ) - : createClientAuthError( - ClientAuthErrorCodes.stateNotFound, - "Server State" - ); - } - - let decodedServerResponseState: string; - let decodedRequestState: string; - - try { - decodedServerResponseState = decodeURIComponent( - serverResponse.state - ); - } catch (e) { - throw createClientAuthError( - ClientAuthErrorCodes.invalidState, - serverResponse.state - ); - } - - try { - decodedRequestState = decodeURIComponent(requestState); - } catch (e) { - throw createClientAuthError( - ClientAuthErrorCodes.invalidState, - serverResponse.state - ); - } - - if (decodedServerResponseState !== decodedRequestState) { - throw createClientAuthError(ClientAuthErrorCodes.stateMismatch); - } - - // Check for error - if ( - serverResponse.error || - serverResponse.error_description || - serverResponse.suberror - ) { - const serverErrorNo = parseServerErrorNo(serverResponse); - if ( - isInteractionRequiredError( - serverResponse.error, - serverResponse.error_description, - serverResponse.suberror - ) - ) { - throw new InteractionRequiredAuthError( - serverResponse.error || "", - serverResponse.error_description, - serverResponse.suberror, - serverResponse.timestamp || "", - serverResponse.trace_id || "", - serverResponse.correlation_id || "", - serverResponse.claims || "", - serverErrorNo - ); - } - - throw new ServerError( - serverResponse.error || "", - serverResponse.error_description, - serverResponse.suberror, - serverErrorNo - ); - } - } - /** * Function which validates server authorization token response. * @param serverResponse @@ -377,7 +279,10 @@ export class ResponseHandler { cacheRecord.account ) { const key = cacheRecord.account.generateAccountKey(); - const account = this.cacheStorage.getAccount(key); + const account = this.cacheStorage.getAccount( + key, + request.correlationId + ); if (!account) { this.logger.warning( "Account used to refresh tokens not in persistence, refreshed tokens will not be stored in the cache" @@ -467,6 +372,7 @@ export class ResponseHandler { authority, this.homeAccountIdentifier, this.cryptoObj.base64Decode, + request.correlationId, idTokenClaims, serverTokenResponse.client_info, env, @@ -708,6 +614,7 @@ export function buildAccountToCache( authority: Authority, homeAccountId: string, base64Decode: (input: string) => string, + correlationId: string, idTokenClaims?: TokenClaims, clientInfo?: string, environment?: string, @@ -726,7 +633,7 @@ export function buildAccountToCache( let cachedAccount: AccountEntity | null = null; if (baseAccountKey) { - cachedAccount = cacheStorage.getAccount(baseAccountKey); + cachedAccount = cacheStorage.getAccount(baseAccountKey, correlationId); } const baseAccount = diff --git a/lib/msal-common/src/response/ServerAuthorizationCodeResponse.ts b/lib/msal-common/src/response/ServerAuthorizationCodeResponse.ts deleted file mode 100644 index 26468c8fc8..0000000000 --- a/lib/msal-common/src/response/ServerAuthorizationCodeResponse.ts +++ /dev/null @@ -1,34 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -/** - * Deserialized response object from server authorization code request. - * - code: authorization code from server - * - client_info: client info object - * - state: OAuth2 request state - * - error: error sent back in hash - * - error: description - */ -export type ServerAuthorizationCodeResponse = { - // Success case - code?: string; - client_info?: string; - state?: string; - cloud_instance_name?: string; - cloud_instance_host_name?: string; - cloud_graph_host_name?: string; - msgraph_host?: string; - // Error case - error?: string; - error_uri?: string; - error_description?: string; - suberror?: string; - timestamp?: string; - trace_id?: string; - correlation_id?: string; - claims?: string; - // Native Account ID - accountId?: string; -}; diff --git a/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts b/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts index a8171a02c4..da632d012b 100644 --- a/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts +++ b/lib/msal-common/src/telemetry/performance/PerformanceEvent.ts @@ -214,19 +214,21 @@ export const PerformanceEvents = { "standardInteractionClientGetClientConfiguration", StandardInteractionClientInitializeAuthorizationRequest: "standardInteractionClientInitializeAuthorizationRequest", - StandardInteractionClientInitializeAuthorizationCodeRequest: - "standardInteractionClientInitializeAuthorizationCodeRequest", /** * getAuthCodeUrl API (msal-browser and msal-node). */ GetAuthCodeUrl: "getAuthCodeUrl", + GetStandardParams: "getStandardParams", /** * Functions from InteractionHandler (msal-browser) */ HandleCodeResponseFromServer: "handleCodeResponseFromServer", HandleCodeResponse: "handleCodeResponse", + HandleResponseEar: "handleResponseEar", + HandleResponsePlatformBroker: "handleResponsePlatformBroker", + HandleResponseCode: "handleResponseCode", UpdateTokenEndpointAuthority: "updateTokenEndpointAuthority", /** @@ -235,7 +237,6 @@ export const PerformanceEvents = { AuthClientAcquireToken: "authClientAcquireToken", AuthClientExecuteTokenRequest: "authClientExecuteTokenRequest", AuthClientCreateTokenRequestBody: "authClientCreateTokenRequestBody", - AuthClientCreateQueryString: "authClientCreateQueryString", /** * Generate functions in PopTokenGenerator (msal-common) @@ -317,6 +318,8 @@ export const PerformanceEvents = { UrlEncodeArr: "urlEncodeArr", Encrypt: "encrypt", Decrypt: "decrypt", + GenerateEarKey: "generateEarKey", + DecryptEarResponse: "decryptEarResponse", } as const; export type PerformanceEvents = (typeof PerformanceEvents)[keyof typeof PerformanceEvents]; @@ -433,10 +436,6 @@ export const PerformanceEventAbbreviations: ReadonlyMap = PerformanceEvents.StandardInteractionClientInitializeAuthorizationRequest, "StdIntClientInitAuthReq", ], - [ - PerformanceEvents.StandardInteractionClientInitializeAuthorizationCodeRequest, - "StdIntClientInitAuthCodeReq", - ], [PerformanceEvents.GetAuthCodeUrl, "GetAuthCodeUrl"], @@ -445,6 +444,12 @@ export const PerformanceEventAbbreviations: ReadonlyMap = "HandleCodeResFromServer", ], [PerformanceEvents.HandleCodeResponse, "HandleCodeResp"], + [PerformanceEvents.HandleResponseEar, "HandleRespEar"], + [PerformanceEvents.HandleResponseCode, "HandleRespCode"], + [ + PerformanceEvents.HandleResponsePlatformBroker, + "HandleRespPlatBroker", + ], [PerformanceEvents.UpdateTokenEndpointAuthority, "UpdTEndpointAuth"], [PerformanceEvents.AuthClientAcquireToken, "AuthClientAT"], @@ -453,10 +458,6 @@ export const PerformanceEventAbbreviations: ReadonlyMap = PerformanceEvents.AuthClientCreateTokenRequestBody, "AuthClientCreateTReqBody", ], - [ - PerformanceEvents.AuthClientCreateQueryString, - "AuthClientCreateQueryStr", - ], [PerformanceEvents.PopTokenGenerateCnf, "PopTGenCnf"], [PerformanceEvents.PopTokenGenerateKid, "PopTGenKid"], [PerformanceEvents.HandleServerTokenResponse, "HandleServerTRes"], @@ -552,6 +553,8 @@ export const PerformanceEventAbbreviations: ReadonlyMap = [PerformanceEvents.UrlEncodeArr, "urlEncArr"], [PerformanceEvents.Encrypt, "encrypt"], [PerformanceEvents.Decrypt, "decrypt"], + [PerformanceEvents.GenerateEarKey, "genEarKey"], + [PerformanceEvents.DecryptEarResponse, "decryptEarResp"], ]); /** @@ -700,6 +703,11 @@ export type PerformanceEvent = { */ libraryVersion: string; + /** + * Version of the library used last. Used to track upgrades and downgrades + */ + previousLibraryVersion?: string; + /** * Whether the response is from a native component (e.g., WAM) * @@ -885,6 +893,11 @@ export type PerformanceEvent = { prompt?: string; usePreGeneratedPkce?: boolean; + + // Number of MSAL JS instances in the frame + msalInstanceCount?: number; + // Number of MSAL JS instances using the same client id in the frame + sameClientIdInstanceCount?: number; }; export type PerformanceEventContext = { diff --git a/lib/msal-common/src/telemetry/server/ServerTelemetryManager.ts b/lib/msal-common/src/telemetry/server/ServerTelemetryManager.ts index 6de307b3a0..3ac50221c9 100644 --- a/lib/msal-common/src/telemetry/server/ServerTelemetryManager.ts +++ b/lib/msal-common/src/telemetry/server/ServerTelemetryManager.ts @@ -204,7 +204,8 @@ export class ServerTelemetryManager { this.cacheManager.setServerTelemetry( this.telemetryCacheKey, - lastRequests + lastRequests, + this.correlationId ); return; @@ -219,7 +220,8 @@ export class ServerTelemetryManager { this.cacheManager.setServerTelemetry( this.telemetryCacheKey, - lastRequests + lastRequests, + this.correlationId ); return lastRequests.cacheHits; } @@ -250,7 +252,10 @@ export class ServerTelemetryManager { const errorCount = lastRequests.errors.length; if (numErrorsFlushed === errorCount) { // All errors were sent on last request, clear Telemetry cache - this.cacheManager.removeItem(this.telemetryCacheKey); + this.cacheManager.removeItem( + this.telemetryCacheKey, + this.correlationId + ); } else { // Partial data was flushed to server, construct a new telemetry cache item with errors that were not flushed const serverTelemEntity: ServerTelemetryEntity = { @@ -263,7 +268,8 @@ export class ServerTelemetryManager { this.cacheManager.setServerTelemetry( this.telemetryCacheKey, - serverTelemEntity + serverTelemEntity, + this.correlationId ); } } @@ -351,7 +357,8 @@ export class ServerTelemetryManager { lastRequests.nativeBrokerErrorCode = errorCode; this.cacheManager.setServerTelemetry( this.telemetryCacheKey, - lastRequests + lastRequests, + this.correlationId ); } @@ -364,7 +371,8 @@ export class ServerTelemetryManager { delete lastRequests.nativeBrokerErrorCode; this.cacheManager.setServerTelemetry( this.telemetryCacheKey, - lastRequests + lastRequests, + this.correlationId ); } diff --git a/lib/msal-common/src/utils/Constants.ts b/lib/msal-common/src/utils/Constants.ts index f0fc7c0cf8..011693c4e8 100644 --- a/lib/msal-common/src/utils/Constants.ts +++ b/lib/msal-common/src/utils/Constants.ts @@ -34,11 +34,8 @@ export const Constants = { PROFILE_SCOPE: "profile", OFFLINE_ACCESS_SCOPE: "offline_access", EMAIL_SCOPE: "email", - // Default response type for authorization code flow - CODE_RESPONSE_TYPE: "code", CODE_GRANT_TYPE: "authorization_code", RT_GRANT_TYPE: "refresh_token", - FRAGMENT_RESPONSE_MODE: "fragment", S256_CODE_CHALLENGE_METHOD: "S256", URL_FORM_CONTENT_TYPE: "application/x-www-form-urlencoded;charset=utf-8", AUTHORIZATION_PENDING: "authorization_pending", @@ -58,8 +55,6 @@ export const Constants = { "login.microsoft.com", "sts.windows.net", ], - TOKEN_RESPONSE_TYPE: "token", - ID_TOKEN_RESPONSE_TYPE: "id_token", SHR_NONCE_VALIDITY: 240, INVALID_INSTANCE: "invalid_instance", }; @@ -75,6 +70,7 @@ export const HttpStatus = { UNAUTHORIZED: 401, NOT_FOUND: 404, REQUEST_TIMEOUT: 408, + GONE: 410, TOO_MANY_REQUESTS: 429, CLIENT_ERROR_RANGE_END: 499, SERVER_ERROR: 500, @@ -161,8 +157,20 @@ export const CodeChallengeMethodValues = { S256: "S256", }; +/** + * Allowed values for response_type + */ +export const OAuthResponseType = { + CODE: "code", + IDTOKEN_TOKEN: "id_token token", + IDTOKEN_TOKEN_REFRESHTOKEN: "id_token token refresh_token", +} as const; +export type OAuthResponseType = + (typeof OAuthResponseType)[keyof typeof OAuthResponseType]; + /** * allowed values for server response type + * @deprecated Use ResponseMode instead */ export const ServerResponseType = { QUERY: "query", @@ -175,7 +183,8 @@ export type ServerResponseType = * allowed values for response_mode */ export const ResponseMode = { - ...ServerResponseType, + QUERY: "query", + FRAGMENT: "fragment", FORM_POST: "form_post", } as const; export type ResponseMode = (typeof ResponseMode)[keyof typeof ResponseMode]; @@ -317,15 +326,6 @@ export const PasswordGrantConstants = { export type PasswordGrantConstants = (typeof PasswordGrantConstants)[keyof typeof PasswordGrantConstants]; -/** - * Response codes - */ -export const ResponseCodes = { - httpSuccess: 200, - httpBadRequest: 400, -} as const; -export type ResponseCodes = (typeof ResponseCodes)[keyof typeof ResponseCodes]; - /** * Region Discovery Sources */ @@ -380,3 +380,10 @@ export const ONE_DAY_IN_MS = 86400000; // Token renewal offset default in seconds export const DEFAULT_TOKEN_RENEWAL_OFFSET_SEC = 300; + +export const EncodingTypes = { + BASE64: "base64", + HEX: "hex", + UTF8: "utf-8", +} as const; +export type EncodingTypes = (typeof EncodingTypes)[keyof typeof EncodingTypes]; diff --git a/lib/msal-common/src/utils/UrlUtils.ts b/lib/msal-common/src/utils/UrlUtils.ts index d3e219f1fb..7d972f596b 100644 --- a/lib/msal-common/src/utils/UrlUtils.ts +++ b/lib/msal-common/src/utils/UrlUtils.ts @@ -3,11 +3,12 @@ * Licensed under the MIT License. */ -import { ServerAuthorizationCodeResponse } from "../response/ServerAuthorizationCodeResponse.js"; +import { AuthorizeResponse } from "../response/AuthorizeResponse.js"; import { ClientAuthErrorCodes, createClientAuthError, } from "../error/ClientAuthError.js"; +import { StringDict } from "./MsalTypes.js"; /** * Parses hash string from given string. Returns empty string if no hash symbol is found. @@ -31,7 +32,7 @@ export function stripLeadingHashOrQuery(responseString: string): string { */ export function getDeserializedResponse( responseString: string -): ServerAuthorizationCodeResponse | null { +): AuthorizeResponse | null { // Check if given hash is empty if (!responseString || responseString.indexOf("=") < 0) { return null; @@ -40,12 +41,14 @@ export function getDeserializedResponse( // Strip the # or ? symbol if present const normalizedResponse = stripLeadingHashOrQuery(responseString); // If # symbol was not present, above will return empty string, so give original hash value - const deserializedHash: ServerAuthorizationCodeResponse = - Object.fromEntries(new URLSearchParams(normalizedResponse)); + const deserializedHash: AuthorizeResponse = Object.fromEntries( + new URLSearchParams(normalizedResponse) + ); // Check for known response properties if ( deserializedHash.code || + deserializedHash.ear_jwe || deserializedHash.error || deserializedHash.error_description || deserializedHash.state @@ -62,11 +65,23 @@ export function getDeserializedResponse( /** * Utility to create a URL from the params map */ -export function mapToQueryString(parameters: Map): string { +export function mapToQueryString( + parameters: Map, + encodeExtraParams: boolean = true, + extraQueryParameters?: StringDict +): string { const queryParameterArray: Array = new Array(); parameters.forEach((value, key) => { - queryParameterArray.push(`${key}=${value}`); + if ( + !encodeExtraParams && + extraQueryParameters && + key in extraQueryParameters + ) { + queryParameterArray.push(`${key}=${value}`); + } else { + queryParameterArray.push(`${key}=${encodeURIComponent(value)}`); + } }); return queryParameterArray.join("&"); diff --git a/lib/msal-common/test/account/AuthToken.spec.ts b/lib/msal-common/test/account/AuthToken.spec.ts index ccd778618e..8e46f20bfd 100644 --- a/lib/msal-common/test/account/AuthToken.spec.ts +++ b/lib/msal-common/test/account/AuthToken.spec.ts @@ -1,84 +1,17 @@ import * as AuthToken from "../../src/account/AuthToken"; -import { - TEST_DATA_CLIENT_INFO, - RANDOM_TEST_GUID, - TEST_POP_VALUES, - TEST_CRYPTO_VALUES, - TEST_TOKENS, - ID_TOKEN_CLAIMS, -} from "../test_kit/StringConstants"; +import { TEST_TOKENS, ID_TOKEN_CLAIMS } from "../test_kit/StringConstants"; import { ICrypto } from "../../src/crypto/ICrypto"; import { ClientAuthErrorMessage, ClientAuthError, } from "../../src/error/ClientAuthError"; import { AuthError } from "../../src/error/AuthError"; - -const TEST_ID_TOKEN_PAYLOAD = TEST_TOKENS.IDTOKEN_V2.split(".")[1]; +import { mockCrypto } from "../client/ClientTestUtils.js"; describe("AuthToken.ts Class Unit Tests", () => { let cryptoInterface: ICrypto; beforeEach(() => { - cryptoInterface = { - createNewGuid(): string { - return RANDOM_TEST_GUID; - }, - base64Decode(input: string): string { - switch (input) { - case TEST_POP_VALUES.ENCODED_REQ_CNF: - return TEST_POP_VALUES.DECODED_REQ_CNF; - case TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO: - return TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO; - case TEST_ID_TOKEN_PAYLOAD: - return JSON.stringify(ID_TOKEN_CLAIMS); - default: - return input; - } - }, - base64Encode(input: string): string { - switch (input) { - case "123-test-uid": - return "MTIzLXRlc3QtdWlk"; - case "456-test-uid": - return "NDU2LXRlc3QtdWlk"; - case TEST_POP_VALUES.DECODED_REQ_CNF: - return TEST_POP_VALUES.ENCODED_REQ_CNF; - default: - return input; - } - }, - base64UrlEncode(input: string): string { - switch (input) { - case '{"kid": "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc"}': - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - encodeKid(input: string): string { - switch (input) { - case "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc": - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - async getPublicKeyThumbprint(): Promise { - return TEST_POP_VALUES.KID; - }, - async signJwt(): Promise { - return ""; - }, - async removeTokenBindingKey(): Promise { - return Promise.resolve(true); - }, - async clearKeystore(): Promise { - return Promise.resolve(true); - }, - async hashString(): Promise { - return Promise.resolve(TEST_CRYPTO_VALUES.TEST_SHA256_HASH); - }, - }; + cryptoInterface = mockCrypto; }); afterEach(() => { diff --git a/lib/msal-common/test/account/ClientInfo.spec.ts b/lib/msal-common/test/account/ClientInfo.spec.ts index bc3111ff29..835637dfb6 100644 --- a/lib/msal-common/test/account/ClientInfo.spec.ts +++ b/lib/msal-common/test/account/ClientInfo.spec.ts @@ -3,12 +3,7 @@ import { buildClientInfoFromHomeAccountId, ClientInfo, } from "../../src/account/ClientInfo"; -import { - TEST_DATA_CLIENT_INFO, - RANDOM_TEST_GUID, - TEST_POP_VALUES, - TEST_CRYPTO_VALUES, -} from "../test_kit/StringConstants"; +import { TEST_DATA_CLIENT_INFO } from "../test_kit/StringConstants"; import { ICrypto } from "../../src/crypto/ICrypto"; import { ClientAuthError, @@ -17,69 +12,13 @@ import { createClientAuthError, } from "../../src/error/ClientAuthError"; import { Constants } from "../../src"; +import { mockCrypto } from "../client/ClientTestUtils.js"; describe("ClientInfo.ts Class Unit Tests", () => { describe("buildClientInfo()", () => { let cryptoInterface: ICrypto; beforeEach(() => { - cryptoInterface = { - createNewGuid(): string { - return RANDOM_TEST_GUID; - }, - base64Decode(input: string): string { - switch (input) { - case TEST_POP_VALUES.ENCODED_REQ_CNF: - return TEST_POP_VALUES.DECODED_REQ_CNF; - case TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO: - return TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO; - default: - return input; - } - }, - base64Encode(input: string): string { - switch (input) { - case "123-test-uid": - return "MTIzLXRlc3QtdWlk"; - case "456-test-uid": - return "NDU2LXRlc3QtdWlk"; - case TEST_POP_VALUES.DECODED_REQ_CNF: - return TEST_POP_VALUES.ENCODED_REQ_CNF; - default: - return input; - } - }, - base64UrlEncode(input: string): string { - switch (input) { - case '{"kid": "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc"}': - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - encodeKid(input: string): string { - switch (input) { - case "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc": - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - async getPublicKeyThumbprint(): Promise { - return TEST_POP_VALUES.KID; - }, - async signJwt(): Promise { - return ""; - }, - async removeTokenBindingKey(): Promise { - return Promise.resolve(true); - }, - async clearKeystore(): Promise { - return Promise.resolve(true); - }, - async hashString(): Promise { - return Promise.resolve(TEST_CRYPTO_VALUES.TEST_SHA256_HASH); - }, - }; + cryptoInterface = mockCrypto; }); afterEach(() => { diff --git a/lib/msal-common/test/authority/Authority.spec.ts b/lib/msal-common/test/authority/Authority.spec.ts index f8fbcffaf4..73c7671427 100644 --- a/lib/msal-common/test/authority/Authority.spec.ts +++ b/lib/msal-common/test/authority/Authority.spec.ts @@ -46,6 +46,7 @@ import { import { RegionDiscovery } from "../../src/authority/RegionDiscovery"; import { InstanceDiscoveryMetadata } from "../../src/authority/AuthorityMetadata"; import * as authorityMetadata from "../../src/authority/AuthorityMetadata"; +import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient.js"; let mockStorage: MockStorageClass; @@ -87,7 +88,8 @@ describe("Authority.ts Class Unit Tests", () => { mockStorage = new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, - logger + logger, + new StubPerformanceClient() ); }); afterEach(() => { diff --git a/lib/msal-common/test/cache/CacheManager.spec.ts b/lib/msal-common/test/cache/CacheManager.spec.ts index 06f1966e8c..acb2f17d6c 100644 --- a/lib/msal-common/test/cache/CacheManager.spec.ts +++ b/lib/msal-common/test/cache/CacheManager.spec.ts @@ -23,11 +23,8 @@ import { TEST_TOKEN_LIFETIMES, ID_TOKEN_ALT_CLAIMS, GUEST_ID_TOKEN_CLAIMS, + RANDOM_TEST_GUID, } from "../test_kit/StringConstants.js"; -import { - ClientAuthErrorCodes, - createClientAuthError, -} from "../../src/error/ClientAuthError.js"; import { AccountInfo } from "../../src/account/AccountInfo.js"; import { MockCache } from "./MockCache.js"; import { buildAccountFromIdTokenClaims, buildIdToken } from "msal-test-utils"; @@ -282,10 +279,10 @@ describe("CacheManager.ts test cases", () => { expect( mockCache.cacheManager.getIdToken( TEST_ACCOUNT_INFO, + correlationId, undefined, TEST_ACCOUNT_INFO.tenantId, - mockPerfClient, - correlationId + mockPerfClient ) ).toBeNull(); expect( @@ -330,10 +327,15 @@ describe("CacheManager.ts test cases", () => { buildAccountFromIdTokenClaims(ID_TOKEN_ALT_CLAIMS).getAccountInfo(); it("getAllAccounts returns an empty array if there are no accounts in the cache", () => { mockCache.clearCache(); - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(0); + expect( + mockCache.cacheManager.getAllAccounts({}, RANDOM_TEST_GUID) + ).toHaveLength(0); }); it("getAllAccounts (gets all AccountInfo objects)", async () => { - const accounts = mockCache.cacheManager.getAllAccounts(); + const accounts = mockCache.cacheManager.getAllAccounts( + {}, + RANDOM_TEST_GUID + ); expect(accounts).not.toBeNull(); // 2 home accounts + 1 tenant profile @@ -344,9 +346,12 @@ describe("CacheManager.ts test cases", () => { }); it("getAllAccounts with isHomeTenant filter does not return guest tenant profiles as AccountInfo objects", () => { - const homeAccounts = mockCache.cacheManager.getAllAccounts({ - isHomeTenant: true, - }); + const homeAccounts = mockCache.cacheManager.getAllAccounts( + { + isHomeTenant: true, + }, + RANDOM_TEST_GUID + ); expect(homeAccounts).not.toBeNull(); expect(homeAccounts.length).toBe(2); expect(homeAccounts[0].idTokenClaims).toEqual(ID_TOKEN_CLAIMS); @@ -360,14 +365,19 @@ describe("CacheManager.ts test cases", () => { loginHint: ID_TOKEN_CLAIMS.login_hint, }; - let accounts = - mockCache.cacheManager.getAllAccounts(successFilter); + let accounts = mockCache.cacheManager.getAllAccounts( + successFilter, + RANDOM_TEST_GUID + ); expect(accounts.length).toEqual(1); const wrongFilter: AccountFilter = { loginHint: "WrongHint", }; - accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); + accounts = mockCache.cacheManager.getAllAccounts( + wrongFilter, + RANDOM_TEST_GUID + ); expect(accounts.length).toBe(0); }); @@ -377,14 +387,19 @@ describe("CacheManager.ts test cases", () => { loginHint: ID_TOKEN_CLAIMS.preferred_username, }; - let accounts = - mockCache.cacheManager.getAllAccounts(successFilter); + let accounts = mockCache.cacheManager.getAllAccounts( + successFilter, + RANDOM_TEST_GUID + ); expect(accounts.length).toEqual(1); const wrongFilter: AccountFilter = { loginHint: "WrongHint", }; - accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); + accounts = mockCache.cacheManager.getAllAccounts( + wrongFilter, + RANDOM_TEST_GUID + ); expect(accounts.length).toBe(0); }); @@ -394,41 +409,59 @@ describe("CacheManager.ts test cases", () => { loginHint: ID_TOKEN_CLAIMS.upn, }; - let accounts = - mockCache.cacheManager.getAllAccounts(successFilter); + let accounts = mockCache.cacheManager.getAllAccounts( + successFilter, + RANDOM_TEST_GUID + ); expect(accounts.length).toEqual(1); const wrongFilter: AccountFilter = { loginHint: "WrongHint", }; - accounts = mockCache.cacheManager.getAllAccounts(wrongFilter); + accounts = mockCache.cacheManager.getAllAccounts( + wrongFilter, + RANDOM_TEST_GUID + ); expect(accounts.length).toBe(0); }); }); describe("getAllAccounts with filter", () => { it("Matches accounts by username", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + expect( + mockCache.cacheManager.getAllAccounts({}, RANDOM_TEST_GUID) + ).toHaveLength(3); const account1Filter = { username: account1.username }; const account2Filter = { username: account2.username }; - const accounts = - mockCache.cacheManager.getAllAccounts(account1Filter); + const accounts = mockCache.cacheManager.getAllAccounts( + account1Filter, + RANDOM_TEST_GUID + ); expect(accounts).toHaveLength(1); expect( - mockCache.cacheManager.getAllAccounts(account1Filter)[0] - .username + mockCache.cacheManager.getAllAccounts( + account1Filter, + RANDOM_TEST_GUID + )[0].username ).toBe(account1.username); expect( - mockCache.cacheManager.getAllAccounts(account2Filter) + mockCache.cacheManager.getAllAccounts( + account2Filter, + RANDOM_TEST_GUID + ) ).toHaveLength(1); expect( - mockCache.cacheManager.getAllAccounts(account2Filter)[0] - .username + mockCache.cacheManager.getAllAccounts( + account2Filter, + RANDOM_TEST_GUID + )[0].username ).toBe(account2.username); }); it("Matches accounts by homeAccountId", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + expect( + mockCache.cacheManager.getAllAccounts({}, RANDOM_TEST_GUID) + ).toHaveLength(3); const multiTenantAccountFilter = { homeAccountId: account1.homeAccountId, }; @@ -444,7 +477,8 @@ describe("CacheManager.ts test cases", () => { // Multi-tenant account has two tenant profiles which will both match the same homeAccountId const multiTenantAccountProfiles = mockCache.cacheManager.getAllAccounts( - multiTenantAccountFilter + multiTenantAccountFilter, + RANDOM_TEST_GUID ); expect(multiTenantAccountProfiles).toHaveLength(2); expect(multiTenantAccountProfiles[0].homeAccountId).toBe( @@ -454,7 +488,8 @@ describe("CacheManager.ts test cases", () => { // Set isHomeTenant = true to only get baseAccount const multiTenantAccountHomeTenantOnlyProfiles = mockCache.cacheManager.getAllAccounts( - multiTenantAccountHomeTenantOnlyFilter + multiTenantAccountHomeTenantOnlyFilter, + RANDOM_TEST_GUID ); expect(multiTenantAccountHomeTenantOnlyProfiles).toHaveLength( 1 @@ -463,16 +498,23 @@ describe("CacheManager.ts test cases", () => { multiTenantAccountHomeTenantOnlyProfiles[0].tenantId ).toBe(account1.tenantId); expect( - mockCache.cacheManager.getAllAccounts(account2Filter) + mockCache.cacheManager.getAllAccounts( + account2Filter, + RANDOM_TEST_GUID + ) ).toHaveLength(1); expect( - mockCache.cacheManager.getAllAccounts(account2Filter)[0] - .homeAccountId + mockCache.cacheManager.getAllAccounts( + account2Filter, + RANDOM_TEST_GUID + )[0].homeAccountId ).toBe(account2.homeAccountId); }); it("Matches accounts by localAccountId", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + expect( + mockCache.cacheManager.getAllAccounts({}, RANDOM_TEST_GUID) + ).toHaveLength(3); // Local account ID is sourced from ID token claims so for this test we compare against the decoded ID token claims instead of mock account object const account1Filter = { localAccountId: ID_TOKEN_CLAIMS.oid, @@ -481,23 +523,35 @@ describe("CacheManager.ts test cases", () => { localAccountId: ID_TOKEN_ALT_CLAIMS.oid, }; expect( - mockCache.cacheManager.getAllAccounts(account1Filter) + mockCache.cacheManager.getAllAccounts( + account1Filter, + RANDOM_TEST_GUID + ) ).toHaveLength(1); expect( - mockCache.cacheManager.getAllAccounts(account1Filter)[0] - .localAccountId + mockCache.cacheManager.getAllAccounts( + account1Filter, + RANDOM_TEST_GUID + )[0].localAccountId ).toBe(account1Filter.localAccountId); expect( - mockCache.cacheManager.getAllAccounts(account2Filter) + mockCache.cacheManager.getAllAccounts( + account2Filter, + RANDOM_TEST_GUID + ) ).toHaveLength(1); expect( - mockCache.cacheManager.getAllAccounts(account2Filter)[0] - .localAccountId + mockCache.cacheManager.getAllAccounts( + account2Filter, + RANDOM_TEST_GUID + )[0].localAccountId ).toBe(account2Filter.localAccountId); }); it("Matches accounts by tenantId", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + expect( + mockCache.cacheManager.getAllAccounts({}, RANDOM_TEST_GUID) + ).toHaveLength(3); const firstTenantAccountFilter = { tenantId: account1.tenantId, }; @@ -506,29 +560,35 @@ describe("CacheManager.ts test cases", () => { }; expect( mockCache.cacheManager.getAllAccounts( - firstTenantAccountFilter + firstTenantAccountFilter, + RANDOM_TEST_GUID ) ).toHaveLength(1); expect( mockCache.cacheManager.getAllAccounts( - firstTenantAccountFilter + firstTenantAccountFilter, + RANDOM_TEST_GUID )[0].tenantId ).toBe(firstTenantAccountFilter.tenantId); // Guest profile of first user account is from the same tenant as account 2 expect( mockCache.cacheManager.getAllAccounts( - secondTenantAccountFilter + secondTenantAccountFilter, + RANDOM_TEST_GUID ) ).toHaveLength(2); expect( mockCache.cacheManager.getAllAccounts( - secondTenantAccountFilter + secondTenantAccountFilter, + RANDOM_TEST_GUID )[0].tenantId ).toBe(secondTenantAccountFilter.tenantId); }); it("Matches accounts by environment", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + expect( + mockCache.cacheManager.getAllAccounts({}, RANDOM_TEST_GUID) + ).toHaveLength(3); // Add local account ID to further filter because environments are aliases of eachother const firstEnvironmentAccountsFilter = { homeAccountId: account1.homeAccountId, @@ -540,28 +600,34 @@ describe("CacheManager.ts test cases", () => { }; expect( mockCache.cacheManager.getAllAccounts( - firstEnvironmentAccountsFilter + firstEnvironmentAccountsFilter, + RANDOM_TEST_GUID ) ).toHaveLength(2); expect( mockCache.cacheManager.getAllAccounts( - firstEnvironmentAccountsFilter + firstEnvironmentAccountsFilter, + RANDOM_TEST_GUID )[0].environment ).toBe(account1.environment); expect( mockCache.cacheManager.getAllAccounts( - secondEnvironmentAccountsFilter + secondEnvironmentAccountsFilter, + RANDOM_TEST_GUID ) ).toHaveLength(1); expect( mockCache.cacheManager.getAllAccounts( - secondEnvironmentAccountsFilter + secondEnvironmentAccountsFilter, + RANDOM_TEST_GUID )[0].environment ).toBe(account2.environment); }); it("Matches accounts by all filters", () => { - expect(mockCache.cacheManager.getAllAccounts()).toHaveLength(3); + expect( + mockCache.cacheManager.getAllAccounts({}, RANDOM_TEST_GUID) + ).toHaveLength(3); const account1Filter = { ...account1, localAccountId: ID_TOKEN_CLAIMS.oid, @@ -571,18 +637,28 @@ describe("CacheManager.ts test cases", () => { localAccountId: ID_TOKEN_ALT_CLAIMS.oid, }; expect( - mockCache.cacheManager.getAllAccounts(account1Filter) + mockCache.cacheManager.getAllAccounts( + account1Filter, + RANDOM_TEST_GUID + ) ).toHaveLength(1); expect( - mockCache.cacheManager.getAllAccounts(account1Filter)[0] - .localAccountId + mockCache.cacheManager.getAllAccounts( + account1Filter, + RANDOM_TEST_GUID + )[0].localAccountId ).toBe(account1Filter.localAccountId); expect( - mockCache.cacheManager.getAllAccounts(account2Filter) + mockCache.cacheManager.getAllAccounts( + account2Filter, + RANDOM_TEST_GUID + ) ).toHaveLength(1); expect( - mockCache.cacheManager.getAllAccounts(account2Filter)[0] - .localAccountId + mockCache.cacheManager.getAllAccounts( + account2Filter, + RANDOM_TEST_GUID + )[0].localAccountId ).toBe(account2Filter.localAccountId); }); }); @@ -595,18 +671,24 @@ describe("CacheManager.ts test cases", () => { ).getAccountInfo(); it("returns null if no accounts match filter", () => { expect( - mockCache.cacheManager.getAccountInfoFilteredBy({ - homeAccountId: "inexistent-account-id", - }) + mockCache.cacheManager.getAccountInfoFilteredBy( + { + homeAccountId: "inexistent-account-id", + }, + RANDOM_TEST_GUID + ) ).toBeNull(); }); it("returns an account matching filter", () => { const resultAccount = - mockCache.cacheManager.getAccountInfoFilteredBy({ - homeAccountId: multiTenantAccount.homeAccountId, - tenantId: multiTenantAccount.tenantId, - }); + mockCache.cacheManager.getAccountInfoFilteredBy( + { + homeAccountId: multiTenantAccount.homeAccountId, + tenantId: multiTenantAccount.tenantId, + }, + RANDOM_TEST_GUID + ); expect(resultAccount).not.toBeNull(); expect(resultAccount).toMatchObject(multiTenantAccount); }); @@ -624,14 +706,20 @@ describe("CacheManager.ts test cases", () => { homeAccountId: multiTenantAccount.homeAccountId, }; // Remove main ID token - mockCache.cacheManager.removeIdToken(mainIdTokenKey); + mockCache.cacheManager.removeIdToken( + mainIdTokenKey, + RANDOM_TEST_GUID + ); const resultAccount = - mockCache.cacheManager.getAccountInfoFilteredBy(filter); + mockCache.cacheManager.getAccountInfoFilteredBy( + filter, + RANDOM_TEST_GUID + ); expect(resultAccount).not.toBeNull(); expect(resultAccount?.tenantId).toBe(GUEST_ID_TOKEN_CLAIMS.tid); const allAccountsReversed = mockCache.cacheManager - .getAllAccounts() + .getAllAccounts({}, RANDOM_TEST_GUID) .reverse(); jest.spyOn( @@ -640,7 +728,10 @@ describe("CacheManager.ts test cases", () => { ).mockReturnValueOnce(allAccountsReversed); const reversedResultAccount = - mockCache.cacheManager.getAccountInfoFilteredBy(filter); + mockCache.cacheManager.getAccountInfoFilteredBy( + filter, + RANDOM_TEST_GUID + ); expect(reversedResultAccount).not.toBeNull(); expect(reversedResultAccount?.tenantId).toBe( GUEST_ID_TOKEN_CLAIMS.tid @@ -649,11 +740,14 @@ describe("CacheManager.ts test cases", () => { it("returns account matching filter with isHomeTenant = true", () => { const resultAccount = - mockCache.cacheManager.getAccountInfoFilteredBy({ - homeAccountId: multiTenantAccount.homeAccountId, - tenantId: multiTenantAccount.tenantId, - isHomeTenant: true, - }); + mockCache.cacheManager.getAccountInfoFilteredBy( + { + homeAccountId: multiTenantAccount.homeAccountId, + tenantId: multiTenantAccount.tenantId, + isHomeTenant: true, + }, + RANDOM_TEST_GUID + ); expect(resultAccount).not.toBeNull(); expect(resultAccount).toMatchObject(multiTenantAccount); }); @@ -665,19 +759,25 @@ describe("CacheManager.ts test cases", () => { ID_TOKEN_CLAIMS, [GUEST_ID_TOKEN_CLAIMS] ).getAccountInfo(); - const resultAccount = mockCache.cacheManager.getBaseAccountInfo({ - homeAccountId: multiTenantAccount.homeAccountId, - tenantId: GUEST_ID_TOKEN_CLAIMS.tid, - }); + const resultAccount = mockCache.cacheManager.getBaseAccountInfo( + { + homeAccountId: multiTenantAccount.homeAccountId, + tenantId: GUEST_ID_TOKEN_CLAIMS.tid, + }, + RANDOM_TEST_GUID + ); expect(resultAccount).toEqual(multiTenantAccount); }); it("returns null if no account matches filter", () => { expect( - mockCache.cacheManager.getBaseAccountInfo({ - homeAccountId: "inexistent-homeaccountid", - }) + mockCache.cacheManager.getBaseAccountInfo( + { + homeAccountId: "inexistent-homeaccountid", + }, + RANDOM_TEST_GUID + ) ).toBeNull(); }); }); @@ -776,14 +876,18 @@ describe("CacheManager.ts test cases", () => { const successFilter: AccountFilter = { homeAccountId: matchAccountEntity.homeAccountId, }; - let accounts = - mockCache.cacheManager.getAccountsFilteredBy(successFilter); + let accounts = mockCache.cacheManager.getAccountsFilteredBy( + successFilter, + RANDOM_TEST_GUID + ); // getAccountsFilteredBy only gets cached accounts, so don't expect all tenant profiles to be returned as account objects expect(Object.keys(accounts).length).toEqual(1); const wrongFilter: AccountFilter = { homeAccountId: "Wrong Id" }; - accounts = - mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); + accounts = mockCache.cacheManager.getAccountsFilteredBy( + wrongFilter, + RANDOM_TEST_GUID + ); expect(Object.keys(accounts).length).toEqual(0); }); @@ -792,15 +896,19 @@ describe("CacheManager.ts test cases", () => { const successFilter: AccountFilter = { environment: matchAccountEntity.environment, }; - let accounts = - mockCache.cacheManager.getAccountsFilteredBy(successFilter); + let accounts = mockCache.cacheManager.getAccountsFilteredBy( + successFilter, + RANDOM_TEST_GUID + ); // Both cached accounts have environments that are aliases of eachother, expect both to match expect(Object.keys(accounts).length).toEqual(2); jest.restoreAllMocks(); const wrongFilter: AccountFilter = { environment: "Wrong Env" }; - accounts = - mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); + accounts = mockCache.cacheManager.getAccountsFilteredBy( + wrongFilter, + RANDOM_TEST_GUID + ); expect(Object.keys(accounts).length).toEqual(0); }); @@ -809,13 +917,17 @@ describe("CacheManager.ts test cases", () => { const successFilter: AccountFilter = { realm: matchAccountEntity.realm, }; - let accounts = - mockCache.cacheManager.getAccountsFilteredBy(successFilter); + let accounts = mockCache.cacheManager.getAccountsFilteredBy( + successFilter, + RANDOM_TEST_GUID + ); expect(Object.keys(accounts).length).toEqual(1); const wrongFilter: AccountFilter = { realm: "Wrong Realm" }; - accounts = - mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); + accounts = mockCache.cacheManager.getAccountsFilteredBy( + wrongFilter, + RANDOM_TEST_GUID + ); expect(Object.keys(accounts).length).toEqual(0); }); @@ -824,13 +936,17 @@ describe("CacheManager.ts test cases", () => { const successFilter: AccountFilter = { nativeAccountId: "mocked_native_account_id", }; - let accounts = - mockCache.cacheManager.getAccountsFilteredBy(successFilter); + let accounts = mockCache.cacheManager.getAccountsFilteredBy( + successFilter, + RANDOM_TEST_GUID + ); expect(Object.keys(accounts).length).toEqual(1); const wrongFilter: AccountFilter = { realm: "notNativeAccountId" }; - accounts = - mockCache.cacheManager.getAccountsFilteredBy(wrongFilter); + accounts = mockCache.cacheManager.getAccountsFilteredBy( + wrongFilter, + RANDOM_TEST_GUID + ); expect(Object.keys(accounts).length).toEqual(0); }); }); @@ -1276,9 +1392,12 @@ describe("CacheManager.ts test cases", () => { }); it("credentialType filter (Access Tokens with and without Auth Scheme)", () => { - const accessToken = mockCache.cacheManager.getAccessTokensByFilter({ - credentialType: "AccessToken", - }); + const accessToken = mockCache.cacheManager.getAccessTokensByFilter( + { + credentialType: "AccessToken", + }, + RANDOM_TEST_GUID + ); expect( mockCache.cacheManager.credentialMatchesFilter(accessToken[0], { credentialType: CredentialType.ACCESS_TOKEN, @@ -1292,10 +1411,13 @@ describe("CacheManager.ts test cases", () => { ).toBe(false); const accessTokenWithAuthScheme = - mockCache.cacheManager.getAccessTokensByFilter({ - credentialType: - CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME, - }); + mockCache.cacheManager.getAccessTokensByFilter( + { + credentialType: + CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME, + }, + RANDOM_TEST_GUID + ); expect( mockCache.cacheManager.credentialMatchesFilter( accessTokenWithAuthScheme[0], @@ -1386,10 +1508,14 @@ describe("CacheManager.ts test cases", () => { }); it("tokenType filter", () => { - const accessToken = mockCache.cacheManager.getAccessTokensByFilter({ - credentialType: CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME, - tokenType: AuthenticationScheme.BEARER, - }); + const accessToken = mockCache.cacheManager.getAccessTokensByFilter( + { + credentialType: + CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME, + tokenType: AuthenticationScheme.BEARER, + }, + RANDOM_TEST_GUID + ); expect( mockCache.cacheManager.credentialMatchesFilter(accessToken[0], { tokenType: AuthenticationScheme.BEARER, @@ -1406,10 +1532,14 @@ describe("CacheManager.ts test cases", () => { }) ).toBe(false); - const popToken = mockCache.cacheManager.getAccessTokensByFilter({ - credentialType: CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME, - tokenType: AuthenticationScheme.POP, - }); + const popToken = mockCache.cacheManager.getAccessTokensByFilter( + { + credentialType: + CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME, + tokenType: AuthenticationScheme.POP, + }, + RANDOM_TEST_GUID + ); expect( mockCache.cacheManager.credentialMatchesFilter(popToken[0], { tokenType: AuthenticationScheme.BEARER, @@ -1426,10 +1556,14 @@ describe("CacheManager.ts test cases", () => { }) ).toBe(false); - const sshToken = mockCache.cacheManager.getAccessTokensByFilter({ - credentialType: CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME, - tokenType: AuthenticationScheme.SSH, - }); + const sshToken = mockCache.cacheManager.getAccessTokensByFilter( + { + credentialType: + CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME, + tokenType: AuthenticationScheme.SSH, + }, + RANDOM_TEST_GUID + ); expect( mockCache.cacheManager.credentialMatchesFilter(sshToken[0], { tokenType: AuthenticationScheme.BEARER, @@ -1457,8 +1591,10 @@ describe("CacheManager.ts test cases", () => { keyId: "some_key_id", }; - let accessTokens = - mockCache.cacheManager.getAccessTokensByFilter(successFilter); + let accessTokens = mockCache.cacheManager.getAccessTokensByFilter( + successFilter, + RANDOM_TEST_GUID + ); expect(accessTokens.length).toEqual(1); const wrongFilter = { @@ -1466,8 +1602,10 @@ describe("CacheManager.ts test cases", () => { keyId: "wrong_key_id", }; - accessTokens = - mockCache.cacheManager.getAccessTokensByFilter(wrongFilter); + accessTokens = mockCache.cacheManager.getAccessTokensByFilter( + wrongFilter, + RANDOM_TEST_GUID + ); expect(accessTokens.length).toEqual(0); }); @@ -1479,7 +1617,8 @@ describe("CacheManager.ts test cases", () => { }; let accessTokens = mockCache.cacheManager.getAccessTokensByFilter( - successFilterWithRCHash + successFilterWithRCHash, + RANDOM_TEST_GUID ); expect(accessTokens.length).toEqual(1); @@ -1490,7 +1629,8 @@ describe("CacheManager.ts test cases", () => { }; accessTokens = mockCache.cacheManager.getAccessTokensByFilter( - wrongFilterWithRCHash + wrongFilterWithRCHash, + RANDOM_TEST_GUID ); expect(accessTokens.length).toEqual(0); }); @@ -1503,7 +1643,8 @@ describe("CacheManager.ts test cases", () => { }; let accessTokens = mockCache.cacheManager.getAccessTokensByFilter( - successFilterWithRCHash + successFilterWithRCHash, + RANDOM_TEST_GUID ); expect(accessTokens.length).toEqual(1); @@ -1514,7 +1655,8 @@ describe("CacheManager.ts test cases", () => { }; accessTokens = mockCache.cacheManager.getAccessTokensByFilter( - wrongFilterWithRCHash + wrongFilterWithRCHash, + RANDOM_TEST_GUID ); expect(accessTokens.length).toEqual(0); }); @@ -1553,7 +1695,7 @@ describe("CacheManager.ts test cases", () => { }); it("removeAppMetadata", () => { - mockCache.cacheManager.removeAppMetadata(); + mockCache.cacheManager.removeAppMetadata(RANDOM_TEST_GUID); expect( mockCache.cacheManager.getAppMetadata( "appmetadata-login.microsoftonline.com-mock_client_id" @@ -1561,22 +1703,28 @@ describe("CacheManager.ts test cases", () => { ).toBeUndefined(); }); - it("removeAllAccounts", async () => { - const accountsBeforeRemove = mockCache.cacheManager.getAllAccounts(); - await mockCache.cacheManager.removeAllAccounts(); - const accountsAfterRemove = mockCache.cacheManager.getAllAccounts(); + it("removeAllAccounts", () => { + const accountsBeforeRemove = mockCache.cacheManager.getAllAccounts( + {}, + RANDOM_TEST_GUID + ); + mockCache.cacheManager.removeAllAccounts(RANDOM_TEST_GUID); + const accountsAfterRemove = mockCache.cacheManager.getAllAccounts( + {}, + RANDOM_TEST_GUID + ); expect(accountsBeforeRemove).toHaveLength(3); expect(accountsAfterRemove).toHaveLength(0); }); - it("removeAccount", async () => { + it("removeAccount", () => { const accountToRemove = buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS); const accountToRemoveKey = accountToRemove.generateAccountKey(); expect( mockCache.cacheManager.getAccount(accountToRemoveKey) ).not.toBeNull(); - await mockCache.cacheManager.removeAccount(accountToRemoveKey); + mockCache.cacheManager.removeAccount(accountToRemoveKey); expect( mockCache.cacheManager.getAccount(accountToRemoveKey) ).toBeNull(); @@ -1596,8 +1744,9 @@ describe("CacheManager.ts test cases", () => { extendedExpiresOn: "4600", }; - await mockCache.cacheManager.removeAccessToken( - CacheHelpers.generateCredentialKey(at) + mockCache.cacheManager.removeAccessToken( + CacheHelpers.generateCredentialKey(at), + RANDOM_TEST_GUID ); const atKey = CacheHelpers.generateCredentialKey(at); expect(mockCache.cacheManager.getAccount(atKey)).toBeNull(); @@ -1624,8 +1773,9 @@ describe("CacheManager.ts test cases", () => { "removeTokenBindingKey" ); - await mockCache.cacheManager.removeAccessToken( - CacheHelpers.generateCredentialKey(atWithAuthScheme) + mockCache.cacheManager.removeAccessToken( + CacheHelpers.generateCredentialKey(atWithAuthScheme), + RANDOM_TEST_GUID ); const atKey = CacheHelpers.generateCredentialKey(atWithAuthScheme); expect(mockCache.cacheManager.getAccount(atKey)).toBeNull(); @@ -1655,45 +1805,15 @@ describe("CacheManager.ts test cases", () => { "removeTokenBindingKey" ); - await mockCache.cacheManager.removeAccessToken( - CacheHelpers.generateCredentialKey(atWithAuthScheme) + mockCache.cacheManager.removeAccessToken( + CacheHelpers.generateCredentialKey(atWithAuthScheme), + RANDOM_TEST_GUID ); const atKey = CacheHelpers.generateCredentialKey(atWithAuthScheme); expect(mockCache.cacheManager.getAccount(atKey)).toBeNull(); expect(removeTokenBindingKeySpy).toHaveBeenCalledTimes(0); }); - it("throws bindingKeyNotRemoved error when key is not deleted from storage", async () => { - const atWithAuthScheme = { - environment: "login.microsoftonline.com", - credentialType: CredentialType.ACCESS_TOKEN_WITH_AUTH_SCHEME, - secret: "an access token", - realm: "microsoft", - target: "scope1 scope2 scope3", - clientId: "mock_client_id", - cachedAt: "1000", - homeAccountId: "uid.utid", - extendedExpiresOn: "4600", - expiresOn: "4600", - keyId: "V6N_HMPagNpYS_wxM14X73q3eWzbTr9Z31RyHkIcN0Y", - tokenType: AuthenticationScheme.POP, - }; - - jest.spyOn(mockCrypto, "removeTokenBindingKey").mockImplementation( - (keyId: string): Promise => { - return Promise.reject(); - } - ); - - return await expect( - mockCache.cacheManager.removeAccessToken( - CacheHelpers.generateCredentialKey(atWithAuthScheme) - ) - ).rejects.toThrow( - createClientAuthError(ClientAuthErrorCodes.bindingKeyNotRemoved) - ); - }); - it("getAccessToken matches multiple tokens, removes them and returns null", (done) => { mockCache.cacheManager.clear().then(async () => { const mockedAtEntity: AccessTokenEntity = @@ -1770,35 +1890,18 @@ describe("CacheManager.ts test cases", () => { forceRefresh: false, }; - const mockPerfClient = new MockPerformanceClient(); - const correlationId = "test-correlation-id"; - - mockPerfClient.addPerformanceCallback((events) => { - expect(events.length).toBe(1); - expect(events[0].multiMatchedAT).toEqual(2); - done(); - }); - - const measurement = mockPerfClient.startMeasurement( - PerformanceEvents.AcquireTokenSilent, - correlationId - ); - expect( mockCache.cacheManager.getAccessToken( mockedAccountInfo, silentFlowRequest, undefined, - undefined, - mockPerfClient, - correlationId + undefined ) ).toBeNull(); expect( mockCache.cacheManager.getTokenKeys().accessToken.length ).toEqual(0); - - measurement.end(); + done(); }); }); @@ -2162,7 +2265,8 @@ describe("CacheManager.ts test cases", () => { const matchAccountInfo = buildAccountFromIdTokenClaims(ID_TOKEN_CLAIMS).getAccountInfo(); const account = mockCache.cacheManager.readAccountFromCache( - matchAccountInfo + matchAccountInfo, + RANDOM_TEST_GUID ) as AccountEntity; if (!account) { throw TestError.createTestSetupError( @@ -2173,11 +2277,14 @@ describe("CacheManager.ts test cases", () => { }); it("getAccountsFilteredBy nativeAccountId", () => { - const account = mockCache.cacheManager.getAccountsFilteredBy({ - nativeAccountId: - CACHE_MOCKS.MOCK_ACCOUNT_INFO_WITH_NATIVE_ACCOUNT_ID - .nativeAccountId, - }) as AccountEntity[]; + const account = mockCache.cacheManager.getAccountsFilteredBy( + { + nativeAccountId: + CACHE_MOCKS.MOCK_ACCOUNT_INFO_WITH_NATIVE_ACCOUNT_ID + .nativeAccountId, + }, + RANDOM_TEST_GUID + ) as AccountEntity[]; if (account.length < 1) { throw TestError.createTestSetupError( "account does not have a value" @@ -2193,7 +2300,8 @@ describe("CacheManager.ts test cases", () => { buildAccountFromIdTokenClaims(ID_TOKEN_ALT_CLAIMS).getAccountInfo(); // Get home ID token by default const idToken = mockCache.cacheManager.getIdToken( - baseAccountInfo + baseAccountInfo, + RANDOM_TEST_GUID ) as IdTokenEntity; if (!idToken) { throw TestError.createTestSetupError( @@ -2203,6 +2311,7 @@ describe("CacheManager.ts test cases", () => { expect(idToken.realm).toBe(baseAccountInfo.tenantId); const guestIdToken = mockCache.cacheManager.getIdToken( baseAccountInfo, + RANDOM_TEST_GUID, undefined, GUEST_ID_TOKEN_CLAIMS.tid ) as IdTokenEntity; @@ -2217,7 +2326,8 @@ describe("CacheManager.ts test cases", () => { it("getRefreshToken", () => { const refreshToken = mockCache.cacheManager.getRefreshToken( CACHE_MOCKS.MOCK_ACCOUNT_INFO, - false + false, + RANDOM_TEST_GUID ) as RefreshTokenEntity; if (!refreshToken) { throw TestError.createTestSetupError( @@ -2230,7 +2340,8 @@ describe("CacheManager.ts test cases", () => { it("getRefreshToken Error", () => { const refreshToken = mockCache.cacheManager.getRefreshToken( { ...CACHE_MOCKS.MOCK_ACCOUNT_INFO, homeAccountId: "fake-home-id" }, - true + true, + RANDOM_TEST_GUID ); expect(refreshToken).toBe(null); }); @@ -2238,7 +2349,8 @@ describe("CacheManager.ts test cases", () => { it("getRefreshToken with familyId", () => { const refreshToken = mockCache.cacheManager.getRefreshToken( CACHE_MOCKS.MOCK_ACCOUNT_INFO, - true + true, + RANDOM_TEST_GUID ) as RefreshTokenEntity; if (!refreshToken) { throw TestError.createTestSetupError( @@ -2276,7 +2388,8 @@ describe("CacheManager.ts test cases", () => { const cachedToken = mockCache.cacheManager.getRefreshToken( mockedAccountInfo, - false + false, + RANDOM_TEST_GUID ) as RefreshTokenEntity; if (!cachedToken) { throw TestError.createTestSetupError( diff --git a/lib/msal-common/test/cache/MockCache.ts b/lib/msal-common/test/cache/MockCache.ts index 961cfb4dbf..ee07de9f7f 100644 --- a/lib/msal-common/test/cache/MockCache.ts +++ b/lib/msal-common/test/cache/MockCache.ts @@ -7,6 +7,7 @@ import { StaticAuthorityOptions } from "../../src/authority/AuthorityOptions.js" import { RefreshTokenEntity } from "../../src/cache/entities/RefreshTokenEntity.js"; import { ICrypto } from "../../src/crypto/ICrypto.js"; import { Logger } from "../../src/logger/Logger.js"; +import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient.js"; import { AuthenticationScheme, CredentialType, @@ -33,6 +34,7 @@ export class MockCache { clientId, cryptoImpl, new Logger({}), + new StubPerformanceClient(), staticAuthorityOptions ); } diff --git a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts index fe03c29e18..df6750ed31 100644 --- a/lib/msal-common/test/cache/entities/AccountEntity.spec.ts +++ b/lib/msal-common/test/cache/entities/AccountEntity.spec.ts @@ -8,13 +8,10 @@ import { } from "../../../src/network/INetworkModule.js"; import { ICrypto } from "../../../src/crypto/ICrypto.js"; import { - RANDOM_TEST_GUID, TEST_DATA_CLIENT_INFO, TEST_TOKENS, TEST_URIS, - TEST_POP_VALUES, PREFERRED_CACHE_ALIAS, - TEST_CRYPTO_VALUES, ID_TOKEN_CLAIMS, GUEST_ID_TOKEN_CLAIMS, TEST_CONFIG, @@ -31,67 +28,9 @@ import { Authority } from "../../../src/authority/Authority.js"; import { AuthorityType } from "../../../src/authority/AuthorityType.js"; import { TokenClaims } from "../../../src/index.js"; import { buildAccountFromIdTokenClaims } from "msal-test-utils"; +import { StubPerformanceClient } from "../../../src/telemetry/performance/StubPerformanceClient.js"; -const cryptoInterface: ICrypto = { - createNewGuid(): string { - return RANDOM_TEST_GUID; - }, - base64Decode(input: string): string { - switch (input) { - case TEST_POP_VALUES.ENCODED_REQ_CNF: - return TEST_POP_VALUES.DECODED_REQ_CNF; - case TEST_DATA_CLIENT_INFO.TEST_CACHE_RAW_CLIENT_INFO: - return TEST_DATA_CLIENT_INFO.TEST_CACHE_DECODED_CLIENT_INFO; - case TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO_GUIDS: - return TEST_DATA_CLIENT_INFO.TEST_CACHE_DECODED_CLIENT_INFO_GUIDS; - default: - return input; - } - }, - base64Encode(input: string): string { - switch (input) { - case TEST_POP_VALUES.DECODED_REQ_CNF: - return TEST_POP_VALUES.ENCODED_REQ_CNF; - case "uid": - return "dWlk"; - case "utid": - return "dXRpZA=="; - default: - return input; - } - }, - base64UrlEncode(input: string): string { - switch (input) { - case '{"kid": "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc"}': - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - encodeKid(input: string): string { - switch (input) { - case "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc": - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - async getPublicKeyThumbprint(): Promise { - return TEST_POP_VALUES.KID; - }, - async signJwt(): Promise { - return ""; - }, - async removeTokenBindingKey(): Promise { - return Promise.resolve(true); - }, - async clearKeystore(): Promise { - return Promise.resolve(true); - }, - async hashString(): Promise { - return Promise.resolve(TEST_CRYPTO_VALUES.TEST_SHA256_HASH); - }, -}; +const cryptoInterface: ICrypto = mockCrypto; const networkInterface: INetworkModule = { sendGetRequestAsync(url: string, options?: NetworkRequestOptions): T { @@ -121,11 +60,12 @@ const loggerOptions = { logLevel: LogLevel.Verbose, }; const logger = new Logger(loggerOptions); +const performanceClient = new StubPerformanceClient(); const authority = new Authority( Constants.DEFAULT_AUTHORITY, networkInterface, - new MockStorageClass("client-id", mockCrypto, logger), + new MockStorageClass("client-id", mockCrypto, logger, performanceClient), authorityOptions, logger, TEST_CONFIG.CORRELATION_ID @@ -276,7 +216,12 @@ describe("AccountEntity.ts Unit Tests", () => { const authority = new Authority( Constants.DEFAULT_AUTHORITY, networkInterface, - new MockStorageClass("client-id", mockCrypto, logger), + new MockStorageClass( + "client-id", + mockCrypto, + logger, + performanceClient + ), authorityOptions, logger, TEST_CONFIG.CORRELATION_ID @@ -324,7 +269,12 @@ describe("AccountEntity.ts Unit Tests", () => { const authority = new Authority( Constants.DEFAULT_AUTHORITY, networkInterface, - new MockStorageClass("client-id", mockCrypto, logger), + new MockStorageClass( + "client-id", + mockCrypto, + logger, + performanceClient + ), { protocolMode: ProtocolMode.OIDC, knownAuthorities: [Constants.DEFAULT_AUTHORITY], @@ -657,7 +607,12 @@ describe("AccountEntity.ts Unit Tests for ADFS", () => { const authority = new Authority( "https://myadfs.com/adfs", networkInterface, - new MockStorageClass("client-id", mockCrypto, logger), + new MockStorageClass( + "client-id", + mockCrypto, + logger, + performanceClient + ), authorityOptions, logger, TEST_CONFIG.CORRELATION_ID @@ -713,7 +668,12 @@ describe("AccountEntity.ts Unit Tests for ADFS", () => { const authority = new Authority( "https://myadfs.com/adfs", networkInterface, - new MockStorageClass("client-id", mockCrypto, logger), + new MockStorageClass( + "client-id", + mockCrypto, + logger, + performanceClient + ), authorityOptions, logger, TEST_CONFIG.CORRELATION_ID diff --git a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts index 3b2856cb1a..8ffec6dba2 100644 --- a/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts +++ b/lib/msal-common/test/client/AuthorizationCodeClient.spec.ts @@ -1,5 +1,4 @@ import { - ALTERNATE_OPENID_CONFIG_RESPONSE, AUTHENTICATION_RESULT, DEFAULT_OPENID_CONFIG_RESPONSE, TEST_CONFIG, @@ -7,10 +6,8 @@ import { TEST_URIS, TEST_DATA_CLIENT_INFO, RANDOM_TEST_GUID, - TEST_STATE_VALUES, TEST_POP_VALUES, POP_AUTHENTICATION_RESULT, - TEST_ACCOUNT_INFO, CORS_SIMPLE_REQUEST_HEADERS, TEST_SSH_VALUES, AUTHENTICATION_RESULT_WITH_HEADERS, @@ -19,8 +16,6 @@ import { import { ClientConfiguration } from "../../src/config/ClientConfiguration.js"; import { BaseClient } from "../../src/client/BaseClient.js"; import { - PromptValue, - ResponseMode, AuthenticationScheme, ThrottlingConstants, Constants, @@ -28,13 +23,11 @@ import { ONE_DAY_IN_MS, } from "../../src/utils/Constants.js"; import * as AADServerParamKeys from "../../src/constants/AADServerParamKeys.js"; -import { ClientTestUtils, MockStorageClass } from "./ClientTestUtils.js"; +import { ClientTestUtils } from "./ClientTestUtils.js"; import { TestError } from "../test_kit/TestErrors.js"; import { Authority } from "../../src/authority/Authority.js"; import { AuthorizationCodeClient } from "../../src/client/AuthorizationCodeClient.js"; -import { CommonAuthorizationUrlRequest } from "../../src/request/CommonAuthorizationUrlRequest.js"; import { TokenClaims } from "../../src/account/TokenClaims.js"; -import { ServerError } from "../../src/error/ServerError.js"; import { CommonAuthorizationCodeRequest } from "../../src/request/CommonAuthorizationCodeRequest.js"; import * as AuthToken from "../../src/account/AuthToken.js"; import { @@ -42,13 +35,11 @@ import { createClientAuthError, } from "../../src/error/ClientAuthError.js"; import { - AuthError, CcsCredentialType, ClientConfigurationErrorCodes, createClientConfigurationError, } from "../../src/index.js"; import { ProtocolMode } from "../../src/authority/ProtocolMode.js"; -import { MockPerformanceClient } from "../telemetry/PerformanceClient.spec.js"; describe("AuthorizationCodeClient unit tests", () => { afterEach(() => { @@ -70,1388 +61,6 @@ describe("AuthorizationCodeClient unit tests", () => { }); }); - describe("Authorization url creation", () => { - it("Creates an authorization url with default parameters", async () => { - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.QUERY, - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: TEST_CONFIG.DEFAULT_SCOPES, - codeChallenge: TEST_CONFIG.TEST_CHALLENGE, - codeChallengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(Constants.DEFAULT_AUTHORITY)).toBe(true); - expect( - loginUrl.includes( - DEFAULT_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( - "{tenant}", - "common" - ) - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.SCOPE}=${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_TYPE}=${Constants.CODE_RESPONSE_TYPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.REDIRECT_URI}=${encodeURIComponent( - TEST_URIS.TEST_REDIRECT_URI_LOCALHOST - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent( - ResponseMode.QUERY - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CODE_CHALLENGE}=${encodeURIComponent( - TEST_CONFIG.TEST_CHALLENGE - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${ - AADServerParamKeys.CODE_CHALLENGE_METHOD - }=${encodeURIComponent( - Constants.S256_CODE_CHALLENGE_METHOD - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.X_APP_NAME}=${TEST_CONFIG.applicationName}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.X_APP_VER}=${TEST_CONFIG.applicationVersion}` - ) - ).toBe(true); - }); - - it("Creates an authorization url passing in optional parameters", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FORM_POST, - codeChallenge: TEST_CONFIG.TEST_CHALLENGE, - codeChallengeMethod: TEST_CONFIG.CODE_CHALLENGE_METHOD, - state: TEST_CONFIG.STATE, - prompt: PromptValue.LOGIN, - loginHint: TEST_CONFIG.LOGIN_HINT, - domainHint: TEST_CONFIG.DOMAIN_HINT, - claims: TEST_CONFIG.CLAIMS, - nonce: TEST_CONFIG.NONCE, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(TEST_CONFIG.validAuthority)).toBe(true); - expect( - loginUrl.includes( - DEFAULT_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( - "{tenant}", - "common" - ) - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.SCOPE}=${TEST_CONFIG.DEFAULT_GRAPH_SCOPE}%20${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_TYPE}=${Constants.CODE_RESPONSE_TYPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.REDIRECT_URI}=${encodeURIComponent( - TEST_URIS.TEST_REDIRECT_URI_LOCALHOST - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent( - ResponseMode.FORM_POST - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.STATE}=${encodeURIComponent( - TEST_CONFIG.STATE - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.PROMPT}=${PromptValue.LOGIN}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.NONCE}=${encodeURIComponent( - TEST_CONFIG.NONCE - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CODE_CHALLENGE}=${encodeURIComponent( - TEST_CONFIG.TEST_CHALLENGE - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${ - AADServerParamKeys.CODE_CHALLENGE_METHOD - }=${encodeURIComponent(TEST_CONFIG.CODE_CHALLENGE_METHOD)}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( - TEST_CONFIG.DOMAIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CLAIMS}=${encodeURIComponent( - TEST_CONFIG.CLAIMS - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.X_APP_NAME}=${TEST_CONFIG.applicationName}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.X_APP_VER}=${TEST_CONFIG.applicationVersion}` - ) - ).toBe(true); - }); - - it("Adds CCS entry if loginHint is provided", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const mockPerfClient = new MockPerformanceClient(); - const client = new AuthorizationCodeClient(config, mockPerfClient); - let resEvents; - mockPerfClient.addPerformanceCallback((events) => { - resEvents = events; - }); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - loginHint: TEST_CONFIG.LOGIN_HINT, - prompt: PromptValue.LOGIN, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const rootMeasurement = mockPerfClient.startMeasurement( - "root-measurement", - authCodeUrlRequest.correlationId - ); - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${HeaderNames.CCS_HEADER}=${encodeURIComponent( - `UPN:${TEST_CONFIG.LOGIN_HINT}` - )}` - ) - ).toBe(true); - rootMeasurement.end({ success: true }); - // @ts-ignore - const event = resEvents[0]; - expect(event.loginHintFromRequest).toBeTruthy(); - expect(event.loginHintFromUpn).toBeFalsy(); - expect(event.loginHintFromClaim).toBeFalsy(); - }); - - it("Adds CCS entry if account is provided", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - const testAccount = TEST_ACCOUNT_INFO; - // @ts-ignore - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - | "login_hint" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - prompt: PromptValue.NONE, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - testTokenClaims.sid - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${HeaderNames.CCS_HEADER}=${encodeURIComponent( - `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` - )}` - ) - ).toBe(true); - }); - - it("prefers login_hint claim over sid/upn if both provided", async () => { - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const mockPerfClient = new MockPerformanceClient(); - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config, mockPerfClient); - let resEvents; - mockPerfClient.addPerformanceCallback((events) => { - resEvents = events; - }); - const testAccount = TEST_ACCOUNT_INFO; - // @ts-ignore - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - login_hint: "opaque-login-hint-claim", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - prompt: PromptValue.NONE, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const rootMeasurement = mockPerfClient.startMeasurement( - "root-measurement", - authCodeUrlRequest.correlationId - ); - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - testTokenClaims.sid - )}` - ) - ).toBe(false); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - testTokenClaims.login_hint - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${HeaderNames.CCS_HEADER}=${encodeURIComponent( - `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` - )}` - ) - ).toBe(true); - - rootMeasurement.end({ success: true }); - // @ts-ignore - const event = resEvents[0]; - expect(event.loginHintFromUpn).toBeFalsy(); - expect(event.loginHintFromClaim).toBeTruthy(); - expect(event.loginHintFromRequest).toBeFalsy(); - expect(event.domainHintFromRequest).toBeFalsy(); - expect(event.sidFromClaim).toBeFalsy(); - expect(event.sidFromRequest).toBeFalsy(); - }); - - it("skips login_hint claim if domainHint param is set", async () => { - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const mockPerfClient = new MockPerformanceClient(); - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config, mockPerfClient); - let resEvents; - mockPerfClient.addPerformanceCallback((events) => { - resEvents = events; - }); - const testAccount = TEST_ACCOUNT_INFO; - // @ts-ignore - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - login_hint: "opaque-login-hint-claim", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - domainHint: TEST_CONFIG.DOMAIN_HINT, - }; - const rootMeasurement = mockPerfClient.startMeasurement( - "root-measurement", - authCodeUrlRequest.correlationId - ); - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - testTokenClaims.sid - )}` - ) - ).toBe(false); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - testAccount.username - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( - TEST_CONFIG.DOMAIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${HeaderNames.CCS_HEADER}=${encodeURIComponent( - `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` - )}` - ) - ).toBe(true); - - rootMeasurement.end({ success: true }); - // @ts-ignore - const event = resEvents[0]; - expect(event.loginHintFromUpn).toBeTruthy(); - expect(event.loginHintFromClaim).toBeFalsy(); - expect(event.loginHintFromRequest).toBeFalsy(); - expect(event.domainHintFromRequest).toBeTruthy(); - expect(event.sidFromClaim).toBeFalsy(); - expect(event.sidFromRequest).toBeFalsy(); - }); - - it("picks up both loginHint and domainHint params", async () => { - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - const testAccount = TEST_ACCOUNT_INFO; - // @ts-ignore - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - login_hint: "opaque-login-hint-claim", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - domainHint: TEST_CONFIG.DOMAIN_HINT, - loginHint: TEST_CONFIG.LOGIN_HINT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - testTokenClaims.sid - )}` - ) - ).toBe(false); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( - TEST_CONFIG.DOMAIN_HINT - )}` - ) - ).toBe(true); - }); - - it("Prefers sid over loginHint if both provided and prompt=None", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const mockPerfClient = new MockPerformanceClient(); - const client = new AuthorizationCodeClient(config, mockPerfClient); - let resEvents; - mockPerfClient.addPerformanceCallback((events) => { - resEvents = events; - }); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - loginHint: TEST_CONFIG.LOGIN_HINT, - prompt: PromptValue.NONE, - sid: TEST_CONFIG.SID, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const rootMeasurement = mockPerfClient.startMeasurement( - "root-measurement", - authCodeUrlRequest.correlationId - ); - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl).toEqual( - expect.not.arrayContaining([ - `${AADServerParamKeys.LOGIN_HINT}=`, - ]) - ); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - TEST_CONFIG.SID - )}` - ) - ).toBe(true); - - rootMeasurement.end({ success: true }); - // @ts-ignore - const event = resEvents[0]; - expect(event.loginHintFromRequest).toBeFalsy(); - expect(event.loginHintFromClaim).toBeFalsy(); - expect(event.loginHintFromUpn).toBeFalsy(); - expect(event.domainHintFromRequest).toBeFalsy(); - expect(event.sidFromRequest).toBeTruthy(); - expect(event.sidFromRequest).toBeTruthy(); - expect(event.prompt).toEqual(PromptValue.NONE); - }); - - it("Prefers loginHint over sid if both provided and prompt!=None", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - loginHint: TEST_CONFIG.LOGIN_HINT, - prompt: PromptValue.LOGIN, - sid: TEST_CONFIG.SID, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Ignores sid if prompt!=None", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const mockPerfClient = new MockPerformanceClient(); - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config, mockPerfClient); - let resEvents; - mockPerfClient.addPerformanceCallback((events) => { - resEvents = events; - }); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - prompt: PromptValue.LOGIN, - sid: TEST_CONFIG.SID, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const rootMeasurement = mockPerfClient.startMeasurement( - "root-measurement", - authCodeUrlRequest.correlationId - ); - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( - false - ); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - - rootMeasurement.end({ success: true }); - // @ts-ignore - const event = resEvents[0]; - expect(event.loginHintFromUpn).toBeFalsy(); - expect(event.loginHintFromClaim).toBeFalsy(); - expect(event.loginHintFromRequest).toBeFalsy(); - expect(event.domainHintFromRequest).toBeFalsy(); - expect(event.sidFromClaim).toBeFalsy(); - expect(event.sidFromRequest).toBeFalsy(); - expect(event.prompt).toEqual(PromptValue.LOGIN); - }); - - it("Prefers loginHint over Account if both provided and account does not have token claims", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - loginHint: TEST_CONFIG.LOGIN_HINT, - account: TEST_ACCOUNT_INFO, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_ACCOUNT_INFO.username - )}` - ) - ).toBe(false); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Uses sid from account if not provided in request and prompt=None, overrides login_hint", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - const testAccount = TEST_ACCOUNT_INFO; - // @ts-ignore - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - loginHint: TEST_CONFIG.LOGIN_HINT, - prompt: PromptValue.NONE, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SID}=${encodeURIComponent( - testTokenClaims.sid - )}` - ) - ).toBe(true); - expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( - false - ); - }); - - it("Uses loginHint instead of sid from account prompt!=None", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - const testAccount = TEST_ACCOUNT_INFO; - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - | "iat" - | "x5c_ca" - | "ts" - | "at" - | "u" - | "p" - | "m" - | "login_hint" - | "aud" - | "nbf" - | "roles" - | "amr" - | "idp" - | "auth_time" - | "tfp" - | "acr" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - sid: "testSid", - tenant_region_scope: "test_tenant_region_scope", - tenant_region_sub_scope: "test_tenant_region_sub_scope", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - loginHint: TEST_CONFIG.LOGIN_HINT, - prompt: PromptValue.LOGIN, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - }); - - it("Uses login_hint instead of username if sid is not present in token claims for account or request", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - const testAccount = TEST_ACCOUNT_INFO; - const testTokenClaims: Required< - Omit< - TokenClaims, - | "home_oid" - | "upn" - | "cloud_instance_host_name" - | "cnf" - | "emails" - | "sid" - | "iat" - | "x5c_ca" - | "ts" - | "at" - | "u" - | "p" - | "m" - | "login_hint" - | "aud" - | "nbf" - | "roles" - | "amr" - | "idp" - | "auth_time" - | "tfp" - | "acr" - > - > = { - ver: "2.0", - iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, - sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", - exp: 1536361411, - name: "Abe Lincoln", - preferred_username: "AbeLi@microsoft.com", - oid: "00000000-0000-0000-66f3-3332eca7ea81", - tid: "3338040d-6c67-4c5b-b112-36a304b66dad", - nonce: "123523", - tenant_region_scope: "test_tenant_region_scope", - tenant_region_sub_scope: "test_tenant_region_sub_scope", - }; - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: { - ...testAccount, - idTokenClaims: testTokenClaims, - }, - loginHint: TEST_CONFIG.LOGIN_HINT, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_CONFIG.LOGIN_HINT - )}` - ) - ).toBe(true); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Sets login_hint to Account.username if login_hint and sid are not provided", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: TEST_ACCOUNT_INFO, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( - TEST_ACCOUNT_INFO.username - )}` - ) - ).toBe(true); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Ignores Account if prompt is select_account", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - account: TEST_ACCOUNT_INFO, - prompt: "select_account", - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( - false - ); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Ignores loginHint if prompt is select_account", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - loginHint: "testaccount@microsoft.com", - prompt: "select_account", - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( - false - ); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Ignores sid if prompt is select_account", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - sid: "testsid", - prompt: "select_account", - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( - false - ); - expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); - }); - - it("Creates a login URL with scopes from given token request", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(ALTERNATE_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const testScope1 = "testscope1"; - const testScope2 = "testscope2"; - const loginRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIR_URI, - scopes: [testScope1, testScope2], - codeChallenge: TEST_CONFIG.TEST_CHALLENGE, - codeChallengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FRAGMENT, - }; - - const loginUrl = await client.getAuthCodeUrl(loginRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.SCOPE}=${encodeURIComponent( - `${testScope1} ${testScope2}` - )}` - ) - ).toBe(true); - }); - - it("Does not append an extra '?' when the authorization endpoint already contains a query string", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromHardcodedValues" - ).mockReturnValue({ - token_endpoint: - "https://login.windows.net/common/oauth2/v2.0/token?param1=value1", - issuer: "https://login.windows.net/{tenantid}/v2.0", - userinfo_endpoint: "https://graph.microsoft.com/oidc/userinfo", - authorization_endpoint: - "https://login.windows.net/common/oauth2/v2.0/authorize?param1=value1", - end_session_endpoint: - "https://login.windows.net/common/oauth2/v2.0/logout?param1=value1", - }); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.QUERY, - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: TEST_CONFIG.DEFAULT_SCOPES, - codeChallenge: TEST_CONFIG.TEST_CHALLENGE, - codeChallengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.BEARER, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect(loginUrl.split("?").length).toEqual(2); - expect(loginUrl.includes(`param1=value1`)).toBe(true); - expect( - loginUrl.includes( - ALTERNATE_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( - "{tenant}", - "common" - ) - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.SCOPE}=${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_TYPE}=${Constants.CODE_RESPONSE_TYPE}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.REDIRECT_URI}=${encodeURIComponent( - TEST_URIS.TEST_REDIRECT_URI_LOCALHOST - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent( - ResponseMode.QUERY - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.CODE_CHALLENGE}=${encodeURIComponent( - TEST_CONFIG.TEST_CHALLENGE - )}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${ - AADServerParamKeys.CODE_CHALLENGE_METHOD - }=${encodeURIComponent( - Constants.S256_CODE_CHALLENGE_METHOD - )}` - ) - ).toBe(true); - }); - }); - - it("Adds req-cnf as needed", async () => { - // Override with alternate authority openid_config - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); - - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - if (!config.cryptoInterface) { - throw TestError.createTestSetupError( - "configuration cryptoInterface not initialized correctly." - ); - } - - const authCodeUrlRequest: CommonAuthorizationUrlRequest = { - redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, - scopes: [ - ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, - ...TEST_CONFIG.DEFAULT_SCOPES, - ], - authority: TEST_CONFIG.validAuthority, - responseMode: ResponseMode.FORM_POST, - codeChallenge: TEST_CONFIG.TEST_CHALLENGE, - codeChallengeMethod: TEST_CONFIG.CODE_CHALLENGE_METHOD, - state: TEST_CONFIG.STATE, - prompt: PromptValue.LOGIN, - loginHint: TEST_CONFIG.LOGIN_HINT, - domainHint: TEST_CONFIG.DOMAIN_HINT, - claims: TEST_CONFIG.CLAIMS, - nonce: TEST_CONFIG.NONCE, - correlationId: RANDOM_TEST_GUID, - authenticationScheme: AuthenticationScheme.POP, - platformBroker: true, - }; - const loginUrl = await client.getAuthCodeUrl(authCodeUrlRequest); - expect( - loginUrl.includes( - `${AADServerParamKeys.NATIVE_BROKER}=${encodeURIComponent("1")}` - ) - ).toBe(true); - expect( - loginUrl.includes( - `${AADServerParamKeys.REQ_CNF}=${encodeURIComponent( - TEST_POP_VALUES.ENCODED_REQ_CNF - )}` - ) - ).toBe(true); - }); - - describe("handleFragmentResponse()", () => { - it("returns valid server code response", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - - const client: AuthorizationCodeClient = new AuthorizationCodeClient( - config - ); - const authCodePayload = client.handleFragmentResponse( - { - code: "thisIsATestCode", - state: TEST_STATE_VALUES.ENCODED_LIB_STATE, - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - }, - TEST_STATE_VALUES.ENCODED_LIB_STATE - ); - expect(authCodePayload.code).toBe("thisIsATestCode"); - expect(authCodePayload.state).toBe( - TEST_STATE_VALUES.ENCODED_LIB_STATE - ); - }); - - it("throws server error when error is in hash", async () => { - jest.spyOn( - Authority.prototype, - "getEndpointMetadataFromNetwork" - ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client: AuthorizationCodeClient = new AuthorizationCodeClient( - config - ); - const cacheStorageMock = - config.storageInterface as MockStorageClass; - - let error: AuthError | null = null; - try { - client.handleFragmentResponse( - { - error: "error_code", - error_description: "msal error description", - state: TEST_STATE_VALUES.ENCODED_LIB_STATE, - }, - TEST_STATE_VALUES.ENCODED_LIB_STATE - ); - } catch (e) { - error = e as AuthError; - } - expect(error).toBeInstanceOf(ServerError); - expect(error?.errorCode).toEqual("error_code"); - expect(error?.errorMessage).toEqual("msal error description"); - expect(cacheStorageMock.getKeys().length).toBe(1); - expect(cacheStorageMock.getAuthorityMetadataKeys().length).toBe(1); - }); - }); - describe("Acquire a token", () => { let config: ClientConfiguration; beforeEach(async () => { @@ -2235,7 +844,11 @@ describe("AuthorizationCodeClient unit tests", () => { ).toBe(true); expect( returnVal.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${ + AADServerParamKeys.X_MS_LIB_CAPABILITY + }=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(true); }); @@ -2416,7 +1029,11 @@ describe("AuthorizationCodeClient unit tests", () => { ).toBe(true); expect( returnVal.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${ + AADServerParamKeys.X_MS_LIB_CAPABILITY + }=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(true); }); @@ -3358,7 +1975,8 @@ describe("AuthorizationCodeClient unit tests", () => { .find((value) => value.indexOf("accesstoken") >= 0); const accessTokenCacheItem = accessTokenKey ? config.storageInterface?.getAccessTokenCredential( - accessTokenKey + accessTokenKey, + RANDOM_TEST_GUID ) : null; @@ -3957,168 +2575,6 @@ describe("AuthorizationCodeClient unit tests", () => { }); }); - describe("createAuthCodeUrlQueryString tests", () => { - it("pick up default client_id", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - }); - - expect(queryString).toContain( - `client_id=${TEST_CONFIG.MSAL_CLIENT_ID}` - ); - }); - - it("pick up extra query client_id param", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - extraQueryParameters: { - client_id: "child_client_id", - }, - }); - - expect(queryString).toContain(`client_id=child_client_id`); - }); - - it("pick up instance_aware config param when set to true", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - config.authOptions.instanceAware = true; - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - }); - - expect(queryString).toContain(`instance_aware=true`); - }); - - it("do not pick up instance_aware config param when set to false", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - config.authOptions.instanceAware = false; - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - }); - - expect(queryString.includes("instance_aware")).toBeFalsy(); - }); - - it("pick up instance_aware EQ param when config is set to false", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - config.authOptions.instanceAware = false; - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - extraQueryParameters: { - instance_aware: "true", - }, - }); - - expect(queryString).toContain(`instance_aware=true`); - }); - - it("pick up instance_aware EQ param when config is set to true", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - config.authOptions.instanceAware = true; - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - prompt: PromptValue.LOGIN, - redirectUri: "localhost", - extraQueryParameters: { - instance_aware: "false", - }, - }); - - expect(queryString).toContain(`instance_aware=false`); - }); - - it("pick up broker params", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - redirectUri: "localhost", - embeddedClientId: "child_client_id_1", - }); - - expect(queryString).toContain(`client_id=child_client_id_1`); - expect(queryString).toContain( - `brk_client_id=${config.authOptions.clientId}` - ); - expect(queryString).toContain( - `brk_redirect_uri=${encodeURIComponent("https://localhost")}` - ); - }); - - it("broker params take precedence over extra query params", async () => { - const config: ClientConfiguration = - await ClientTestUtils.createTestClientConfiguration(); - const client = new AuthorizationCodeClient(config); - - const queryString = - // @ts-ignore - await client.createAuthCodeUrlQueryString({ - scopes: ["User.Read"], - redirectUri: "localhost", - embeddedClientId: "child_client_id_1", - extraQueryParameters: { - client_id: "child_client_id_2", - brk_client_id: "broker_client_id_2", - brk_redirect_uri: "broker_redirect_uri_2", - }, - }); - - expect(queryString).toContain(`client_id=child_client_id_1`); - expect(queryString).toContain( - `brk_client_id=${config.authOptions.clientId}` - ); - expect(queryString).toContain( - `brk_redirect_uri=${encodeURIComponent("https://localhost")}` - ); - }); - }); - describe("createTokenRequestBody tests", () => { it("pick up default client_id", async () => { const config: ClientConfiguration = diff --git a/lib/msal-common/test/client/BaseClient.spec.ts b/lib/msal-common/test/client/BaseClient.spec.ts index 36850a7f57..05e9e3a8ce 100644 --- a/lib/msal-common/test/client/BaseClient.spec.ts +++ b/lib/msal-common/test/client/BaseClient.spec.ts @@ -220,7 +220,8 @@ describe("BaseClient.ts Class Unit Tests", () => { ThrottlingUtils.preProcess( // @ts-ignore client.cacheManager, - thumbprint + thumbprint, + RANDOM_TEST_GUID ) ).toThrowError(ServerError); }); diff --git a/lib/msal-common/test/client/ClientTestUtils.ts b/lib/msal-common/test/client/ClientTestUtils.ts index 620a3e9017..750843f6ea 100644 --- a/lib/msal-common/test/client/ClientTestUtils.ts +++ b/lib/msal-common/test/client/ClientTestUtils.ts @@ -8,6 +8,7 @@ import { TEST_CONFIG, TEST_CRYPTO_VALUES, TEST_POP_VALUES, + TEST_TOKENS, } from "../test_kit/StringConstants.js"; import { CacheManager } from "../../src/cache/CacheManager.js"; @@ -29,9 +30,10 @@ import { createClientAuthError, } from "../../src/error/ClientAuthError.js"; import { ServerTelemetryManager } from "../../src/telemetry/server/ServerTelemetryManager.js"; -import { Constants } from "../../src/utils/Constants.js"; +import { Constants, EncodingTypes } from "../../src/utils/Constants.js"; import { AuthorityOptions } from "../../src/authority/AuthorityOptions.js"; import { TokenKeys } from "../../src/cache/utils/CacheTypes.js"; +import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient.js"; const ACCOUNT_KEYS = "ACCOUNT_KEYS"; const TOKEN_KEYS = "TOKEN_KEYS"; @@ -59,8 +61,8 @@ export class MockStorageClass extends CacheManager { } } - async removeAccount(key: string): Promise { - await super.removeAccount(key); + removeAccount(key: string): void { + super.removeAccount(key, RANDOM_TEST_GUID); const currentAccounts = this.getAccountKeys(); const removalIndex = currentAccounts.indexOf(key); if (removalIndex > -1) { @@ -70,17 +72,17 @@ export class MockStorageClass extends CacheManager { } getAccountKeys(): string[] { - return this.store[ACCOUNT_KEYS] || []; + return [...(this.store[ACCOUNT_KEYS] || [])]; } getTokenKeys(): TokenKeys { - return ( - this.store[TOKEN_KEYS] || { + return { + ...(this.store[TOKEN_KEYS] || { idToken: [], accessToken: [], refreshToken: [], - } - ); + }), + } as TokenKeys; } // Credentials (idtokens) @@ -196,27 +198,30 @@ export const mockCrypto = { return RANDOM_TEST_GUID; }, base64Decode(input: string): string { - return Buffer.from(input, "base64").toString("utf8"); + return Buffer.from(input, EncodingTypes.BASE64).toString("utf8"); }, base64Encode(input: string): string { - return Buffer.from(input, "utf-8").toString("base64"); + return Buffer.from(input, EncodingTypes.UTF8).toString( + EncodingTypes.BASE64 + ); }, base64UrlEncode(input: string): string { - return Buffer.from(input, "utf-8").toString("base64url"); + return Buffer.from(input, EncodingTypes.UTF8).toString("base64url"); }, encodeKid(input: string): string { - return Buffer.from(JSON.stringify({ kid: input }), "utf-8").toString( - "base64url" - ); + return Buffer.from( + JSON.stringify({ kid: input }), + EncodingTypes.UTF8 + ).toString("base64url"); }, async getPublicKeyThumbprint(): Promise { return TEST_POP_VALUES.KID; }, - async removeTokenBindingKey(keyId: string): Promise { - return Promise.resolve(true); + async removeTokenBindingKey(keyId: string): Promise { + return Promise.resolve(); }, async signJwt(): Promise { - return ""; + return TEST_TOKENS.POP_TOKEN; }, async clearKeystore(): Promise { return Promise.resolve(true); @@ -235,6 +240,7 @@ export class ClientTestUtils { TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, new Logger({}), + new StubPerformanceClient(), { canonicalAuthority: TEST_CONFIG.validAuthority, } @@ -253,35 +259,11 @@ export class ClientTestUtils { }, }; - const authorityOptions: AuthorityOptions = { - protocolMode: protocolMode, - knownAuthorities: [TEST_CONFIG.validAuthority], - cloudDiscoveryMetadata: "", - authorityMetadata: "", - }; - - const loggerOptions = { - loggerCallback: (): void => {}, - piiLoggingEnabled: true, - logLevel: LogLevel.Verbose, - }; - const logger = new Logger(loggerOptions); - - const authority = new Authority( - TEST_CONFIG.validAuthority, - mockHttpClient, - mockStorage, - authorityOptions, - logger, - TEST_CONFIG.CORRELATION_ID + const authority = await getDiscoveredAuthority( + protocolMode, + mockStorage ); - await authority.resolveEndpointsAsync().catch((error) => { - throw createClientAuthError( - ClientAuthErrorCodes.endpointResolutionError - ); - }); - let serverTelemetryManager = null; if (telem) { @@ -330,3 +312,56 @@ export class ClientTestUtils { }; } } + +export async function getDiscoveredAuthority( + protocolMode: ProtocolMode = ProtocolMode.AAD, + mockStorage: MockStorageClass = new MockStorageClass( + TEST_CONFIG.MSAL_CLIENT_ID, + mockCrypto, + new Logger({}), + new StubPerformanceClient(), + { + canonicalAuthority: TEST_CONFIG.validAuthority, + } + ) +): Promise { + const mockHttpClient = { + sendGetRequestAsync(): T { + return {} as T; + }, + sendPostRequestAsync(): T { + return {} as T; + }, + }; + + const authorityOptions: AuthorityOptions = { + protocolMode: protocolMode, + knownAuthorities: [TEST_CONFIG.validAuthority], + cloudDiscoveryMetadata: "", + authorityMetadata: "", + }; + + const loggerOptions = { + loggerCallback: (): void => {}, + piiLoggingEnabled: true, + logLevel: LogLevel.Verbose, + }; + const logger = new Logger(loggerOptions); + + const authority = new Authority( + TEST_CONFIG.validAuthority, + mockHttpClient, + mockStorage, + authorityOptions, + logger, + TEST_CONFIG.CORRELATION_ID + ); + + await authority.resolveEndpointsAsync().catch((error) => { + throw createClientAuthError( + ClientAuthErrorCodes.endpointResolutionError + ); + }); + + return authority; +} diff --git a/lib/msal-common/test/client/RefreshTokenClient.spec.ts b/lib/msal-common/test/client/RefreshTokenClient.spec.ts index 95ed518136..c15db39244 100644 --- a/lib/msal-common/test/client/RefreshTokenClient.spec.ts +++ b/lib/msal-common/test/client/RefreshTokenClient.spec.ts @@ -19,6 +19,7 @@ import { CORS_RESPONSE_HEADERS, TEST_SSH_VALUES, BAD_TOKEN_ERROR_RESPONSE, + RANDOM_TEST_GUID, } from "../test_kit/StringConstants.js"; import { BaseClient } from "../../src/client/BaseClient.js"; import { @@ -359,7 +360,10 @@ describe("RefreshTokenClient unit tests", () => { testFamilyRefreshTokenEntity, TEST_CONFIG.CORRELATION_ID ); - config.storageInterface!.setAppMetadata(testAppMetadata); + config.storageInterface!.setAppMetadata( + testAppMetadata, + RANDOM_TEST_GUID + ); client = new RefreshTokenClient(config, stubPerformanceClient); }); @@ -519,7 +523,11 @@ describe("RefreshTokenClient unit tests", () => { ).toBe(true); expect( result.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${ + AADServerParamKeys.X_MS_LIB_CAPABILITY + }=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(true); }); @@ -781,7 +789,11 @@ describe("RefreshTokenClient unit tests", () => { ).toBe(true); expect( result.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${ + AADServerParamKeys.X_MS_LIB_CAPABILITY + }=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(true); }); @@ -899,7 +911,11 @@ describe("RefreshTokenClient unit tests", () => { ).toBe(true); expect( result.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${ + AADServerParamKeys.X_MS_LIB_CAPABILITY + }=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(true); }); @@ -1092,7 +1108,10 @@ describe("RefreshTokenClient unit tests", () => { testFamilyRefreshTokenEntity, TEST_CONFIG.CORRELATION_ID ); - config.storageInterface!.setAppMetadata(testAppMetadata); + config.storageInterface!.setAppMetadata( + testAppMetadata, + RANDOM_TEST_GUID + ); client = new RefreshTokenClient(config, stubPerformanceClient); }); @@ -1400,7 +1419,10 @@ describe("RefreshTokenClient unit tests", () => { rtEntity, TEST_CONFIG.CORRELATION_ID ); - config.storageInterface!.setAppMetadata(testAppMetadata); + config.storageInterface!.setAppMetadata( + testAppMetadata, + RANDOM_TEST_GUID + ); const mockPerfClient = new MockPerformanceClient(); const rootMeasurement = mockPerfClient.startMeasurement( "test-measurement", @@ -1443,7 +1465,8 @@ describe("RefreshTokenClient unit tests", () => { expect( config.storageInterface!.getRefreshTokenCredential( - badRefreshTokenKey + badRefreshTokenKey, + RANDOM_TEST_GUID ) ).toBe(rtEntity); @@ -1453,7 +1476,8 @@ describe("RefreshTokenClient unit tests", () => { expect( config.storageInterface!.getRefreshTokenCredential( - badRefreshTokenKey + badRefreshTokenKey, + RANDOM_TEST_GUID ) ).toBe(null); diff --git a/lib/msal-common/test/client/SilentFlowClient.spec.ts b/lib/msal-common/test/client/SilentFlowClient.spec.ts index b62fa17c51..1922e3642a 100644 --- a/lib/msal-common/test/client/SilentFlowClient.spec.ts +++ b/lib/msal-common/test/client/SilentFlowClient.spec.ts @@ -783,7 +783,8 @@ describe("SilentFlowClient unit tests", () => { new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, - logger + logger, + new StubPerformanceClient() ) ); client = new SilentFlowClient(config, stubPerformanceClient); diff --git a/lib/msal-common/test/config/ClientConfiguration.spec.ts b/lib/msal-common/test/config/ClientConfiguration.spec.ts index 9a49c658f1..b6f778bd93 100644 --- a/lib/msal-common/test/config/ClientConfiguration.spec.ts +++ b/lib/msal-common/test/config/ClientConfiguration.spec.ts @@ -6,16 +6,13 @@ import { AuthError } from "../../src/error/AuthError.js"; import { NetworkRequestOptions } from "../../src/network/INetworkModule.js"; import { Logger, LogLevel } from "../../src/logger/Logger.js"; import { version } from "../../src/packageMetadata.js"; -import { - TEST_CONFIG, - TEST_CRYPTO_VALUES, - TEST_POP_VALUES, -} from "../test_kit/StringConstants.js"; +import { RANDOM_TEST_GUID, TEST_CONFIG } from "../test_kit/StringConstants.js"; import { MockStorageClass, mockCrypto } from "../client/ClientTestUtils.js"; import { MockCache } from "../cache/entities/cacheConstants.js"; import { Constants } from "../../src/utils/Constants.js"; import * as ClientAuthErrorCodes from "../../src/error/ClientAuthErrorCodes.js"; import { createClientAuthError } from "../../src/error/ClientAuthError.js"; +import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient.js"; describe("ClientConfiguration.ts Class Unit Tests", () => { it("buildConfiguration assigns default functions", async () => { @@ -51,12 +48,12 @@ describe("ClientConfiguration.ts Class Unit Tests", () => { expect(emptyConfig.storageInterface).not.toBeNull(); expect(emptyConfig.storageInterface.getAccount).not.toBeNull(); expect(() => - emptyConfig.storageInterface.getAccount("testKey") + emptyConfig.storageInterface.getAccount("testKey", RANDOM_TEST_GUID) ).toThrowError( createClientAuthError(ClientAuthErrorCodes.methodNotImplemented) ); expect(() => - emptyConfig.storageInterface.getAccount("testKey") + emptyConfig.storageInterface.getAccount("testKey", RANDOM_TEST_GUID) ).toThrowError(AuthError); expect(emptyConfig.storageInterface.getKeys).not.toBeNull(); expect(() => emptyConfig.storageInterface.getKeys()).toThrowError( @@ -67,12 +64,12 @@ describe("ClientConfiguration.ts Class Unit Tests", () => { ); expect(emptyConfig.storageInterface.removeItem).not.toBeNull(); expect(() => - emptyConfig.storageInterface.removeItem("testKey") + emptyConfig.storageInterface.removeItem("testKey", RANDOM_TEST_GUID) ).toThrowError( createClientAuthError(ClientAuthErrorCodes.methodNotImplemented) ); expect(() => - emptyConfig.storageInterface.removeItem("testKey") + emptyConfig.storageInterface.removeItem("testKey", RANDOM_TEST_GUID) ).toThrowError(AuthError); expect(emptyConfig.storageInterface.setAccount).not.toBeNull(); expect(() => @@ -124,7 +121,8 @@ describe("ClientConfiguration.ts Class Unit Tests", () => { const cacheStorageMock = new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, - new Logger({}) + new Logger({}), + new StubPerformanceClient() ); const testNetworkResult = { @@ -137,48 +135,7 @@ describe("ClientConfiguration.ts Class Unit Tests", () => { authOptions: { clientId: TEST_CONFIG.MSAL_CLIENT_ID, }, - cryptoInterface: { - createNewGuid: (): string => { - return "newGuid"; - }, - base64Decode: (input: string): string => { - return "testDecodedString"; - }, - base64Encode: (input: string): string => { - return "testEncodedString"; - }, - base64UrlEncode(input: string): string { - switch (input) { - case '{"kid": "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc"}': - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - encodeKid(input: string): string { - switch (input) { - case "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc": - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - async getPublicKeyThumbprint(): Promise { - return TEST_POP_VALUES.KID; - }, - async signJwt(): Promise { - return "signedJwt"; - }, - async removeTokenBindingKey(): Promise { - return Promise.resolve(true); - }, - async clearKeystore(): Promise { - return Promise.resolve(true); - }, - async hashString(): Promise { - return Promise.resolve(TEST_CRYPTO_VALUES.TEST_SHA256_HASH); - }, - }, + cryptoInterface: mockCrypto, storageInterface: cacheStorageMock, networkInterface: { sendGetRequestAsync: async ( @@ -219,62 +176,11 @@ describe("ClientConfiguration.ts Class Unit Tests", () => { }, }); await cacheStorageMock.setAccount(MockCache.acc); - // Crypto interface tests expect(newConfig.cryptoInterface).not.toBeNull(); - expect(newConfig.cryptoInterface.base64Decode).not.toBeNull(); - expect(newConfig.cryptoInterface.base64Decode("testString")).toBe( - "testDecodedString" - ); - expect(newConfig.cryptoInterface.base64Encode).not.toBeNull(); - expect(newConfig.cryptoInterface.base64Encode("testString")).toBe( - "testEncodedString" - ); - expect(newConfig.cryptoInterface.removeTokenBindingKey).not.toBeNull(); - expect( - newConfig.cryptoInterface.removeTokenBindingKey("testString") - ).resolves.toBe(true); - // Storage interface tests expect(newConfig.storageInterface).not.toBeNull(); - expect(newConfig.storageInterface.getAccount).not.toBeNull(); - expect( - newConfig.storageInterface.getAccount( - MockCache.acc.generateAccountKey() - ) - ).toBe(MockCache.acc); - expect(newConfig.storageInterface.getKeys).not.toBeNull(); - expect(newConfig.storageInterface.getKeys()).toEqual([ - MockCache.acc.generateAccountKey(), - "ACCOUNT_KEYS", - ]); - expect(newConfig.storageInterface.removeItem).not.toBeNull(); - expect(newConfig.storageInterface.removeItem).toBe( - cacheStorageMock.removeItem - ); - expect(newConfig.storageInterface.setAccount).not.toBeNull(); - expect(newConfig.storageInterface.setAccount).toBe( - cacheStorageMock.setAccount - ); - // Network interface tests expect(newConfig.networkInterface).not.toBeNull(); - expect(newConfig.networkInterface.sendGetRequestAsync).not.toBeNull(); - - expect( - //@ts-ignore - newConfig.networkInterface.sendGetRequestAsync("", null) - ).resolves.toBe(testNetworkResult); - expect(newConfig.networkInterface.sendPostRequestAsync).not.toBeNull(); - - expect( - //@ts-ignore - newConfig.networkInterface.sendPostRequestAsync("", null) - ).resolves.toBe(testNetworkResult); - // Logger option tests expect(newConfig.loggerOptions).not.toBeNull(); - expect(newConfig.loggerOptions.loggerCallback).not.toBeNull(); - expect(newConfig.loggerOptions.piiLoggingEnabled).toBe(true); - // Cache options tests expect(newConfig.cacheOptions).not.toBeNull(); - expect(newConfig.cacheOptions.claimsBasedCachingEnabled).toBe(true); // Client info tests expect(newConfig.libraryInfo.sku).toBe(TEST_CONFIG.TEST_SKU); expect(newConfig.libraryInfo.version).toBe(TEST_CONFIG.TEST_VERSION); diff --git a/lib/msal-common/test/crypto/PopTokenGenerator.spec.ts b/lib/msal-common/test/crypto/PopTokenGenerator.spec.ts index c7906b2b10..c70231ee96 100644 --- a/lib/msal-common/test/crypto/PopTokenGenerator.spec.ts +++ b/lib/msal-common/test/crypto/PopTokenGenerator.spec.ts @@ -1,10 +1,8 @@ import { RANDOM_TEST_GUID, TEST_POP_VALUES, - TEST_DATA_CLIENT_INFO, TEST_CONFIG, TEST_URIS, - TEST_CRYPTO_VALUES, } from "../test_kit/StringConstants.js"; import { PopTokenGenerator } from "../../src/crypto/PopTokenGenerator.js"; import { ICrypto } from "../../src/crypto/ICrypto.js"; @@ -14,78 +12,14 @@ import { UrlString } from "../../src/url/UrlString.js"; import { AuthenticationScheme } from "../../src/utils/Constants.js"; import { SignedHttpRequest } from "../../src/crypto/SignedHttpRequest.js"; import { Logger } from "../../src/logger/Logger.js"; +import { mockCrypto } from "../client/ClientTestUtils.js"; describe("PopTokenGenerator Unit Tests", () => { afterEach(() => { jest.restoreAllMocks(); }); - const cryptoInterface: ICrypto = { - createNewGuid(): string { - return RANDOM_TEST_GUID; - }, - base64Decode(input: string): string { - switch (input) { - case TEST_POP_VALUES.ENCODED_REQ_CNF: - return TEST_POP_VALUES.DECODED_REQ_CNF; - case TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO: - return TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO; - case TEST_POP_VALUES.SAMPLE_POP_AT_PAYLOAD_ENCODED: - return TEST_POP_VALUES.SAMPLE_POP_AT_PAYLOAD_DECODED; - default: - return input; - } - }, - base64Encode(input: string): string { - switch (input) { - case "123-test-uid": - return "MTIzLXRlc3QtdWlk"; - case "456-test-uid": - return "NDU2LXRlc3QtdWlk"; - case TEST_POP_VALUES.DECODED_REQ_CNF: - return TEST_POP_VALUES.ENCODED_REQ_CNF; - case TEST_POP_VALUES.SAMPLE_POP_AT_PAYLOAD_DECODED: - return TEST_POP_VALUES.SAMPLE_POP_AT_PAYLOAD_ENCODED; - default: - return input; - } - }, - base64UrlEncode(input: string): string { - switch (input) { - case '{"kid": "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc"}': - return "e2tpZDogIlhuc3VBdnR0VFBwMG5uMUtfWU1MZVBMRGJwN3N5Q0toTkh0N0hqWUhKWWMifQ"; - case '{"kid":"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs","xms_ksl":"sw"}': - return "eyJraWQiOiJOemJMc1hoOHVEQ2NkLTZNTndYRjRXXzdub1dYRlpBZkhreFpzUkdDOVhzIiwieG1zX2tzbCI6InN3In0"; - default: - return input; - } - }, - encodeKid(input: string): string { - switch (input) { - case "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc": - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - case "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs": - return "eyJraWQiOiJOemJMc1hoOHVEQ2NkLTZNTndYRjRXXzdub1dYRlpBZkhreFpzUkdDOVhzIiwieG1zX2tzbCI6InN3In0"; - default: - return input; - } - }, - async getPublicKeyThumbprint(): Promise { - return TEST_POP_VALUES.KID; - }, - async signJwt(): Promise { - return ""; - }, - async removeTokenBindingKey(): Promise { - return Promise.resolve(true); - }, - async clearKeystore(): Promise { - return Promise.resolve(true); - }, - async hashString(): Promise { - return Promise.resolve(TEST_CRYPTO_VALUES.TEST_SHA256_HASH); - }, - }; + const cryptoInterface: ICrypto = mockCrypto; describe("generateCnf", () => { const testRequest = { diff --git a/lib/msal-common/test/network/ThrottlingUtils.spec.ts b/lib/msal-common/test/network/ThrottlingUtils.spec.ts index 01f01ef712..3a0f9b0232 100644 --- a/lib/msal-common/test/network/ThrottlingUtils.spec.ts +++ b/lib/msal-common/test/network/ThrottlingUtils.spec.ts @@ -13,9 +13,13 @@ import { THUMBPRINT, THROTTLING_ENTITY, TEST_CONFIG, + RANDOM_TEST_GUID, } from "../test_kit/StringConstants.js"; import { ServerError } from "../../src/error/ServerError.js"; import { BaseAuthRequest, Logger } from "../../src/index.js"; +import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient.js"; + +const performanceClient = new StubPerformanceClient(); describe("ThrottlingUtils", () => { afterAll(() => { @@ -40,7 +44,8 @@ describe("ThrottlingUtils", () => { const cache = new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, - new Logger({}) + new Logger({}), + performanceClient ); const removeItemStub = jest .spyOn(cache, "removeItem") @@ -51,12 +56,12 @@ describe("ThrottlingUtils", () => { jest.spyOn(Date, "now").mockReturnValue(1); try { - ThrottlingUtils.preProcess(cache, thumbprint); + ThrottlingUtils.preProcess(cache, thumbprint, RANDOM_TEST_GUID); } catch {} expect(removeItemStub).toHaveBeenCalledTimes(0); expect(() => - ThrottlingUtils.preProcess(cache, thumbprint) + ThrottlingUtils.preProcess(cache, thumbprint, RANDOM_TEST_GUID) ).toThrowError(ServerError); }); @@ -66,7 +71,8 @@ describe("ThrottlingUtils", () => { const cache = new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, - new Logger({}) + new Logger({}), + performanceClient ); const removeItemStub = jest .spyOn(cache, "removeItem") @@ -76,11 +82,11 @@ describe("ThrottlingUtils", () => { ); jest.spyOn(Date, "now").mockReturnValue(10); - ThrottlingUtils.preProcess(cache, thumbprint); + ThrottlingUtils.preProcess(cache, thumbprint, RANDOM_TEST_GUID); expect(removeItemStub).toHaveBeenCalledTimes(1); expect(() => - ThrottlingUtils.preProcess(cache, thumbprint) + ThrottlingUtils.preProcess(cache, thumbprint, RANDOM_TEST_GUID) ).not.toThrow(); }); @@ -89,18 +95,19 @@ describe("ThrottlingUtils", () => { const cache = new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, - new Logger({}) + new Logger({}), + performanceClient ); const removeItemStub = jest .spyOn(cache, "removeItem") .mockImplementation(); jest.spyOn(cache, "getThrottlingCache").mockReturnValue(null); - ThrottlingUtils.preProcess(cache, thumbprint); + ThrottlingUtils.preProcess(cache, thumbprint, RANDOM_TEST_GUID); expect(removeItemStub).toHaveBeenCalledTimes(0); expect(() => - ThrottlingUtils.preProcess(cache, thumbprint) + ThrottlingUtils.preProcess(cache, thumbprint, RANDOM_TEST_GUID) ).not.toThrow(); }); }); @@ -116,13 +123,19 @@ describe("ThrottlingUtils", () => { const cache = new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, - new Logger({}) + new Logger({}), + performanceClient ); const setItemStub = jest .spyOn(cache, "setThrottlingCache") .mockImplementation(); - ThrottlingUtils.postProcess(cache, thumbprint, res); + ThrottlingUtils.postProcess( + cache, + thumbprint, + res, + RANDOM_TEST_GUID + ); expect(setItemStub).toHaveBeenCalledTimes(1); }); @@ -136,13 +149,19 @@ describe("ThrottlingUtils", () => { const cache = new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, - new Logger({}) + new Logger({}), + performanceClient ); const setItemStub = jest .spyOn(cache, "setThrottlingCache") .mockImplementation(); - ThrottlingUtils.postProcess(cache, thumbprint, res); + ThrottlingUtils.postProcess( + cache, + thumbprint, + res, + RANDOM_TEST_GUID + ); expect(setItemStub).toHaveBeenCalledTimes(0); }); }); @@ -269,7 +288,8 @@ describe("ThrottlingUtils", () => { const cache = new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, - new Logger({}) + new Logger({}), + performanceClient ); const clientId = TEST_CONFIG.MSAL_CLIENT_ID; const removeItemStub = jest.spyOn(cache, "removeItem"); diff --git a/lib/msal-common/test/protocol/Authorize.spec.ts b/lib/msal-common/test/protocol/Authorize.spec.ts new file mode 100644 index 0000000000..1b80f3f993 --- /dev/null +++ b/lib/msal-common/test/protocol/Authorize.spec.ts @@ -0,0 +1,1743 @@ +import { Authority } from "../../src/authority/Authority.js"; +import { AuthOptions } from "../../src/config/ClientConfiguration.js"; +import { CommonAuthorizationUrlRequest } from "../../src/request/CommonAuthorizationUrlRequest.js"; +import { + AuthenticationScheme, + Constants, + HeaderNames, + PromptValue, + ResponseMode, +} from "../../src/utils/Constants.js"; +import { getDiscoveredAuthority } from "../client/ClientTestUtils.js"; +import { + DEFAULT_OPENID_CONFIG_RESPONSE, + RANDOM_TEST_GUID, + TEST_ACCOUNT_INFO, + TEST_CONFIG, + TEST_DATA_CLIENT_INFO, + TEST_STATE_VALUES, + TEST_URIS, +} from "../test_kit/StringConstants.js"; +import * as AADServerParamKeys from "../../src/constants/AADServerParamKeys.js"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; +import * as UrlUtils from "../../src/utils/UrlUtils.js"; +import { MockPerformanceClient } from "../telemetry/PerformanceClient.spec.js"; +import { TokenClaims } from "../../src/account/TokenClaims.js"; +import { Logger } from "../../src/logger/Logger.js"; +import { AuthError } from "../../src/error/AuthError.js"; +import { ServerError } from "../../src/error/ServerError.js"; +import { AuthorizeResponse } from "../../src/response/AuthorizeResponse.js"; +import { InteractionRequiredAuthError } from "../../src/error/InteractionRequiredAuthError.js"; +import { + ClientAuthError, + ClientAuthErrorCodes, +} from "../../src/error/ClientAuthError.js"; +import * as RequestParameterBuilder from "../../src/request/RequestParameterBuilder.js"; + +describe("Authorize Protocol Tests", () => { + let authOptions: AuthOptions; + let authority: Authority; + + beforeEach(async () => { + jest.spyOn( + Authority.prototype, + "getEndpointMetadataFromNetwork" + ).mockResolvedValue(DEFAULT_OPENID_CONFIG_RESPONSE.body); + authority = await getDiscoveredAuthority(); + authOptions = { + clientId: TEST_CONFIG.MSAL_CLIENT_ID, + authority: authority, + redirectUri: "https://localhost", + }; + }); + + afterEach(() => { + jest.restoreAllMocks(); + }); + describe("Authorization url creation", () => { + it("Creates an authorization url with default parameters", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.QUERY, + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + scopes: TEST_CONFIG.DEFAULT_SCOPES, + codeChallenge: TEST_CONFIG.TEST_CHALLENGE, + codeChallengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(Constants.DEFAULT_AUTHORITY)).toBe(true); + expect( + loginUrl.includes( + DEFAULT_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( + "{tenant}", + "common" + ) + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.SCOPE}=${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.REDIRECT_URI}=${encodeURIComponent( + TEST_URIS.TEST_REDIRECT_URI_LOCALHOST + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent( + ResponseMode.QUERY + )}` + ) + ).toBe(true); + }); + + it("Creates an authorization url passing in optional parameters", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FORM_POST, + codeChallenge: TEST_CONFIG.TEST_CHALLENGE, + codeChallengeMethod: TEST_CONFIG.CODE_CHALLENGE_METHOD, + state: TEST_CONFIG.STATE, + prompt: PromptValue.LOGIN, + loginHint: TEST_CONFIG.LOGIN_HINT, + domainHint: TEST_CONFIG.DOMAIN_HINT, + claims: TEST_CONFIG.CLAIMS, + nonce: TEST_CONFIG.NONCE, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(TEST_CONFIG.validAuthority)).toBe(true); + expect( + loginUrl.includes( + DEFAULT_OPENID_CONFIG_RESPONSE.body.authorization_endpoint.replace( + "{tenant}", + "common" + ) + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.SCOPE}=${TEST_CONFIG.DEFAULT_GRAPH_SCOPE}%20${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.REDIRECT_URI}=${encodeURIComponent( + TEST_URIS.TEST_REDIRECT_URI_LOCALHOST + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent( + ResponseMode.FORM_POST + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.STATE}=${encodeURIComponent( + TEST_CONFIG.STATE + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.PROMPT}=${PromptValue.LOGIN}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.NONCE}=${encodeURIComponent( + TEST_CONFIG.NONCE + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( + TEST_CONFIG.DOMAIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.CLAIMS}=${encodeURIComponent( + TEST_CONFIG.CLAIMS + )}` + ) + ).toBe(true); + }); + + it("Adds CCS entry if loginHint is provided", async () => { + const mockPerfClient = new MockPerformanceClient(); + let resEvents; + mockPerfClient.addPerformanceCallback((events) => { + resEvents = events; + }); + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + loginHint: TEST_CONFIG.LOGIN_HINT, + prompt: PromptValue.LOGIN, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const rootMeasurement = mockPerfClient.startMeasurement( + "root-measurement", + authCodeUrlRequest.correlationId + ); + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}), + mockPerfClient + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${HeaderNames.CCS_HEADER}=${encodeURIComponent( + `UPN:${TEST_CONFIG.LOGIN_HINT}` + )}` + ) + ).toBe(true); + rootMeasurement.end({ success: true }); + // @ts-ignore + const event = resEvents[0]; + expect(event.loginHintFromRequest).toBeTruthy(); + expect(event.loginHintFromUpn).toBeFalsy(); + expect(event.loginHintFromClaim).toBeFalsy(); + }); + + it("Adds CCS entry if account is provided", async () => { + const testAccount = TEST_ACCOUNT_INFO; + // @ts-ignore + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + | "login_hint" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + prompt: PromptValue.NONE, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + testTokenClaims.sid + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${HeaderNames.CCS_HEADER}=${encodeURIComponent( + `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` + )}` + ) + ).toBe(true); + }); + + it("prefers login_hint claim over sid/upn if both provided", async () => { + const mockPerfClient = new MockPerformanceClient(); + let resEvents; + mockPerfClient.addPerformanceCallback((events) => { + resEvents = events; + }); + const testAccount = TEST_ACCOUNT_INFO; + // @ts-ignore + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + login_hint: "opaque-login-hint-claim", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + prompt: PromptValue.NONE, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const rootMeasurement = mockPerfClient.startMeasurement( + "root-measurement", + authCodeUrlRequest.correlationId + ); + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}), + mockPerfClient + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + testTokenClaims.sid + )}` + ) + ).toBe(false); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + testTokenClaims.login_hint + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${HeaderNames.CCS_HEADER}=${encodeURIComponent( + `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` + )}` + ) + ).toBe(true); + + rootMeasurement.end({ success: true }); + // @ts-ignore + const event = resEvents[0]; + expect(event.loginHintFromUpn).toBeFalsy(); + expect(event.loginHintFromClaim).toBeTruthy(); + expect(event.loginHintFromRequest).toBeFalsy(); + expect(event.domainHintFromRequest).toBeFalsy(); + expect(event.sidFromClaim).toBeFalsy(); + expect(event.sidFromRequest).toBeFalsy(); + }); + + it("skips login_hint claim if domainHint param is set", async () => { + const mockPerfClient = new MockPerformanceClient(); + let resEvents; + mockPerfClient.addPerformanceCallback((events) => { + resEvents = events; + }); + const testAccount = TEST_ACCOUNT_INFO; + // @ts-ignore + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + login_hint: "opaque-login-hint-claim", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + domainHint: TEST_CONFIG.DOMAIN_HINT, + }; + const rootMeasurement = mockPerfClient.startMeasurement( + "root-measurement", + authCodeUrlRequest.correlationId + ); + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}), + mockPerfClient + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + testTokenClaims.sid + )}` + ) + ).toBe(false); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + testAccount.username + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( + TEST_CONFIG.DOMAIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${HeaderNames.CCS_HEADER}=${encodeURIComponent( + `Oid:${TEST_DATA_CLIENT_INFO.TEST_UID}@${TEST_DATA_CLIENT_INFO.TEST_UTID}` + )}` + ) + ).toBe(true); + + rootMeasurement.end({ success: true }); + // @ts-ignore + const event = resEvents[0]; + expect(event.loginHintFromUpn).toBeTruthy(); + expect(event.loginHintFromClaim).toBeFalsy(); + expect(event.loginHintFromRequest).toBeFalsy(); + expect(event.domainHintFromRequest).toBeTruthy(); + expect(event.sidFromClaim).toBeFalsy(); + expect(event.sidFromRequest).toBeFalsy(); + }); + + it("picks up both loginHint and domainHint params", async () => { + const testAccount = TEST_ACCOUNT_INFO; + // @ts-ignore + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + login_hint: "opaque-login-hint-claim", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + domainHint: TEST_CONFIG.DOMAIN_HINT, + loginHint: TEST_CONFIG.LOGIN_HINT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + testTokenClaims.sid + )}` + ) + ).toBe(false); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( + TEST_CONFIG.DOMAIN_HINT + )}` + ) + ).toBe(true); + }); + + it("Prefers sid over loginHint if both provided and prompt=None", async () => { + const mockPerfClient = new MockPerformanceClient(); + let resEvents; + mockPerfClient.addPerformanceCallback((events) => { + resEvents = events; + }); + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + loginHint: TEST_CONFIG.LOGIN_HINT, + prompt: PromptValue.NONE, + sid: TEST_CONFIG.SID, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const rootMeasurement = mockPerfClient.startMeasurement( + "root-measurement", + authCodeUrlRequest.correlationId + ); + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}), + mockPerfClient + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl).toEqual( + expect.not.arrayContaining([ + `${AADServerParamKeys.LOGIN_HINT}=`, + ]) + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + TEST_CONFIG.SID + )}` + ) + ).toBe(true); + + rootMeasurement.end({ success: true }); + // @ts-ignore + const event = resEvents[0]; + expect(event.loginHintFromRequest).toBeFalsy(); + expect(event.loginHintFromClaim).toBeFalsy(); + expect(event.loginHintFromUpn).toBeFalsy(); + expect(event.domainHintFromRequest).toBeFalsy(); + expect(event.sidFromRequest).toBeTruthy(); + expect(event.prompt).toEqual(PromptValue.NONE); + }); + + it("Prefers loginHint over sid if both provided and prompt!=None", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + loginHint: TEST_CONFIG.LOGIN_HINT, + prompt: PromptValue.LOGIN, + sid: TEST_CONFIG.SID, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Ignores sid if prompt!=None", async () => { + const mockPerfClient = new MockPerformanceClient(); + let resEvents; + mockPerfClient.addPerformanceCallback((events) => { + resEvents = events; + }); + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + prompt: PromptValue.LOGIN, + sid: TEST_CONFIG.SID, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const rootMeasurement = mockPerfClient.startMeasurement( + "root-measurement", + authCodeUrlRequest.correlationId + ); + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}), + mockPerfClient + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( + false + ); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + + rootMeasurement.end({ success: true }); + // @ts-ignore + const event = resEvents[0]; + expect(event.loginHintFromUpn).toBeFalsy(); + expect(event.loginHintFromClaim).toBeFalsy(); + expect(event.loginHintFromRequest).toBeFalsy(); + expect(event.domainHintFromRequest).toBeFalsy(); + expect(event.sidFromClaim).toBeFalsy(); + expect(event.sidFromRequest).toBeFalsy(); + expect(event.prompt).toEqual(PromptValue.LOGIN); + }); + + it("Prefers loginHint over Account if both provided and account does not have token claims", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + loginHint: TEST_CONFIG.LOGIN_HINT, + account: TEST_ACCOUNT_INFO, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_ACCOUNT_INFO.username + )}` + ) + ).toBe(false); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Uses sid from account if not provided in request and prompt=None, overrides login_hint", async () => { + const testAccount = TEST_ACCOUNT_INFO; + // @ts-ignore + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + loginHint: TEST_CONFIG.LOGIN_HINT, + prompt: PromptValue.NONE, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + testTokenClaims.sid + )}` + ) + ).toBe(true); + expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( + false + ); + }); + + it("Uses loginHint instead of sid from account prompt!=None", async () => { + const testAccount = TEST_ACCOUNT_INFO; + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + | "iat" + | "x5c_ca" + | "ts" + | "at" + | "u" + | "p" + | "m" + | "login_hint" + | "aud" + | "nbf" + | "roles" + | "amr" + | "idp" + | "auth_time" + | "tfp" + | "acr" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + sid: "testSid", + tenant_region_scope: "test_tenant_region_scope", + tenant_region_sub_scope: "test_tenant_region_sub_scope", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + loginHint: TEST_CONFIG.LOGIN_HINT, + prompt: PromptValue.LOGIN, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + }); + + it("Uses login_hint instead of username if sid is not present in token claims for account or request", async () => { + const testAccount = TEST_ACCOUNT_INFO; + const testTokenClaims: Required< + Omit< + TokenClaims, + | "home_oid" + | "upn" + | "cloud_instance_host_name" + | "cnf" + | "emails" + | "sid" + | "iat" + | "x5c_ca" + | "ts" + | "at" + | "u" + | "p" + | "m" + | "login_hint" + | "aud" + | "nbf" + | "roles" + | "amr" + | "idp" + | "auth_time" + | "tfp" + | "acr" + > + > = { + ver: "2.0", + iss: `${TEST_URIS.DEFAULT_INSTANCE}9188040d-6c67-4c5b-b112-36a304b66dad/v2.0`, + sub: "AAAAAAAAAAAAAAAAAAAAAIkzqFVrSaSaFHy782bbtaQ", + exp: 1536361411, + name: "Abe Lincoln", + preferred_username: "AbeLi@microsoft.com", + oid: "00000000-0000-0000-66f3-3332eca7ea81", + tid: "3338040d-6c67-4c5b-b112-36a304b66dad", + nonce: "123523", + tenant_region_scope: "test_tenant_region_scope", + tenant_region_sub_scope: "test_tenant_region_sub_scope", + }; + + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + account: { + ...testAccount, + idTokenClaims: testTokenClaims, + }, + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + loginHint: TEST_CONFIG.LOGIN_HINT, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Sets login_hint to Account.username if login_hint and sid are not provided", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + account: TEST_ACCOUNT_INFO, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_ACCOUNT_INFO.username + )}` + ) + ).toBe(true); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Ignores Account if prompt is select_account", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + account: TEST_ACCOUNT_INFO, + prompt: "select_account", + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( + false + ); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Ignores loginHint if prompt is select_account", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + loginHint: "testaccount@microsoft.com", + prompt: "select_account", + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( + false + ); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Ignores sid if prompt is select_account", async () => { + const authCodeUrlRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIRECT_URI_LOCALHOST, + scopes: [ + ...TEST_CONFIG.DEFAULT_GRAPH_SCOPE, + ...TEST_CONFIG.DEFAULT_SCOPES, + ], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + sid: "testsid", + prompt: "select_account", + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + authCodeUrlRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect(loginUrl.includes(`${AADServerParamKeys.LOGIN_HINT}=`)).toBe( + false + ); + expect(loginUrl.includes(`${AADServerParamKeys.SID}=`)).toBe(false); + }); + + it("Creates a login URL with scopes from given token request", async () => { + const testScope1 = "testscope1"; + const testScope2 = "testscope2"; + const loginRequest: CommonAuthorizationUrlRequest = { + redirectUri: TEST_URIS.TEST_REDIR_URI, + scopes: [testScope1, testScope2], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + codeChallenge: TEST_CONFIG.TEST_CHALLENGE, + codeChallengeMethod: Constants.S256_CODE_CHALLENGE_METHOD, + correlationId: RANDOM_TEST_GUID, + authenticationScheme: AuthenticationScheme.BEARER, + authority: TEST_CONFIG.validAuthority, + responseMode: ResponseMode.FRAGMENT, + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + loginRequest, + new Logger({}) + ); + const loginUrl = AuthorizeProtocol.getAuthorizeUrl( + authority, + params + ); + expect( + loginUrl.includes( + `${AADServerParamKeys.SCOPE}=${encodeURIComponent( + `${testScope1} ${testScope2}` + )}` + ) + ).toBe(true); + }); + }); + + describe("createAuthCodeUrlQueryString tests", () => { + it("pick up default client_id", async () => { + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString).toContain( + `client_id=${TEST_CONFIG.MSAL_CLIENT_ID}` + ); + }); + + it("pick up extra query client_id param", async () => { + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + extraQueryParameters: { + client_id: "child_client_id", + }, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString).toContain(`client_id=child_client_id`); + }); + + it("pick up instance_aware config param when set to true", async () => { + authOptions.instanceAware = true; + + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString).toContain(`instance_aware=true`); + }); + + it("do not pick up instance_aware config param when set to false", async () => { + authOptions.instanceAware = false; + + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString.includes("instance_aware")).toBeFalsy(); + }); + + it("pick up instance_aware EQ param when config is set to false", async () => { + authOptions.instanceAware = false; + + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + extraQueryParameters: { + instance_aware: "true", + }, + }; + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + RequestParameterBuilder.addExtraQueryParameters( + params, + request.extraQueryParameters! + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString).toContain(`instance_aware=true`); + }); + + it("pick up instance_aware EQ param when config is set to true", async () => { + authOptions.instanceAware = true; + + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + prompt: PromptValue.LOGIN, + redirectUri: "localhost", + extraQueryParameters: { + instance_aware: "false", + }, + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + RequestParameterBuilder.addExtraQueryParameters( + params, + request.extraQueryParameters! + ); + const queryString = UrlUtils.mapToQueryString(params); + + expect(queryString).toContain(`instance_aware=false`); + }); + + it("pick up broker params", async () => { + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + redirectUri: "localhost", + embeddedClientId: "child_client_id_1", + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + const queryString = UrlUtils.mapToQueryString(params); + expect(queryString).toContain(`client_id=child_client_id_1`); + expect(queryString).toContain( + `brk_client_id=${TEST_CONFIG.MSAL_CLIENT_ID}` + ); + expect(queryString).toContain( + `brk_redirect_uri=${encodeURIComponent("https://localhost")}` + ); + }); + + it("broker params take precedence over extra query params", async () => { + const request: CommonAuthorizationUrlRequest = { + scopes: ["User.Read"], + nonce: RANDOM_TEST_GUID, + state: TEST_CONFIG.STATE, + authority: TEST_CONFIG.validAuthority, + correlationId: RANDOM_TEST_GUID, + responseMode: ResponseMode.FRAGMENT, + redirectUri: "localhost", + embeddedClientId: "child_client_id_1", + extraQueryParameters: { + client_id: "child_client_id_2", + brk_client_id: "broker_client_id_2", + brk_redirect_uri: "broker_redirect_uri_2", + }, + }; + + const params = + AuthorizeProtocol.getStandardAuthorizeRequestParameters( + authOptions, + request, + new Logger({}) + ); + RequestParameterBuilder.addExtraQueryParameters( + params, + request.extraQueryParameters! + ); + const queryString = UrlUtils.mapToQueryString(params); + expect(queryString).toContain(`client_id=child_client_id_1`); + expect(queryString).toContain( + `brk_client_id=${TEST_CONFIG.MSAL_CLIENT_ID}` + ); + expect(queryString).toContain( + `brk_redirect_uri=${encodeURIComponent("https://localhost")}` + ); + }); + }); + + describe("getAuthorizationCodePayload", () => { + it("returns valid server code response", () => { + const authCodePayload = + AuthorizeProtocol.getAuthorizationCodePayload( + { + code: "thisIsATestCode", + state: TEST_STATE_VALUES.ENCODED_LIB_STATE, + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + }, + TEST_STATE_VALUES.ENCODED_LIB_STATE + ); + expect(authCodePayload.code).toBe("thisIsATestCode"); + expect(authCodePayload.state).toBe( + TEST_STATE_VALUES.ENCODED_LIB_STATE + ); + }); + + it("throws server error when error is in hash", () => { + let error: AuthError | null = null; + try { + AuthorizeProtocol.getAuthorizationCodePayload( + { + error: "error_code", + error_description: "msal error description", + state: TEST_STATE_VALUES.ENCODED_LIB_STATE, + }, + TEST_STATE_VALUES.ENCODED_LIB_STATE + ); + } catch (e) { + error = e as AuthError; + } + expect(error).toBeInstanceOf(ServerError); + expect(error?.errorCode).toEqual("error_code"); + expect(error?.errorMessage).toEqual("msal error description"); + }); + }); + + describe("validateAuthorizationResponse", () => { + it("throws state mismatch error", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + "differentState" + ); + } catch (e) { + expect(e).toBeInstanceOf(ClientAuthError); + // @ts-ignore + expect(e.errorCode).toBe(ClientAuthErrorCodes.stateMismatch); + done(); + } + }); + + it("Does not throw state mismatch error when states match", () => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + }; + + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + }); + + it("Does not throw state mismatch error when Uri encoded characters have different casing", () => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + }; + + const testAltState = + "eyJpZCI6IjExNTUzYTliLTcxMTYtNDhiMS05ZDQ4LWY2ZDRhOGZmODM3MSIsInRzIjoxNTkyODQ2NDgyfQ%3d%3d"; + + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + testAltState + ); + }); + + it("throws interactionRequiredError", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "interaction_required", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(InteractionRequiredAuthError); + done(); + } + }); + + it("thows ServerError if error in response", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "test_error", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + done(); + } + }); + + it("throws ServerError if error_description in response", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error_description: "test_error", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + done(); + } + }); + + it("throws ServerError if suberror in response", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + suberror: "test_error", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + done(); + } + }); + + it("throws invalid state error", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + "dummy-state-%20%%%30%%%%%40" + ); + } catch (e) { + expect(e).toBeInstanceOf(ClientAuthError); + const err = e as ClientAuthError; + expect(err.errorCode).toBe(ClientAuthErrorCodes.invalidState); + done(); + } + }); + + it("throws ServerError and parser error no", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "test_error", + error_uri: + "https://login.microsoftonline.com/error_code=500011", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + const serverError = e as ServerError; + expect(serverError.errorNo).toEqual("500011"); + done(); + } + }); + + it("throws InteractionRequiredAuthError and parser error no", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "interaction_required", + error_uri: + "https://login.microsoftonline.com/error_code=500011", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(InteractionRequiredAuthError); + const serverError = e as InteractionRequiredAuthError; + expect(serverError.errorNo).toEqual("500011"); + done(); + } + }); + + it("throws ServerError and skips invalid error uri", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "test_error", + error_uri: "https://login.microsoftonline.com/500011", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + const serverError = e as ServerError; + expect(serverError.errorNo).toBeUndefined(); + done(); + } + }); + + it("throws ServerError and skips undefined error uri", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "test_error", + error_uri: undefined, + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + const serverError = e as ServerError; + expect(serverError.errorNo).toBeUndefined(); + done(); + } + }); + + it("throws ServerError and skips empty error uri", (done) => { + const testServerCodeResponse: AuthorizeResponse = { + code: "testCode", + client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, + state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, + error: "test_error", + error_uri: "", + }; + + try { + AuthorizeProtocol.validateAuthorizationResponse( + testServerCodeResponse, + TEST_STATE_VALUES.URI_ENCODED_LIB_STATE + ); + } catch (e) { + expect(e).toBeInstanceOf(ServerError); + const serverError = e as ServerError; + expect(serverError.errorNo).toBeUndefined(); + done(); + } + }); + }); +}); diff --git a/lib/msal-common/test/request/RequestParameterBuilder.spec.ts b/lib/msal-common/test/request/RequestParameterBuilder.spec.ts index 9a51de0677..244a4b3a02 100644 --- a/lib/msal-common/test/request/RequestParameterBuilder.spec.ts +++ b/lib/msal-common/test/request/RequestParameterBuilder.spec.ts @@ -5,6 +5,7 @@ import { GrantType, AuthenticationScheme, HeaderNames, + OAuthResponseType, } from "../../src/utils/Constants.js"; import * as AADServerParamKeys from "../../src/constants/AADServerParamKeys.js"; import { @@ -35,7 +36,10 @@ describe("RequestParameterBuilder unit tests", () => { it("Build query string from RequestParameterBuilder object", () => { const parameters = new Map(); - RequestParameterBuilder.addResponseTypeCode(parameters); + RequestParameterBuilder.addResponseType( + parameters, + OAuthResponseType.CODE + ); RequestParameterBuilder.addResponseMode( parameters, ResponseMode.FORM_POST @@ -101,7 +105,7 @@ describe("RequestParameterBuilder unit tests", () => { const requestQueryString = UrlUtils.mapToQueryString(parameters); expect( requestQueryString.includes( - `${AADServerParamKeys.RESPONSE_TYPE}=${Constants.CODE_RESPONSE_TYPE}` + `${AADServerParamKeys.RESPONSE_TYPE}=${OAuthResponseType.CODE}` ) ).toBe(true); expect( @@ -226,6 +230,231 @@ describe("RequestParameterBuilder unit tests", () => { ).toBe(true); }); + it("Doesn't encode extra params by default", () => { + const parameters = new Map(); + RequestParameterBuilder.addResponseType( + parameters, + OAuthResponseType.CODE + ); + RequestParameterBuilder.addResponseMode( + parameters, + ResponseMode.FORM_POST + ); + RequestParameterBuilder.addScopes( + parameters, + TEST_CONFIG.DEFAULT_SCOPES + ); + RequestParameterBuilder.addClientId( + parameters, + TEST_CONFIG.MSAL_CLIENT_ID + ); + RequestParameterBuilder.addRedirectUri( + parameters, + TEST_URIS.TEST_REDIRECT_URI_LOCALHOST + ); + RequestParameterBuilder.addDomainHint( + parameters, + TEST_CONFIG.DOMAIN_HINT + ); + RequestParameterBuilder.addLoginHint( + parameters, + TEST_CONFIG.LOGIN_HINT + ); + RequestParameterBuilder.addClaims(parameters, TEST_CONFIG.CLAIMS, []); + RequestParameterBuilder.addCorrelationId( + parameters, + TEST_CONFIG.CORRELATION_ID + ); + RequestParameterBuilder.addPrompt( + parameters, + PromptValue.SELECT_ACCOUNT + ); + RequestParameterBuilder.addState(parameters, TEST_CONFIG.STATE); + RequestParameterBuilder.addNonce(parameters, TEST_CONFIG.NONCE); + RequestParameterBuilder.addCodeChallengeParams( + parameters, + TEST_CONFIG.TEST_CHALLENGE, + TEST_CONFIG.CODE_CHALLENGE_METHOD + ); + RequestParameterBuilder.addAuthorizationCode( + parameters, + TEST_TOKENS.AUTHORIZATION_CODE + ); + RequestParameterBuilder.addDeviceCode( + parameters, + DEVICE_CODE_RESPONSE.deviceCode + ); + RequestParameterBuilder.addCodeVerifier( + parameters, + TEST_CONFIG.TEST_VERIFIER + ); + RequestParameterBuilder.addGrantType( + parameters, + GrantType.DEVICE_CODE_GRANT + ); + RequestParameterBuilder.addSid(parameters, TEST_CONFIG.SID); + RequestParameterBuilder.addLogoutHint( + parameters, + TEST_CONFIG.LOGIN_HINT + ); + RequestParameterBuilder.addExtraQueryParameters(parameters, { + extra_params: "param1,param2", + }); + + const requestQueryString = UrlUtils.mapToQueryString( + parameters, + false, + { + extra_params: "param1,param2", + } + ); + expect( + requestQueryString.includes( + `${AADServerParamKeys.RESPONSE_TYPE}=${OAuthResponseType.CODE}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.RESPONSE_MODE}=${encodeURIComponent( + ResponseMode.FORM_POST + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.SCOPE}=${Constants.OPENID_SCOPE}%20${Constants.PROFILE_SCOPE}%20${Constants.OFFLINE_ACCESS_SCOPE}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.CLIENT_ID}=${TEST_CONFIG.MSAL_CLIENT_ID}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.REDIRECT_URI}=${encodeURIComponent( + TEST_URIS.TEST_REDIRECT_URI_LOCALHOST + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.DOMAIN_HINT}=${encodeURIComponent( + TEST_CONFIG.DOMAIN_HINT + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.LOGIN_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.CLAIMS}=${encodeURIComponent( + TEST_CONFIG.CLAIMS + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.CLIENT_REQUEST_ID}=${encodeURIComponent( + TEST_CONFIG.CORRELATION_ID + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.PROMPT}=${PromptValue.SELECT_ACCOUNT}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.STATE}=${encodeURIComponent( + TEST_CONFIG.STATE + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.NONCE}=${encodeURIComponent( + TEST_CONFIG.NONCE + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.CODE_CHALLENGE}=${encodeURIComponent( + TEST_CONFIG.TEST_CHALLENGE + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${ + AADServerParamKeys.CODE_CHALLENGE_METHOD + }=${encodeURIComponent(TEST_CONFIG.CODE_CHALLENGE_METHOD)}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.CODE}=${encodeURIComponent( + TEST_TOKENS.AUTHORIZATION_CODE + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.DEVICE_CODE}=${encodeURIComponent( + DEVICE_CODE_RESPONSE.deviceCode + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.CODE_VERIFIER}=${encodeURIComponent( + TEST_CONFIG.TEST_VERIFIER + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.SID}=${encodeURIComponent( + TEST_CONFIG.SID + )}` + ) + ).toBe(true); + expect( + requestQueryString.includes( + `${AADServerParamKeys.LOGOUT_HINT}=${encodeURIComponent( + TEST_CONFIG.LOGIN_HINT + )}` + ) + ).toBe(true); + expect(requestQueryString.includes(`extra_params=param1,param2`)).toBe( + true + ); + }); + + it("Encodes extra params if encodeParams is true and extra params are passed in", () => { + const parameters = new Map(); + RequestParameterBuilder.addExtraQueryParameters(parameters, { + extra_params: "param1,param2", + }); + + const requestQueryString = UrlUtils.mapToQueryString(parameters, true, { + extra_params: "param1,param2", + }); + + expect( + requestQueryString.includes( + `extra_params=${encodeURIComponent("param1,param2")}` + ) + ).toBe(true); + }); + it("Adds token type and req_cnf correctly for proof-of-possession tokens", () => { const parameters = new Map(); RequestParameterBuilder.addPopToken( @@ -366,11 +595,16 @@ describe("RequestParameterBuilder unit tests", () => { it("addResponseTypeForIdToken does add response_type correctly", () => { const parameters = new Map(); - RequestParameterBuilder.addResponseTypeForTokenAndIdToken(parameters); + RequestParameterBuilder.addResponseType( + parameters, + OAuthResponseType.IDTOKEN_TOKEN + ); const requestQueryString = UrlUtils.mapToQueryString(parameters); expect( requestQueryString.includes( - `${AADServerParamKeys.RESPONSE_TYPE}=${Constants.TOKEN_RESPONSE_TYPE}%20${Constants.ID_TOKEN_RESPONSE_TYPE}` + `${AADServerParamKeys.RESPONSE_TYPE}=${encodeURIComponent( + OAuthResponseType.IDTOKEN_TOKEN + )}` ) ).toBe(true); }); diff --git a/lib/msal-common/test/request/RequestValidator.spec.ts b/lib/msal-common/test/request/RequestValidator.spec.ts deleted file mode 100644 index d96239bda5..0000000000 --- a/lib/msal-common/test/request/RequestValidator.spec.ts +++ /dev/null @@ -1,94 +0,0 @@ -import { PromptValue } from "../../src/utils/Constants"; -import { RequestValidator } from "../../src/request/RequestValidator"; -import { TEST_CONFIG } from "../test_kit/StringConstants"; -import { - ClientConfigurationErrorCodes, - createClientConfigurationError, -} from "../../src/error/ClientConfigurationError"; - -describe("RequestValidator unit tests", () => { - describe("ValidateRedirectUri tests", () => { - it("Throws UrlEmptyError if redirect uri is empty", () => { - expect(function () { - RequestValidator.validateRedirectUri(""); - }).toThrowError( - createClientConfigurationError( - ClientConfigurationErrorCodes.redirectUriEmpty - ) - ); - }); - }); - - describe("ValidatePrompt tests", () => { - it("PromptValue login", () => { - RequestValidator.validatePrompt(PromptValue.LOGIN); - }); - it("PromptValue select_account", () => { - RequestValidator.validatePrompt(PromptValue.SELECT_ACCOUNT); - }); - it("PromptValue consent", () => { - RequestValidator.validatePrompt(PromptValue.CONSENT); - }); - it("PromptValue none", () => { - RequestValidator.validatePrompt(PromptValue.NONE); - }); - it("PromptValue create", () => { - RequestValidator.validatePrompt(PromptValue.CREATE); - }); - it("Throws InvalidPromptError if invalid prompt value passed in", () => { - expect(function () { - RequestValidator.validatePrompt(""); - }).toThrowError( - createClientConfigurationError( - ClientConfigurationErrorCodes.invalidPromptValue - ) - ); - }); - }); - - describe("ValidateClaims tests", () => { - it("Valid Claims", () => { - RequestValidator.validateClaims( - '{"access_token":{"example_claim":{"values": ["example_value"]}}}' - ); - }); - - it("Throws InvalidClaimsError if invalid claims value passed in", () => { - expect(function () { - RequestValidator.validateClaims("invalid_claims_value"); - }).toThrowError( - createClientConfigurationError( - ClientConfigurationErrorCodes.invalidClaims - ) - ); - }); - }); - - describe("ValidateCodeChallengeParams tests", () => { - it("Throws InvalidCodeChallengeParamsError if no code challenge method present", () => { - expect(function () { - RequestValidator.validateCodeChallengeParams( - "", - TEST_CONFIG.CODE_CHALLENGE_METHOD - ); - }).toThrowError( - createClientConfigurationError( - ClientConfigurationErrorCodes.pkceParamsMissing - ) - ); - }); - - it("Throws InvalidCodeChallengeMethodError if no code challenge method present", () => { - expect(function () { - RequestValidator.validateCodeChallengeParams( - TEST_CONFIG.TEST_CHALLENGE, - "255" - ); - }).toThrowError( - createClientConfigurationError( - ClientConfigurationErrorCodes.invalidCodeChallengeMethod - ) - ); - }); - }); -}); diff --git a/lib/msal-common/test/response/ResponseHandler.spec.ts b/lib/msal-common/test/response/ResponseHandler.spec.ts index f0a069816d..ccdb7e51e8 100644 --- a/lib/msal-common/test/response/ResponseHandler.spec.ts +++ b/lib/msal-common/test/response/ResponseHandler.spec.ts @@ -4,12 +4,9 @@ import { AUTHENTICATION_RESULT, ID_TOKEN_CLAIMS, POP_AUTHENTICATION_RESULT, - RANDOM_TEST_GUID, TEST_CONFIG, - TEST_CRYPTO_VALUES, TEST_DATA_CLIENT_INFO, TEST_POP_VALUES, - TEST_STATE_VALUES, TEST_TOKEN_LIFETIMES, TEST_TOKENS, TEST_URIS, @@ -20,8 +17,7 @@ import { NetworkRequestOptions, } from "../../src/network/INetworkModule.js"; import { ICrypto } from "../../src/crypto/ICrypto.js"; -import { ServerAuthorizationCodeResponse } from "../../src/response/ServerAuthorizationCodeResponse.js"; -import { MockStorageClass } from "../client/ClientTestUtils.js"; +import { mockCrypto, MockStorageClass } from "../client/ClientTestUtils.js"; import { TokenClaims } from "../../src/account/TokenClaims.js"; import { AccountInfo } from "../../src/account/AccountInfo.js"; import { AuthenticationResult } from "../../src/response/AuthenticationResult.js"; @@ -46,8 +42,9 @@ import { CacheErrorMessages, } from "../../src/error/CacheError.js"; import { CacheManager } from "../../src/cache/CacheManager.js"; -import { cacheQuotaExceededErrorCode } from "../../src/error/CacheErrorCodes.js"; +import { cacheQuotaExceeded } from "../../src/error/CacheErrorCodes.js"; import { TestTimeUtils } from "msal-test-utils"; +import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient.js"; const networkInterface: INetworkModule = { sendGetRequestAsync(url: string, options?: NetworkRequestOptions): T { @@ -57,68 +54,7 @@ const networkInterface: INetworkModule = { return {} as T; }, }; -const signedJwt = - "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyLCJjbmYiOnsia2lkIjoiTnpiTHNYaDh1RENjZC02TU53WEY0V183bm9XWEZaQWZIa3hac1JHQzlYcyJ9fQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c"; -const cryptoInterface: ICrypto = { - createNewGuid(): string { - return RANDOM_TEST_GUID; - }, - base64Decode(input: string): string { - switch (input) { - case TEST_POP_VALUES.ENCODED_REQ_CNF: - return TEST_POP_VALUES.DECODED_REQ_CNF; - case TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO: - return TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO; - case TEST_POP_VALUES.SAMPLE_POP_AT_PAYLOAD_ENCODED: - return TEST_POP_VALUES.SAMPLE_POP_AT_PAYLOAD_DECODED; - default: - return input; - } - }, - base64Encode(input: string): string { - switch (input) { - case TEST_POP_VALUES.DECODED_REQ_CNF: - return TEST_POP_VALUES.ENCODED_REQ_CNF; - case TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO: - return TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO; - case TEST_POP_VALUES.SAMPLE_POP_AT_PAYLOAD_DECODED: - return TEST_POP_VALUES.SAMPLE_POP_AT_PAYLOAD_ENCODED; - default: - return input; - } - }, - base64UrlEncode(input: string): string { - switch (input) { - case '{"kid": "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc"}': - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - encodeKid(input: string): string { - switch (input) { - case "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc": - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - async getPublicKeyThumbprint(): Promise { - return TEST_POP_VALUES.KID; - }, - async signJwt(): Promise { - return signedJwt; - }, - async removeTokenBindingKey(): Promise { - return Promise.resolve(true); - }, - async clearKeystore(): Promise { - return Promise.resolve(true); - }, - async hashString(): Promise { - return Promise.resolve(TEST_CRYPTO_VALUES.TEST_SHA256_HASH); - }, -}; +const cryptoInterface: ICrypto = mockCrypto; const testServerTokenResponse = { headers: null, @@ -176,7 +112,8 @@ const logger = new Logger(loggerOptions); const testCacheManager = new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, cryptoInterface, - logger + logger, + new StubPerformanceClient() ); const testAuthority = new Authority( @@ -644,7 +581,7 @@ describe("ResponseHandler.ts", () => { ); expect(result.tokenType).toBe(AuthenticationScheme.POP); - expect(result.accessToken).toBe(signedJwt); + expect(result.accessToken).toBe(TEST_TOKENS.POP_TOKEN); }); it("Does not sign access token when PoP kid is set and PoP scheme enabled", async () => { @@ -725,396 +662,6 @@ describe("ResponseHandler.ts", () => { }); }); - describe("validateServerAuthorizationCodeResponse", () => { - it("throws state mismatch error", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - "differentState" - ); - } catch (e) { - expect(e).toBeInstanceOf(ClientAuthError); - // @ts-ignore - expect(e.errorCode).toBe(ClientAuthErrorCodes.stateMismatch); - done(); - } - }); - - it("Does not throw state mismatch error when states match", () => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - }); - - it("Does not throw state mismatch error when Uri encoded characters have different casing", () => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - }; - - const testAltState = - "eyJpZCI6IjExNTUzYTliLTcxMTYtNDhiMS05ZDQ4LWY2ZDRhOGZmODM3MSIsInRzIjoxNTkyODQ2NDgyfQ%3d%3d"; - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - testAltState - ); - }); - - it("throws interactionRequiredError", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "interaction_required", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(InteractionRequiredAuthError); - done(); - } - }); - - it("thows ServerError if error in response", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "test_error", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - done(); - } - }); - - it("throws ServerError if error_description in response", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error_description: "test_error", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - done(); - } - }); - - it("throws ServerError if suberror in response", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - suberror: "test_error", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - done(); - } - }); - - it("does not call buildClientInfo if clientInfo not in response", () => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - }; - // Can't spy on buildClientInfo, spy on one of its function calls instead - const buildClientInfoSpy = jest.spyOn( - cryptoInterface, - "base64Decode" - ); - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - expect(buildClientInfoSpy).not.toHaveBeenCalled(); - }); - - it("throws invalid state error", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - "dummy-state-%20%%%30%%%%%40" - ); - } catch (e) { - expect(e).toBeInstanceOf(ClientAuthError); - const err = e as ClientAuthError; - expect(err.errorCode).toBe(ClientAuthErrorCodes.invalidState); - done(); - } - }); - - it("throws ServerError and parser error no", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "test_error", - error_uri: - "https://login.microsoftonline.com/error_code=500011", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - const serverError = e as ServerError; - expect(serverError.errorNo).toEqual("500011"); - done(); - } - }); - - it("throws InteractionRequiredAuthError and parser error no", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "interaction_required", - error_uri: - "https://login.microsoftonline.com/error_code=500011", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(InteractionRequiredAuthError); - const serverError = e as InteractionRequiredAuthError; - expect(serverError.errorNo).toEqual("500011"); - done(); - } - }); - - it("throws ServerError and skips invalid error uri", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "test_error", - error_uri: "https://login.microsoftonline.com/500011", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - const serverError = e as ServerError; - expect(serverError.errorNo).toBeUndefined(); - done(); - } - }); - - it("throws ServerError and skips undefined error uri", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "test_error", - error_uri: undefined, - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - const serverError = e as ServerError; - expect(serverError.errorNo).toBeUndefined(); - done(); - } - }); - - it("throws ServerError and skips empty error uri", (done) => { - const testServerCodeResponse: ServerAuthorizationCodeResponse = { - code: "testCode", - client_info: TEST_DATA_CLIENT_INFO.TEST_RAW_CLIENT_INFO, - state: TEST_STATE_VALUES.URI_ENCODED_LIB_STATE, - error: "test_error", - error_uri: "", - }; - - const responseHandler = new ResponseHandler( - "this-is-a-client-id", - testCacheManager, - cryptoInterface, - logger, - null, - null - ); - try { - responseHandler.validateServerAuthorizationCodeResponse( - testServerCodeResponse, - TEST_STATE_VALUES.URI_ENCODED_LIB_STATE - ); - } catch (e) { - expect(e).toBeInstanceOf(ServerError); - const serverError = e as ServerError; - expect(serverError.errorNo).toBeUndefined(); - done(); - } - }); - }); - describe("validateTokenResponse", () => { it("captures server error no", (done) => { const testTokenResponse: ServerAuthorizationTokenResponse = { @@ -1285,7 +832,7 @@ describe("ResponseHandler.ts", () => { const cacheError: CacheError = e as CacheError; expect(cacheError.errorCode).toEqual("cache_quota_exceeded"); expect(cacheError.errorMessage).toEqual( - CacheErrorMessages[cacheQuotaExceededErrorCode] + CacheErrorMessages[cacheQuotaExceeded] ); } }); @@ -1331,7 +878,7 @@ describe("ResponseHandler.ts", () => { const cacheError: CacheError = e as CacheError; expect(cacheError.errorCode).toEqual("cache_quota_exceeded"); expect(cacheError.errorMessage).toEqual( - CacheErrorMessages[cacheQuotaExceededErrorCode] + CacheErrorMessages[cacheQuotaExceeded] ); } }); @@ -1419,10 +966,10 @@ describe("ResponseHandler.ts", () => { expect(e).toBeInstanceOf(CacheError); const cacheError: CacheError = e as CacheError; expect(cacheError.errorCode).toEqual( - CacheErrorCodes.cacheUnknownErrorCode + CacheErrorCodes.cacheErrorUnknown ); expect(cacheError.errorMessage).toEqual( - CacheErrorMessages[CacheErrorCodes.cacheUnknownErrorCode] + CacheErrorMessages[CacheErrorCodes.cacheErrorUnknown] ); } }); diff --git a/lib/msal-common/test/telemetry/PerformanceClient.spec.ts b/lib/msal-common/test/telemetry/PerformanceClient.spec.ts index 6ba199fbdc..c859174c3c 100644 --- a/lib/msal-common/test/telemetry/PerformanceClient.spec.ts +++ b/lib/msal-common/test/telemetry/PerformanceClient.spec.ts @@ -17,8 +17,8 @@ import crypto from "crypto"; import { compactStack, compactStackLine, -} from "../../src/telemetry/performance/PerformanceClient"; -import * as PerformanceClient from "../../src/telemetry/performance/PerformanceClient"; +} from "../../src/telemetry/performance/PerformanceClient.js"; +import * as PerformanceClient from "../../src/telemetry/performance/PerformanceClient.js"; import { PerformanceEventAbbreviations } from "../../src/telemetry/performance/PerformanceEvent"; import { AuthError } from "../../src/error/AuthError.js"; @@ -944,7 +944,7 @@ describe("PerformanceClient.spec.ts", () => { ); const thirdChildEventChild = mockPerfClient.startMeasurement( - PerformanceEvents.AuthClientCreateQueryString, + PerformanceEvents.GetAuthCodeUrl, correlationId ); thirdChildEventChild.end({ success: true }); diff --git a/lib/msal-common/test/telemetry/ServerTelemetryManager.spec.ts b/lib/msal-common/test/telemetry/ServerTelemetryManager.spec.ts index 78b5084a36..80e259dec7 100644 --- a/lib/msal-common/test/telemetry/ServerTelemetryManager.spec.ts +++ b/lib/msal-common/test/telemetry/ServerTelemetryManager.spec.ts @@ -11,11 +11,13 @@ import { AuthError } from "../../src/error/AuthError.js"; import { ServerTelemetryEntity } from "../../src/cache/entities/ServerTelemetryEntity.js"; import { CacheOutcome } from "../../src/utils/Constants.js"; import { Logger } from "../../src/logger/Logger.js"; +import { StubPerformanceClient } from "../../src/telemetry/performance/StubPerformanceClient.js"; const testCacheManager = new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, - new Logger({}) + new Logger({}), + new StubPerformanceClient() ); const testApiCode = 9999999; const testError = "interaction_required"; diff --git a/lib/msal-common/test/utils/ProtocolUtils.spec.ts b/lib/msal-common/test/utils/ProtocolUtils.spec.ts index ff6c8c8ca3..14021b019c 100644 --- a/lib/msal-common/test/utils/ProtocolUtils.spec.ts +++ b/lib/msal-common/test/utils/ProtocolUtils.spec.ts @@ -1,15 +1,12 @@ import { ProtocolUtils } from "../../src/utils/ProtocolUtils.js"; -import { - RANDOM_TEST_GUID, - TEST_CRYPTO_VALUES, - TEST_POP_VALUES, -} from "../test_kit/StringConstants.js"; +import { RANDOM_TEST_GUID } from "../test_kit/StringConstants.js"; import { ICrypto } from "../../src/crypto/ICrypto.js"; import { Constants } from "../../src/utils/Constants.js"; import { ClientAuthError, ClientAuthErrorMessage, } from "../../src/error/ClientAuthError.js"; +import { mockCrypto } from "../client/ClientTestUtils.js"; describe("ProtocolUtils.ts Class Unit Tests", () => { const userState = "userState"; @@ -19,62 +16,7 @@ describe("ProtocolUtils.ts Class Unit Tests", () => { let cryptoInterface: ICrypto; beforeEach(() => { - cryptoInterface = { - createNewGuid(): string { - return RANDOM_TEST_GUID; - }, - base64Decode(input: string): string { - switch (input) { - case TEST_POP_VALUES.ENCODED_REQ_CNF: - return TEST_POP_VALUES.DECODED_REQ_CNF; - case encodedLibState: - return decodedLibState; - default: - return input; - } - }, - base64Encode(input: string): string { - switch (input) { - case TEST_POP_VALUES.DECODED_REQ_CNF: - return TEST_POP_VALUES.ENCODED_REQ_CNF; - case `${decodedLibState}`: - return encodedLibState; - default: - return input; - } - }, - base64UrlEncode(input: string): string { - switch (input) { - case '{"kid": "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc"}': - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - encodeKid(input: string): string { - switch (input) { - case "XnsuAvttTPp0nn1K_YMLePLDbp7syCKhNHt7HjYHJYc": - return "eyJraWQiOiAiWG5zdUF2dHRUUHAwbm4xS19ZTUxlUExEYnA3c3lDS2hOSHQ3SGpZSEpZYyJ9"; - default: - return input; - } - }, - async getPublicKeyThumbprint(): Promise { - return TEST_POP_VALUES.KID; - }, - async signJwt(): Promise { - return ""; - }, - async removeTokenBindingKey(): Promise { - return Promise.resolve(true); - }, - async clearKeystore(): Promise { - return Promise.resolve(true); - }, - async hashString(): Promise { - return Promise.resolve(TEST_CRYPTO_VALUES.TEST_SHA256_HASH); - }, - }; + cryptoInterface = mockCrypto; }); afterEach(() => { @@ -127,7 +69,7 @@ describe("ProtocolUtils.ts Class Unit Tests", () => { it("parseRequestState() returns empty userRequestState if no resource delimiter found in state string", () => { const requestState = ProtocolUtils.parseRequestState( cryptoInterface, - decodedLibState + cryptoInterface.base64Encode(decodedLibState) ); expect(requestState.userRequestState).toHaveLength(0); }); diff --git a/lib/msal-node/CHANGELOG.json b/lib/msal-node/CHANGELOG.json index e6db9c5798..bbfe7d0b13 100644 --- a/lib/msal-node/CHANGELOG.json +++ b/lib/msal-node/CHANGELOG.json @@ -1,6 +1,396 @@ { "name": "@azure/msal-node", "entries": [ + { + "date": "Tue, 08 Jul 2025 14:21:12 GMT", + "version": "3.6.3", + "tag": "@azure/msal-node_v3.6.3", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump @azure/msal-common to v15.8.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 01 Jul 2025 14:23:25 GMT", + "version": "3.6.2", + "tag": "@azure/msal-node_v3.6.2", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-node", + "commit": "b2942e458324c8bf767a068cae3eccbd4863baf3", + "comment": "Add correlationIds to cache APIs #7819" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump @azure/msal-common to v15.8.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Wed, 18 Jun 2025 15:21:55 GMT", + "version": "3.6.1", + "tag": "@azure/msal-node_v3.6.1", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-node", + "commit": "32c6a1bd1f2a8d2da6a1b0ad69a5fd9c7e4107c4", + "comment": "remove access tokens synchronously" + }, + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-node", + "commit": "32c6a1bd1f2a8d2da6a1b0ad69a5fd9c7e4107c4", + "comment": "Added support for DEFAULT_IDENTITY_CLIENT_ID environment variable in Machine Learning Managed Identity #7616" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump @azure/msal-common to v15.7.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Fri, 30 May 2025 22:36:44 GMT", + "version": "3.6.0", + "tag": "@azure/msal-node_v3.6.0", + "comments": { + "patch": [ + { + "author": "shylasummers@microsoft.com", + "package": "@azure/msal-node", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "update common version" + } + ], + "minor": [ + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-node", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "Added token revocation functionality to Managed Identity's App Service and Service Fabric Sources #7679" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ], + "none": [ + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-node", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "Overhauled the comments in Imds.ts to make the code easier to read #7787" + }, + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-node", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "Overhauled the comments in ServiceFabric.ts to make the code easier to read #7789" + }, + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-node", + "commit": "8323e24e3b5b09124bfb83c0e4bbec013a2b7a0c", + "comment": "Added Constants for Encoding Types #7766" + } + ] + } + }, + { + "date": "Tue, 06 May 2025 22:47:43 GMT", + "version": "3.5.3", + "tag": "@azure/msal-node_v3.5.3", + "comments": { + "none": [ + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-node", + "commit": "b6098464c80f3db124ca3db145446ddb71be936d", + "comment": "Minor comment updates for retry policy tests #7704" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-node", + "commit": "b6098464c80f3db124ca3db145446ddb71be936d", + "comment": "New dev dependency" + } + ], + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump @azure/msal-common to v15.6.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 29 Apr 2025 20:25:48 GMT", + "version": "3.5.2", + "tag": "@azure/msal-node_v3.5.2", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-node", + "commit": "2b6112d62495ac4e3245d8a0d9be6b541ffb206c", + "comment": "Fix circular type imports" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump @azure/msal-common to v15.5.2", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 15 Apr 2025 23:34:14 GMT", + "version": "3.5.1", + "tag": "@azure/msal-node_v3.5.1", + "comments": { + "patch": [ + { + "author": "shylasummers@microsoft.com", + "package": "@azure/msal-node", + "commit": "66a6372ce1e4f80992e49d2c153628ad0415b71f", + "comment": "Add config option to not encode extra params" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump @azure/msal-common to v15.5.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 08 Apr 2025 16:56:06 GMT", + "version": "3.5.0", + "tag": "@azure/msal-node_v3.5.0", + "comments": { + "patch": [ + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-node", + "commit": "fabf962dcb69611b4516342107bc7bcc364c4e68", + "comment": "Machine Learning Managed Identity now correctly uses MSI's 2017 API instead of the 2019 API #7631" + } + ], + "minor": [ + { + "author": "rginsburg@microsoft.com", + "package": "@azure/msal-node", + "commit": "fabf962dcb69611b4516342107bc7bcc364c4e68", + "comment": "Implemented a Retry Policy for the IMDS Managed Identity Source #7614" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump @azure/msal-common to v15.5.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 25 Mar 2025 22:29:34 GMT", + "version": "3.4.1", + "tag": "@azure/msal-node_v3.4.1", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-node", + "commit": "8bbed7012616d9106c923d08b5c4e408a741577f", + "comment": "Move addition of extraQueryParameters during request generation" + }, + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-node", + "commit": "8bbed7012616d9106c923d08b5c4e408a741577f", + "comment": "Minor updates to Authorize query string generation" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump @azure/msal-common to v15.4.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, + { + "date": "Thu, 20 Mar 2025 22:09:04 GMT", + "version": "3.4.0", + "tag": "@azure/msal-node_v3.4.0", + "comments": { + "none": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-node", + "commit": "0de0f206568d7f075d12177172d108efc01c9fab", + "comment": "Test changes" + } + ], + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-node", + "commit": "0de0f206568d7f075d12177172d108efc01c9fab", + "comment": "refactor RequestParameterBuilder" + } + ], + "minor": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-node", + "commit": "0de0f206568d7f075d12177172d108efc01c9fab", + "comment": "Refactor /authorize request generation" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump @azure/msal-common to v15.3.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-node", + "comment": "Bump rollup-msal to v0.0.0", + "commit": "not available" + } + ] + } + }, { "date": "Tue, 11 Mar 2025 18:51:25 GMT", "version": "3.3.0", diff --git a/lib/msal-node/CHANGELOG.md b/lib/msal-node/CHANGELOG.md index 618cb12641..cd0edc4335 100644 --- a/lib/msal-node/CHANGELOG.md +++ b/lib/msal-node/CHANGELOG.md @@ -1,9 +1,130 @@ # Change Log - @azure/msal-node - + +## 3.6.3 + +Tue, 08 Jul 2025 14:21:12 GMT + +### Patches + +- Bump @azure/msal-common to v15.8.1 +- Bump eslint-config-msal to v0.0.0 +- Bump rollup-msal to v0.0.0 + +## 3.6.2 + +Tue, 01 Jul 2025 14:23:25 GMT + +### Patches + +- Add correlationIds to cache APIs #7819 (thomas.norling@microsoft.com) +- Bump @azure/msal-common to v15.8.0 +- Bump eslint-config-msal to v0.0.0 +- Bump rollup-msal to v0.0.0 + +## 3.6.1 + +Wed, 18 Jun 2025 15:21:55 GMT + +### Patches + +- remove access tokens synchronously (thomas.norling@microsoft.com) +- Added support for DEFAULT_IDENTITY_CLIENT_ID environment variable in Machine Learning Managed Identity #7616 (rginsburg@microsoft.com) +- Bump @azure/msal-common to v15.7.1 +- Bump eslint-config-msal to v0.0.0 +- Bump rollup-msal to v0.0.0 + +## 3.6.0 + +Fri, 30 May 2025 22:36:44 GMT + +### Minor changes + +- Added token revocation functionality to Managed Identity's App Service and Service Fabric Sources #7679 (rginsburg@microsoft.com) +- Bump eslint-config-msal to v0.0.0 +- Bump rollup-msal to v0.0.0 + +### Patches + +- update common version (shylasummers@microsoft.com) + +## 3.5.3 + +Tue, 06 May 2025 22:47:43 GMT + +### Patches + +- Bump @azure/msal-common to v15.6.0 +- Bump eslint-config-msal to v0.0.0 +- Bump rollup-msal to v0.0.0 + +## 3.5.2 + +Tue, 29 Apr 2025 20:25:48 GMT + +### Patches + +- Fix circular type imports (thomas.norling@microsoft.com) +- Bump @azure/msal-common to v15.5.2 +- Bump eslint-config-msal to v0.0.0 +- Bump rollup-msal to v0.0.0 + +## 3.5.1 + +Tue, 15 Apr 2025 23:34:14 GMT + +### Patches + +- Add config option to not encode extra params (shylasummers@microsoft.com) +- Bump @azure/msal-common to v15.5.1 +- Bump eslint-config-msal to v0.0.0 +- Bump rollup-msal to v0.0.0 + +## 3.5.0 + +Tue, 08 Apr 2025 16:56:06 GMT + +### Minor changes + +- Implemented a Retry Policy for the IMDS Managed Identity Source #7614 (rginsburg@microsoft.com) +- Bump @azure/msal-common to v15.5.0 +- Bump eslint-config-msal to v0.0.0 +- Bump rollup-msal to v0.0.0 + +### Patches + +- Machine Learning Managed Identity now correctly uses MSI's 2017 API instead of the 2019 API #7631 (rginsburg@microsoft.com) + +## 3.4.1 + +Tue, 25 Mar 2025 22:29:34 GMT + +### Patches + +- Move addition of extraQueryParameters during request generation (thomas.norling@microsoft.com) +- Minor updates to Authorize query string generation (thomas.norling@microsoft.com) +- Bump @azure/msal-common to v15.4.0 +- Bump eslint-config-msal to v0.0.0 +- Bump rollup-msal to v0.0.0 + +## 3.4.0 + +Thu, 20 Mar 2025 22:09:04 GMT + +### Minor changes + +- Refactor /authorize request generation (thomas.norling@microsoft.com) +- Bump @azure/msal-common to v15.3.0 +- Bump eslint-config-msal to v0.0.0 +- Bump rollup-msal to v0.0.0 + +### Patches + +- refactor RequestParameterBuilder (thomas.norling@microsoft.com) + ## 3.3.0 Tue, 11 Mar 2025 18:51:25 GMT diff --git a/lib/msal-node/README.md b/lib/msal-node/README.md index 01920811ed..acd6e9b75b 100644 --- a/lib/msal-node/README.md +++ b/lib/msal-node/README.md @@ -90,6 +90,7 @@ Any major MSAL Node release: | MSAL Node version | MSAL support status | Supported Node versions | | ----------------- | ------------------- | ----------------------- | +| 3.x.x | Active development | 16, 18, 20, 22, 24 | | 2.x.x | Active development | 16, 18, 20, 22 | | 1.x.x | In maintenance | 10, 12, 14, 16, 18 | diff --git a/lib/msal-node/apiReview/msal-node.api.md b/lib/msal-node/apiReview/msal-node.api.md index 0967a62b97..17f38c186f 100644 --- a/lib/msal-node/apiReview/msal-node.api.md +++ b/lib/msal-node/apiReview/msal-node.api.md @@ -23,6 +23,7 @@ import { AuthErrorMessage } from '@azure/msal-common/node'; import { Authority } from '@azure/msal-common/node'; import { AuthorityMetadataEntity } from '@azure/msal-common/node'; import { AuthorizationCodePayload } from '@azure/msal-common/node'; +import { AuthorizeResponse } from '@azure/msal-common/node'; import { AzureCloudInstance } from '@azure/msal-common/node'; import { AzureCloudOptions } from '@azure/msal-common/node'; import { AzureRegionConfiguration } from '@azure/msal-common/node'; @@ -72,7 +73,6 @@ import { ProtocolMode } from '@azure/msal-common/node'; import { RefreshTokenCache } from '@azure/msal-common/node'; import { RefreshTokenEntity } from '@azure/msal-common/node'; import { ResponseMode } from '@azure/msal-common/node'; -import { ServerAuthorizationCodeResponse } from '@azure/msal-common/node'; import { ServerError } from '@azure/msal-common/node'; import { ServerTelemetryEntity } from '@azure/msal-common/node'; import { ServerTelemetryManager } from '@azure/msal-common/node'; @@ -113,6 +113,9 @@ export type AuthorizationUrlRequest = Partial; acquireTokenSilent(request: SilentFlowRequest): Promise; - protected buildOauthClientConfiguration(authority: string, requestCorrelationId: string, redirectUri: string, serverTelemetryManager?: ServerTelemetryManager, azureRegionConfiguration?: AzureRegionConfiguration, azureCloudOptions?: AzureCloudOptions): Promise; + protected buildOauthClientConfiguration(discoveredAuthority: Authority, requestCorrelationId: string, redirectUri: string, serverTelemetryManager?: ServerTelemetryManager): Promise; clearCache(): void; protected clientAssertion: ClientAssertion; protected clientSecret: string; // Warning: (ae-forgotten-export) The symbol "NodeConfiguration" needs to be exported by the entry point index.d.ts protected config: NodeConfiguration; + protected createAuthority(authorityString: string, requestCorrelationId: string, azureRegionConfiguration?: AzureRegionConfiguration, azureCloudOptions?: AzureCloudOptions): Promise; // (undocumented) protected readonly cryptoProvider: CryptoProvider; // (undocumented) @@ -226,7 +230,7 @@ export class CryptoProvider implements ICrypto { generatePkceCodes(): Promise; getPublicKeyThumbprint(): Promise; hashString(plainText: string): Promise; - removeTokenBindingKey(): Promise; + removeTokenBindingKey(): Promise; signJwt(): Promise; } @@ -297,7 +301,7 @@ export interface ILoopbackClient { // (undocumented) getRedirectUri(): string; // (undocumented) - listenForAuthCode(successTemplate?: string, errorTemplate?: string): Promise; + listenForAuthCode(successTemplate?: string, errorTemplate?: string): Promise; } export { INativeBrokerPlugin } @@ -393,6 +397,7 @@ export class ManagedIdentityApplication { // @public (undocumented) export type ManagedIdentityConfiguration = { + clientCapabilities?: Array; managedIdentityIdParams?: ManagedIdentityIdParams; system?: NodeSystemOptions; }; @@ -448,6 +453,7 @@ export type NodeAuthOptions = { protocolMode?: ProtocolMode; azureCloudOptions?: AzureCloudOptions; skipAuthorityMetadataCache?: boolean; + encodeExtraQueryParams?: boolean; }; // @public @@ -574,8 +580,6 @@ class Serializer { static serializeRefreshTokens(rtCache: RefreshTokenCache): Record; } -export { ServerAuthorizationCodeResponse } - export { ServerError } // @public (undocumented) @@ -596,14 +600,14 @@ export class TokenCache implements ISerializableTokenCache, ITokenCache { deserialize(cache: string): void; getAccountByHomeId(homeAccountId: string): Promise; getAccountByLocalId(localAccountId: string): Promise; - getAllAccounts(): Promise; + getAllAccounts(correlationId?: string): Promise; getCacheSnapshot(): CacheKVStore; getKVStore(): CacheKVStore; hasChanged(): boolean; overwriteCache(): Promise; // (undocumented) readonly persistence: ICachePlugin; - removeAccount(account: AccountInfo): Promise; + removeAccount(account: AccountInfo, correlationId?: string): Promise; serialize(): string; } @@ -627,7 +631,7 @@ export { ValidCacheType } // Warning: (ae-missing-release-tag) "version" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const version = "3.3.0"; +export const version = "3.6.3"; // Warnings were encountered during analysis: // diff --git a/lib/msal-node/docs/certificate-credentials.md b/lib/msal-node/docs/certificate-credentials.md index 69b2451af9..67cf873365 100644 --- a/lib/msal-node/docs/certificate-credentials.md +++ b/lib/msal-node/docs/certificate-credentials.md @@ -6,7 +6,7 @@ You can build confidential client applications with MSAL Node (web apps, daemon - `managed identity`: this is a certificateless scenario, where trust is established via the Azure infrastructure. No secret / certificate management is required. MSAL does not yet implement this feature, but you may use Azure Identity SDK instead. See https://learn.microsoft.com/en-us/azure/active-directory/managed-identities-azure-resources/ - `clientSecret`: a secret string generated during the app registration, or updated post registration for an existing application. This is not recommended for production. -- `clientCertificate`: a certificate set during the app registration. The certificate needs to have the private key, because it will be used for signing [an assertion](https://learn.microsoft.com/azure/active-directory/develop/certificate-credentials) that MSAL generates. The `thumbprint` is a _X.509 SHA-1_ thumbprint of the certificate (x5t), and the `privateKey` is the PEM encoded private key. +- `clientCertificate`: a certificate set during the app registration. The certificate needs to have the private key, because it will be used for signing [an assertion](https://learn.microsoft.com/azure/active-directory/develop/certificate-credentials) that MSAL generates. The `thumbprintSha256` is a _X.509 SHA-256_ thumbprint of the certificate, and the `privateKey` is the PEM encoded private key. - `clientAssertion`: instead of letting MSAL create an [assertion](https://learn.microsoft.com/azure/active-directory/develop/certificate-credentials), the app developer takes control. Useful for adding extra claims to the assertion or for using KeyVault for signing, instead of a local certificate. The certificate used to sign the assertion still needs to be set during app registration. Note: 1p apps may be required to also send `x5c`. This is the _X.509_ certificate chain used in [subject name/issuer auth scenarios](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/sni.md). @@ -30,7 +30,7 @@ You need to upload your certificate to **Azure AD**. 1. Navigate to [Azure portal](https://portal.azure.com) and select your Azure AD app registration. 2. Select **Certificates & secrets** blade on the left. 3. Click on **Upload** certificate and select the certificate file to upload (e.g. _example.crt_). -4. Click **Add**. Once the certificate is uploaded, the _thumbprint_, _start date_, and _expiration_ values are displayed. +4. Click **Add**. Once the certificate is uploaded, the _thumbprint (SHA-256)_, _start date_, and _expiration_ values are displayed. For more information, see: [Register your certificate with Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials#register-your-certificate-with-microsoft-identity-platform) @@ -45,7 +45,7 @@ const config = { clientId: "YOUR_CLIENT_ID", authority: "https://login.microsoftonline.com/YOUR_TENANT_ID", clientCertificate: { - thumbprint: process.env.thumbprint, // a 40-digit hexadecimal string + thumbprintSha256: process.env.thumbprint, privateKey: process.env.privateKey, }, }, @@ -55,7 +55,7 @@ const config = { const cca = new msal.ConfidentialClientApplication(config); ``` -Both `thumbprint` and `privateKey` are expected to be strings. `privateKey` is further expected to be in the following form (_PKCS#8_): +Both `thumbprintSha256` and `privateKey` are expected to be strings. `privateKey` is further expected to be in the following form (_PKCS#8_): ```text -----BEGIN ENCRYPTED PRIVATE KEY----- @@ -65,7 +65,7 @@ z2HCpDsa7dxOsKIrm7F1AtGBjyB0yVDjlh/FA7jT5sd2ypBh3FVsZGJudQsLRKfE -----END ENCRYPTED PRIVATE KEY----- ``` -> :information_source: Alternatively, your private key may begin with `-----BEGIN PRIVATE KEY-----` (unencrypted _PKCS#8_) or `-----BEGIN RSA PRIVATE KEY-----` (_PKCS#1_). These formats are also permissible. The following can be used to convert any compatible key to the PKCS#8 key type: +> :information*source: Alternatively, your private key may begin with `-----BEGIN PRIVATE KEY-----` (unencrypted \_PKCS#8*) or `-----BEGIN RSA PRIVATE KEY-----` (_PKCS#1_). These formats are also permissible. The following can be used to convert any compatible key to the PKCS#8 key type: > > ```bash > openssl pkcs8 -topk8 -inform PEM -outform PEM -in example.key -out example.key @@ -204,7 +204,7 @@ const config = { clientId: "YOUR_CLIENT_ID", authority: "https://login.microsoftonline.com/YOUR_TENANT_ID", clientCertificate: { - thumbprint: process.env.thumbprint, // a 40-digit hexadecimal string + thumbprintSha256: process.env.thumbprint, privateKey: privateKey, }, }, diff --git a/lib/msal-node/docs/faq.md b/lib/msal-node/docs/faq.md index 0edbe98346..ee267ed964 100644 --- a/lib/msal-node/docs/faq.md +++ b/lib/msal-node/docs/faq.md @@ -39,7 +39,7 @@ Yes. Please refer to [MSAL Node samples](https://github.com/AzureAD/microsoft-au ### Is interactive flow supported? -Currently No. Authentication for MSAL Node using authorization code grant is a two legged flow, as detailed [here](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/request.md). There are plans to provide a single API to achieve this, and invoke the browser on the user's behalf. However it is currently not supported. +Yes, there is an all-in-one `acquireTokenInteractive` API available for `PublicClientApplication` scenarios. Please read more about this API [here](./request.md#acquireTokenInteractive) ### Are SPAs supported by MSAL Node? diff --git a/lib/msal-node/docs/initialize-confidential-client-application.md b/lib/msal-node/docs/initialize-confidential-client-application.md index f86e9e34dd..4e2f675469 100644 --- a/lib/msal-node/docs/initialize-confidential-client-application.md +++ b/lib/msal-node/docs/initialize-confidential-client-application.md @@ -45,7 +45,7 @@ const clientConfig = { authority: "your_authority", clientSecret: process.env.clientSecret, // OR clientCertificate: { - thumbprint: process.env.thumbprint, + thumbprintSha256: process.env.thumbprint, privateKey: process.env.privateKey, }, // OR clientAssertion: clientAssertionCallback, // or a predetermined clientAssertion string @@ -62,7 +62,7 @@ const cca = new msal.ConfidentialClientApplication(clientConfig); - `authority` defaults to `https://login.microsoftonline.com/common/` if the user does not set it during configuration - A Client credential is mandatory for confidential clients. Client credential can be a: - `clientSecret` is secret string generated set on the app registration. - - `clientCertificate` is a certificate set on the app registration. The `thumbprint` is a X.509 SHA-1 thumbprint of the certificate, and the `privateKey` is the PEM encoded private key. `x5c` is the optional X.509 certificate chain used in [subject name/issuer auth scenarios](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/sni.md). + - `clientCertificate` is a certificate set on the app registration. The `thumbprintSha256` is a X.509 SHA-256 thumbprint of the certificate, and the `privateKey` is the PEM encoded private key. `x5c` is the optional X.509 certificate chain used in [subject name/issuer auth scenarios](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/sni.md). - `clientAssertion` is a ClientAssertion object containing an assertion string or a callback function that returns an assertion string that the application uses when requesting a token, as well as the assertion's type (urn:ietf:params:oauth:client-assertion-type:jwt-bearer). The callback is invoked every time MSAL needs to acquire a token from the token issuer. App developers should generally use the callback because assertions expire and new assertions need to be created. App developers are responsible for the assertion lifetime. Use [this mechanism](https://learn.microsoft.com/entra/workload-id/workload-identity-federation-create-trust) to get tokens for a downstream API using a Federated Identity Credential. ## Configure Authority diff --git a/lib/msal-node/docs/request.md b/lib/msal-node/docs/request.md index e14ec4463c..1e2f641c36 100644 --- a/lib/msal-node/docs/request.md +++ b/lib/msal-node/docs/request.md @@ -4,12 +4,46 @@ Since MSAL Node supports various authorization code grants, there is support for ## Authorization Code Flow -### Public APIs +### acquireTokenInteractive + +[acquireTokenInteractive()](https://azuread.github.io/microsoft-authentication-library-for-js/ref/classes/_azure_msal_node.PublicClientApplication.html#acquireTokenInteractive): This API handles both legs of the authorization code flow. It is available for PublicClientApplication only. The request type is documented here: [InteractiveRequest](https://azuread.github.io/microsoft-authentication-library-for-js/ref/types/_azure_msal_node.InteractiveRequest.html). The only required parameter is a `openBrowser` callback which should accept a `url` parameter and open the browser of choice to complete the sign-in. A sample demonstrating its usage can be found [here](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-node-samples/auth-code-cli-app) + +```javascript +import { PublicClientApplication } from "@azure/msal-node"; +import open from "open"; + +// Open browser to sign user in and consent to scopes needed for application +const openBrowser = async (url) => { + // You can open a browser window with any library or method you wish to use - the 'open' npm package is used here for demonstration purposes. + open(url); +}; + +const loginRequest = { + scopes: ["User.Read"], + openBrowser, + successTemplate: "Successfully signed in! You can close this window now." // Will be shown in the browser window after authentication is complete +}; + +// Create msal application object +const pca = new PublicClientApplication({ + auth: { + clientId: "" + } +}); +pca.acquireTokenInteractive(loginRequest).then((response) => { + // Do something with the token e.g. call an API + callAPI(response.accessToken) + }) + .catch((error) => { + console.log(error); + }); + +``` -- [getAuthCodeUrl()](https://azuread.github.io/microsoft-authentication-library-for-js/ref/msal-node/classes/_src_client_publicclientapplication_.publicclientapplication.html#getauthcodeurl): This API is the first leg of the `authorization code grant` for MSAL Node. The request is of the type [AuthorizationUrlRequest](https://azuread.github.io/microsoft-authentication-library-for-js/ref/msal-common/modules/_src_request_authorizationurlrequest_.html). - The application is sent a URL that can be used to generate an `authorization code`. This URL can be opened in a browser of choice, where the user can input their credentials, and will be redirected back to the `redirectUri` (registered during the [app registration](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-app-registration)) with an `authorization code`. The `authorization code` can now be redeemed for a `token` with the following step. Note that if authorization code flow is being done for a public client application, [PKCE](https://tools.ietf.org/html/rfc7636) is recommended. +## getAuthCodeUrl -- [acquireTokenByCode()](https://azuread.github.io/microsoft-authentication-library-for-js/ref/msal-node/classes/_src_client_publicclientapplication_.publicclientapplication.html#acquiretokenbycode): This API is the second leg of the `authorization code grant` for MSAL Node. The request constructed here should be of the type [AuthorizationCodeRequest](https://azuread.github.io/microsoft-authentication-library-for-js/ref/msal-common/modules/_src_request_authorizationcoderequest_.html). The application passed the `authorization code` received as a part of the above step and exchanges it for a `token`. Note that if authorization code flow is being done for a public client application, [PKCE](https://tools.ietf.org/html/rfc7636) is recommended. +[getAuthCodeUrl()](https://azuread.github.io/microsoft-authentication-library-for-js/ref/interfaces/_azure_msal_node.IConfidentialClientApplication.html#getAuthCodeUrl): This API performs the first leg of the authorization code flow. The request is of the type [AuthorizationUrlRequest](https://azuread.github.io/microsoft-authentication-library-for-js/ref/types/_azure_msal_node.AuthorizationUrlRequest.html). +getAuthCodeUrl returns a url that can be used to generate an `authorization code`. This URL can be opened in a browser of choice, where the user can input their credentials, and will be redirected back to the `redirectUri` (registered during the [app registration](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-desktop-app-registration)) with an `authorization code`. The `authorization code` can now be redeemed for a `token` using acquireTokenByCode, documented below. Note that if authorization code flow is being done for a public client application, we recommend using `acquireTokenInteractive` documented above, otherwise the use of [PKCE](https://tools.ietf.org/html/rfc7636) is recommended. ```javascript const authCodeUrlParameters = { @@ -17,23 +51,43 @@ const authCodeUrlParameters = { redirectUri: "your_redirect_uri", }; +const cca = new ConfidentialClientApplication({ + auth: { + clientId: "" + } +}); + // get url to sign user in and consent to scopes needed for application cca.getAuthCodeUrl(authCodeUrlParameters) - .then((response) => { - console.log(response); + .then((url) => { + // redirect to url }) .catch((error) => console.log(JSON.stringify(error))); +``` +## acquireTokenByCode + +[acquireTokenByCode()](https://azuread.github.io/microsoft-authentication-library-for-js/ref/interfaces/_azure_msal_node.IConfidentialClientApplication.html#acquireTokenByCode): This API is the second leg of the authorization code flow. The request is of the type [AuthorizationCodeRequest](https://azuread.github.io/microsoft-authentication-library-for-js/ref/types/_azure_msal_node.AuthorizationCodeRequest.html). The application should have received an `authorization code` as a part of the above step and can now exchange it for a `token`. Note that if authorization code flow is being done for a public client application, we recommend using `acquireTokenInteractive` documented above, otherwise the use of [PKCE](https://tools.ietf.org/html/rfc7636) is recommended. + + +```javascript const tokenRequest = { code: "authorization_code", redirectUri: "your_redirect_uri", scopes: ["sample_scope"], }; +const cca = new ConfidentialClientApplication({ + auth: { + clientId: "" + } +}); + // acquire a token by exchanging the code cca.acquireTokenByCode(tokenRequest) .then((response) => { - console.log("\nResponse: \n:", response); + // Do something with the token e.g. call an API + callAPI(response.accessToken) }) .catch((error) => { console.log(error); diff --git a/lib/msal-node/docs/sni.md b/lib/msal-node/docs/sni.md index c3a369aa58..187e47daad 100644 --- a/lib/msal-node/docs/sni.md +++ b/lib/msal-node/docs/sni.md @@ -8,7 +8,7 @@ First party users should follow the instructions on the [internal AAD wiki](http ## x5c claim -You will need to supply the string from your `pem` encoded certificate to MSAL configuration object in the `clientCertificate.x5c` field in addition to providing both `clientCertificate.thumbprint` and `clientCertificate.privateKey`: +You will need to supply the string from your `pem` encoded certificate to MSAL configuration object in the `clientCertificate.x5c` field in addition to providing both `clientCertificate.thumbprintSha256` and `clientCertificate.privateKey`: Example `x5c` string from a `.pem` file: @@ -49,7 +49,7 @@ const config = { clientId: "ENTER_CLIENT_ID", authority: "https://login.microsoftonline.com/ENTER_TENANT_ID", clientCertificate: { - thumbprint: process.env.thumbprint, // a 40-digit hexadecimal string + thumbprintSha256: process.env.thumbprint, privateKey: process.env.privateKey, x5c: process.env.x5c } diff --git a/lib/msal-node/jest.config.cjs b/lib/msal-node/jest.config.cjs index b7c2194cd3..2ce5ea096c 100644 --- a/lib/msal-node/jest.config.cjs +++ b/lib/msal-node/jest.config.cjs @@ -6,6 +6,7 @@ module.exports = { verbose: true, moduleFileExtensions: ["ts", "tsx", "js", "json", "jsx", "node"], + reporters: ["default", "jest-junit"], testMatch: ["/test/**/*.spec.ts"], transform: { "^.+\\.(ts|tsx)$": "ts-jest", diff --git a/lib/msal-node/package.json b/lib/msal-node/package.json index 234a5da612..65d4a16d04 100644 --- a/lib/msal-node/package.json +++ b/lib/msal-node/package.json @@ -1,7 +1,7 @@ { "$schema": "https://json.schemastore.org/package.json", "name": "@azure/msal-node", - "version": "3.3.0", + "version": "3.6.3", "author": { "name": "Microsoft", "email": "nugetaad@microsoft.com", @@ -73,6 +73,7 @@ "@types/uuid": "^7.0.0", "eslint-config-msal": "file:../../shared-configs/eslint-config-msal", "jest": "^29.5.0", + "jest-junit": "^16.0.0", "prettier": "2.8.7", "rollup": "^4.22.4", "rollup-msal": "file:../../shared-configs/rollup-msal", @@ -82,7 +83,7 @@ "yargs": "^17.3.1" }, "dependencies": { - "@azure/msal-common": "15.2.1", + "@azure/msal-common": "15.8.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, diff --git a/lib/msal-node/src/cache/NodeStorage.ts b/lib/msal-node/src/cache/NodeStorage.ts index d1c0c36582..5a904a0bba 100644 --- a/lib/msal-node/src/cache/NodeStorage.ts +++ b/lib/msal-node/src/cache/NodeStorage.ts @@ -29,6 +29,7 @@ import { JsonCache, CacheKVStore, } from "./serializer/SerializerTypes.js"; +import { StubPerformanceClient } from "@azure/msal-common"; /** * This class implements Storage for node, reading cache from user specified storage location or an extension library @@ -46,7 +47,13 @@ export class NodeStorage extends CacheManager { cryptoImpl: ICrypto, staticAuthorityOptions?: StaticAuthorityOptions ) { - super(clientId, cryptoImpl, logger, staticAuthorityOptions); + super( + clientId, + cryptoImpl, + logger, + new StubPerformanceClient(), + staticAuthorityOptions + ); this.logger = logger; } diff --git a/lib/msal-node/src/cache/TokenCache.ts b/lib/msal-node/src/cache/TokenCache.ts index bdf95577e7..d9961d1e1e 100644 --- a/lib/msal-node/src/cache/TokenCache.ts +++ b/lib/msal-node/src/cache/TokenCache.ts @@ -25,6 +25,8 @@ import { import { Deserializer } from "./serializer/Deserializer.js"; import { Serializer } from "./serializer/Serializer.js"; import { ITokenCache } from "./ITokenCache.js"; +import { CryptoProvider } from "../crypto/CryptoProvider.js"; +import { GuidGenerator } from "../crypto/GuidGenerator.js"; const defaultSerializedCache: JsonCache = { Account: {}, @@ -129,7 +131,9 @@ export class TokenCache implements ISerializableTokenCache, ITokenCache { /** * API that retrieves all accounts currently in cache to the user */ - async getAllAccounts(): Promise { + async getAllAccounts( + correlationId: string = new CryptoProvider().createNewGuid() + ): Promise { this.logger.trace("getAllAccounts called"); let cacheContext; try { @@ -137,7 +141,7 @@ export class TokenCache implements ISerializableTokenCache, ITokenCache { cacheContext = new TokenCacheContext(this, false); await this.persistence.beforeCacheAccess(cacheContext); } - return this.storage.getAllAccounts(); + return this.storage.getAllAccounts({}, correlationId); } finally { if (this.persistence && cacheContext) { await this.persistence.afterCacheAccess(cacheContext); @@ -191,7 +195,10 @@ export class TokenCache implements ISerializableTokenCache, ITokenCache { * API to remove a specific account and the relevant data from cache * @param account - AccountInfo passed by the user */ - async removeAccount(account: AccountInfo): Promise { + async removeAccount( + account: AccountInfo, + correlationId?: string + ): Promise { this.logger.trace("removeAccount called"); let cacheContext; try { @@ -199,8 +206,9 @@ export class TokenCache implements ISerializableTokenCache, ITokenCache { cacheContext = new TokenCacheContext(this, true); await this.persistence.beforeCacheAccess(cacheContext); } - await this.storage.removeAccount( - AccountEntity.generateAccountCacheKey(account) + this.storage.removeAccount( + AccountEntity.generateAccountCacheKey(account), + correlationId || new GuidGenerator().generateGuid() ); } finally { if (this.persistence && cacheContext) { diff --git a/lib/msal-node/src/client/ClientApplication.ts b/lib/msal-node/src/client/ClientApplication.ts index d38064ee7e..147952b5d6 100644 --- a/lib/msal-node/src/client/ClientApplication.ts +++ b/lib/msal-node/src/client/ClientApplication.ts @@ -57,6 +57,7 @@ import { version, name } from "../packageMetadata.js"; import { UsernamePasswordRequest } from "../request/UsernamePasswordRequest.js"; import { NodeAuthError } from "../error/NodeAuthError.js"; import { UsernamePasswordClient } from "./UsernamePasswordClient.js"; +import { getAuthCodeRequestUrl } from "../protocol/Authorize.js"; /** * Base abstract class for all ClientApplications - public and confidential @@ -130,24 +131,22 @@ export abstract class ClientApplication { ...(await this.initializeBaseRequest(request)), responseMode: request.responseMode || ResponseMode.QUERY, authenticationScheme: AuthenticationScheme.BEARER, + state: request.state || "", + nonce: request.nonce || "", }; - const authClientConfig = await this.buildOauthClientConfiguration( + const discoveredAuthority = await this.createAuthority( validRequest.authority, validRequest.correlationId, - validRequest.redirectUri, - undefined, undefined, request.azureCloudOptions ); - const authorizationCodeClient = new AuthorizationCodeClient( - authClientConfig - ); - this.logger.verbose( - "Auth code client created", - validRequest.correlationId + return getAuthCodeRequestUrl( + this.config, + discoveredAuthority, + validRequest, + this.logger ); - return authorizationCodeClient.getAuthCodeUrl(validRequest); } /** @@ -180,14 +179,18 @@ export abstract class ClientApplication { validRequest.correlationId ); try { - const authClientConfig = await this.buildOauthClientConfiguration( + const discoveredAuthority = await this.createAuthority( validRequest.authority, validRequest.correlationId, - validRequest.redirectUri, - serverTelemetryManager, undefined, request.azureCloudOptions ); + const authClientConfig = await this.buildOauthClientConfiguration( + discoveredAuthority, + validRequest.correlationId, + validRequest.redirectUri, + serverTelemetryManager + ); const authorizationCodeClient = new AuthorizationCodeClient( authClientConfig ); @@ -233,14 +236,18 @@ export abstract class ClientApplication { validRequest.correlationId ); try { + const discoveredAuthority = await this.createAuthority( + validRequest.authority, + validRequest.correlationId, + undefined, + request.azureCloudOptions + ); const refreshTokenClientConfig = await this.buildOauthClientConfiguration( - validRequest.authority, + discoveredAuthority, validRequest.correlationId, validRequest.redirectUri || "", - serverTelemetryManager, - undefined, - request.azureCloudOptions + serverTelemetryManager ); const refreshTokenClient = new RefreshTokenClient( refreshTokenClientConfig @@ -283,14 +290,18 @@ export abstract class ClientApplication { ); try { + const discoveredAuthority = await this.createAuthority( + validRequest.authority, + validRequest.correlationId, + undefined, + request.azureCloudOptions + ); const clientConfiguration = await this.buildOauthClientConfiguration( - validRequest.authority, + discoveredAuthority, validRequest.correlationId, validRequest.redirectUri || "", - serverTelemetryManager, - undefined, - validRequest.azureCloudOptions + serverTelemetryManager ); const silentFlowClient = new SilentFlowClient(clientConfiguration); this.logger.verbose( @@ -391,14 +402,18 @@ export abstract class ClientApplication { validRequest.correlationId ); try { + const discoveredAuthority = await this.createAuthority( + validRequest.authority, + validRequest.correlationId, + undefined, + request.azureCloudOptions + ); const usernamePasswordClientConfig = await this.buildOauthClientConfiguration( - validRequest.authority, + discoveredAuthority, validRequest.correlationId, "", - serverTelemetryManager, - undefined, - request.azureCloudOptions + serverTelemetryManager ); const usernamePasswordClient = new UsernamePasswordClient( usernamePasswordClientConfig @@ -465,31 +480,16 @@ export abstract class ClientApplication { * @param serverTelemetryManager - initializes servertelemetry if passed */ protected async buildOauthClientConfiguration( - authority: string, + discoveredAuthority: Authority, requestCorrelationId: string, redirectUri: string, - serverTelemetryManager?: ServerTelemetryManager, - azureRegionConfiguration?: AzureRegionConfiguration, - azureCloudOptions?: AzureCloudOptions + serverTelemetryManager?: ServerTelemetryManager ): Promise { this.logger.verbose( "buildOauthClientConfiguration called", requestCorrelationId ); - // precedence - azureCloudInstance + tenant >> authority and request >> config - const userAzureCloudOptions = azureCloudOptions - ? azureCloudOptions - : this.config.auth.azureCloudOptions; - - // using null assertion operator as we ensure that all config values have default values in buildConfiguration() - const discoveredAuthority = await this.createAuthority( - authority, - requestCorrelationId, - azureRegionConfiguration, - userAzureCloudOptions - ); - this.logger.info( `Building oauth client configuration with the following authority: ${discoveredAuthority.tokenEndpoint}.`, requestCorrelationId @@ -640,7 +640,7 @@ export abstract class ClientApplication { * object. If no authority set in application object, then default to common authority. * @param authorityString - authority from user configuration */ - private async createAuthority( + protected async createAuthority( authorityString: string, requestCorrelationId: string, azureRegionConfiguration?: AzureRegionConfiguration, @@ -651,7 +651,7 @@ export abstract class ClientApplication { // build authority string based on auth params - azureCloudInstance is prioritized if provided const authorityUrl = Authority.generateAuthority( authorityString, - azureCloudOptions + azureCloudOptions || this.config.auth.azureCloudOptions ); const authorityOptions: AuthorityOptions = { diff --git a/lib/msal-node/src/client/ClientAssertion.ts b/lib/msal-node/src/client/ClientAssertion.ts index 19bce20286..be5d4c8b82 100644 --- a/lib/msal-node/src/client/ClientAssertion.ts +++ b/lib/msal-node/src/client/ClientAssertion.ts @@ -9,6 +9,7 @@ import { Constants, createClientAuthError, ClientAuthErrorCodes, + EncodingTypes, } from "@azure/msal-common/node"; import { CryptoProvider } from "../crypto/CryptoProvider.js"; import { EncodingUtils } from "../utils/EncodingUtils.js"; @@ -145,7 +146,7 @@ export class ClientAssertion { Object.assign(header, { [thumbprintHeader]: EncodingUtils.base64EncodeUrl( this.thumbprint, - "hex" + EncodingTypes.HEX ), } as Partial); diff --git a/lib/msal-node/src/client/ClientCredentialClient.ts b/lib/msal-node/src/client/ClientCredentialClient.ts index 20215a1043..efc112f5fc 100644 --- a/lib/msal-node/src/client/ClientCredentialClient.ts +++ b/lib/msal-node/src/client/ClientCredentialClient.ts @@ -137,7 +137,8 @@ export class ClientCredentialClient extends BaseClient { managedIdentityConfiguration.managedIdentityId?.id || clientConfiguration.authOptions.clientId, new ScopeSet(request.scopes || []), - cacheManager + cacheManager, + request.correlationId ); if ( @@ -208,7 +209,8 @@ export class ClientCredentialClient extends BaseClient { authority: Authority, id: string, scopeSet: ScopeSet, - cacheManager: CacheManager + cacheManager: CacheManager, + correlationId: string ): AccessTokenEntity | null { const accessTokenFilter: CredentialFilter = { homeAccountId: Constants.EMPTY_STRING, @@ -220,8 +222,10 @@ export class ClientCredentialClient extends BaseClient { target: ScopeSet.createSearchScopes(scopeSet.asArray()), }; - const accessTokens = - cacheManager.getAccessTokensByFilter(accessTokenFilter); + const accessTokens = cacheManager.getAccessTokensByFilter( + accessTokenFilter, + correlationId + ); if (accessTokens.length < 1) { return null; } else if (accessTokens.length > 1) { diff --git a/lib/msal-node/src/client/ConfidentialClientApplication.ts b/lib/msal-node/src/client/ConfidentialClientApplication.ts index 8ea240a22e..eb341cf28a 100644 --- a/lib/msal-node/src/client/ConfidentialClientApplication.ts +++ b/lib/msal-node/src/client/ConfidentialClientApplication.ts @@ -221,14 +221,18 @@ export class ConfidentialClientApplication validRequest.skipCache ); try { + const discoveredAuthority = await this.createAuthority( + validRequest.authority, + validRequest.correlationId, + azureRegionConfiguration, + request.azureCloudOptions + ); const clientCredentialConfig = await this.buildOauthClientConfiguration( - validRequest.authority, + discoveredAuthority, validRequest.correlationId, "", - serverTelemetryManager, - azureRegionConfiguration, - request.azureCloudOptions + serverTelemetryManager ); const clientCredentialClient = new ClientCredentialClient( clientCredentialConfig, @@ -271,14 +275,18 @@ export class ConfidentialClientApplication ...(await this.initializeBaseRequest(request)), }; try { - const onBehalfOfConfig = await this.buildOauthClientConfiguration( + const discoveredAuthority = await this.createAuthority( validRequest.authority, validRequest.correlationId, - "", - undefined, undefined, request.azureCloudOptions ); + const onBehalfOfConfig = await this.buildOauthClientConfiguration( + discoveredAuthority, + validRequest.correlationId, + "", + undefined + ); const oboClient = new OnBehalfOfClient(onBehalfOfConfig); this.logger.verbose( "On behalf of client created", diff --git a/lib/msal-node/src/client/ManagedIdentityApplication.ts b/lib/msal-node/src/client/ManagedIdentityApplication.ts index 6f2388335a..c98ec75385 100644 --- a/lib/msal-node/src/client/ManagedIdentityApplication.ts +++ b/lib/msal-node/src/client/ManagedIdentityApplication.ts @@ -18,6 +18,7 @@ import { AuthenticationResult, createClientConfigurationError, ClientConfigurationErrorCodes, + EncodingTypes, } from "@azure/msal-common/node"; import { ManagedIdentityConfiguration, @@ -35,6 +36,11 @@ import { DEFAULT_AUTHORITY_FOR_MANAGED_IDENTITY, ManagedIdentitySourceNames, } from "../utils/Constants.js"; +import { ManagedIdentityId } from "../config/ManagedIdentityId.js"; +import { HashUtils } from "../crypto/HashUtils.js"; + +const SOURCES_THAT_SUPPORT_TOKEN_REVOCATION: Array = + [ManagedIdentitySourceNames.SERVICE_FABRIC]; /** * Class to initialize a managed identity and identify the service @@ -56,6 +62,8 @@ export class ManagedIdentityApplication { private managedIdentityClient: ManagedIdentityClient; + private hashUtils: HashUtils; + constructor(configuration?: ManagedIdentityConfiguration) { // undefined config means the managed identity is system-assigned this.config = buildManagedIdentityConfiguration(configuration || {}); @@ -114,6 +122,8 @@ export class ManagedIdentityApplication { this.cryptoProvider, this.config.disableInternalRetries ); + + this.hashUtils = new HashUtils(); } /** @@ -141,14 +151,12 @@ export class ManagedIdentityApplication { ], authority: this.fakeAuthority.canonicalAuthority, correlationId: this.cryptoProvider.createNewGuid(), + claims: managedIdentityRequestParams.claims, + clientCapabilities: this.config.clientCapabilities, }; - if ( - managedIdentityRequestParams.claims || - managedIdentityRequest.forceRefresh - ) { - // make a network call to the managed identity source - return this.managedIdentityClient.sendManagedIdentityTokenRequest( + if (managedIdentityRequest.forceRefresh) { + return this.acquireTokenFromManagedIdentity( managedIdentityRequest, this.config.managedIdentityId, this.fakeAuthority @@ -164,6 +172,36 @@ export class ManagedIdentityApplication { ManagedIdentityApplication.nodeStorage as NodeStorage ); + /* + * Check if claims are present in the managed identity request. + * If so, the cached token will not be used. + */ + if (managedIdentityRequest.claims) { + const sourceName: ManagedIdentitySourceNames = + this.managedIdentityClient.getManagedIdentitySource(); + + /* + * Check if there is a cached token and if the Managed Identity source supports token revocation. + * If so, hash the cached access token and add it to the request. + */ + if ( + cachedAuthenticationResult && + SOURCES_THAT_SUPPORT_TOKEN_REVOCATION.includes(sourceName) + ) { + const revokedTokenSha256Hash: string = this.hashUtils + .sha256(cachedAuthenticationResult.accessToken) + .toString(EncodingTypes.HEX); + managedIdentityRequest.revokedTokenSha256Hash = + revokedTokenSha256Hash; + } + + return this.acquireTokenFromManagedIdentity( + managedIdentityRequest, + this.config.managedIdentityId, + this.fakeAuthority + ); + } + if (cachedAuthenticationResult) { // if the token is not expired but must be refreshed; get a new one in the background if (lastCacheOutcome === CacheOutcome.PROACTIVELY_REFRESHED) { @@ -171,9 +209,9 @@ export class ManagedIdentityApplication { "ClientCredentialClient:getCachedAuthenticationResult - Cached access token's refreshOn property has been exceeded'. It's not expired, but must be refreshed." ); - // make a network call to the managed identity source; refresh the access token in the background + // force refresh; will run in the background const refreshAccessToken = true; - await this.managedIdentityClient.sendManagedIdentityTokenRequest( + await this.acquireTokenFromManagedIdentity( managedIdentityRequest, this.config.managedIdentityId, this.fakeAuthority, @@ -183,8 +221,7 @@ export class ManagedIdentityApplication { return cachedAuthenticationResult; } else { - // make a network call to the managed identity source - return this.managedIdentityClient.sendManagedIdentityTokenRequest( + return this.acquireTokenFromManagedIdentity( managedIdentityRequest, this.config.managedIdentityId, this.fakeAuthority @@ -192,6 +229,30 @@ export class ManagedIdentityApplication { } } + /** + * Acquires a token from a managed identity endpoint. + * + * @param managedIdentityRequest - The request object containing parameters for the managed identity token request. + * @param managedIdentityId - The identifier for the managed identity (e.g., client ID or resource ID). + * @param fakeAuthority - A placeholder authority used for the token request. + * @param refreshAccessToken - Optional flag indicating whether to force a refresh of the access token. + * @returns A promise that resolves to an AuthenticationResult containing the acquired token and related information. + */ + private async acquireTokenFromManagedIdentity( + managedIdentityRequest: ManagedIdentityRequest, + managedIdentityId: ManagedIdentityId, + fakeAuthority: Authority, + refreshAccessToken?: boolean + ): Promise { + // make a network call to the managed identity + return this.managedIdentityClient.sendManagedIdentityTokenRequest( + managedIdentityRequest, + managedIdentityId, + fakeAuthority, + refreshAccessToken + ); + } + /** * Determine the Managed Identity Source based on available environment variables. This API is consumed by Azure Identity SDK. * @returns ManagedIdentitySourceNames - The Managed Identity source's name diff --git a/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts b/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts index 9eccbf287c..431dd65c00 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/AppService.ts @@ -7,12 +7,11 @@ import { INetworkModule, Logger } from "@azure/msal-common/node"; import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js"; import { HttpMethod, - APP_SERVICE_SECRET_HEADER_NAME, - API_VERSION_QUERY_PARAMETER_NAME, - RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, ManagedIdentityEnvironmentVariableNames, ManagedIdentitySourceNames, ManagedIdentityIdType, + ManagedIdentityQueryParameters, + ManagedIdentityHeaders, } from "../../utils/Constants.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js"; @@ -114,11 +113,12 @@ export class AppService extends BaseManagedIdentitySource { this.identityEndpoint ); - request.headers[APP_SERVICE_SECRET_HEADER_NAME] = this.identityHeader; + request.headers[ManagedIdentityHeaders.APP_SERVICE_SECRET_HEADER_NAME] = + this.identityHeader; - request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.API_VERSION] = APP_SERVICE_MSI_API_VERSION; - request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; if ( diff --git a/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts b/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts index 643f714e64..eedb2bb579 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/AzureArc.ts @@ -13,6 +13,7 @@ import { NetworkRequestOptions, Logger, ServerAuthorizationTokenResponse, + EncodingTypes, } from "@azure/msal-common/node"; import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js"; import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js"; @@ -22,15 +23,13 @@ import { createManagedIdentityError, } from "../../error/ManagedIdentityError.js"; import { - API_VERSION_QUERY_PARAMETER_NAME, - AUTHORIZATION_HEADER_NAME, AZURE_ARC_SECRET_FILE_MAX_SIZE_BYTES, HttpMethod, - METADATA_HEADER_NAME, ManagedIdentityEnvironmentVariableNames, + ManagedIdentityHeaders, ManagedIdentityIdType, + ManagedIdentityQueryParameters, ManagedIdentitySourceNames, - RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, } from "../../utils/Constants.js"; import { NodeStorage } from "../../cache/NodeStorage.js"; import { @@ -201,11 +200,11 @@ export class AzureArc extends BaseManagedIdentitySource { this.identityEndpoint.replace("localhost", "127.0.0.1") ); - request.headers[METADATA_HEADER_NAME] = "true"; + request.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true"; - request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.API_VERSION] = ARC_API_VERSION; - request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; // bodyParameters calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity @@ -292,7 +291,7 @@ export class AzureArc extends BaseManagedIdentitySource { // attempt to read the contents of the secret file let secret; try { - secret = readFileSync(secretFilePath, "utf-8"); + secret = readFileSync(secretFilePath, EncodingTypes.UTF8); } catch (e) { throw createManagedIdentityError( ManagedIdentityErrorCodes.unableToReadSecretFile @@ -303,7 +302,9 @@ export class AzureArc extends BaseManagedIdentitySource { this.logger.info( `[Managed Identity] Adding authorization header to the request.` ); - networkRequest.headers[AUTHORIZATION_HEADER_NAME] = authHeaderValue; + networkRequest.headers[ + ManagedIdentityHeaders.AUTHORIZATION_HEADER_NAME + ] = authHeaderValue; try { retryResponse = diff --git a/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts b/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts index 215d1fbf9d..1164ceb375 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/BaseManagedIdentitySource.ts @@ -24,7 +24,11 @@ import { ManagedIdentityId } from "../../config/ManagedIdentityId.js"; import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { ManagedIdentityRequest } from "../../request/ManagedIdentityRequest.js"; -import { HttpMethod, ManagedIdentityIdType } from "../../utils/Constants.js"; +import { + HttpMethod, + ManagedIdentityIdType, + ManagedIdentityQueryParameters, +} from "../../utils/Constants.js"; import { ManagedIdentityTokenResponse } from "../../response/ManagedIdentityTokenResponse.js"; import { NodeStorage } from "../../cache/NodeStorage.js"; import { @@ -38,7 +42,8 @@ import { HttpClientWithRetries } from "../../network/HttpClientWithRetries.js"; * Managed Identity User Assigned Id Query Parameter Names */ export const ManagedIdentityUserAssignedIdQueryParameterNames = { - MANAGED_IDENTITY_CLIENT_ID: "client_id", + MANAGED_IDENTITY_CLIENT_ID_2017: "clientid", // 2017-09-01 API version + MANAGED_IDENTITY_CLIENT_ID: "client_id", // 2019+ API versions MANAGED_IDENTITY_OBJECT_ID: "object_id", MANAGED_IDENTITY_RESOURCE_ID_IMDS: "msi_res_id", MANAGED_IDENTITY_RESOURCE_ID_NON_IMDS: "mi_res_id", @@ -145,6 +150,29 @@ export abstract class BaseManagedIdentitySource { managedIdentityId ); + if (managedIdentityRequest.revokedTokenSha256Hash) { + this.logger.info( + `[Managed Identity] The following claims are present in the request: ${managedIdentityRequest.claims}` + ); + + networkRequest.queryParameters[ + ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH + ] = managedIdentityRequest.revokedTokenSha256Hash; + } + + if (managedIdentityRequest.clientCapabilities?.length) { + const clientCapabilities: string = + managedIdentityRequest.clientCapabilities.toString(); + + this.logger.info( + `[Managed Identity] The following client capabilities are present in the request: ${clientCapabilities}` + ); + + networkRequest.queryParameters[ + ManagedIdentityQueryParameters.XMS_CC + ] = clientCapabilities; + } + const headers: Record = networkRequest.headers; headers[HeaderNames.CONTENT_TYPE] = Constants.URL_FORM_CONTENT_TYPE; @@ -164,7 +192,8 @@ export abstract class BaseManagedIdentitySource { ? this.networkClient : new HttpClientWithRetries( this.networkClient, - networkRequest.retryPolicy + networkRequest.retryPolicy, + this.logger ); const reqTimestamp = TimeUtils.nowSeconds(); @@ -226,20 +255,26 @@ export abstract class BaseManagedIdentitySource { public getManagedIdentityUserAssignedIdQueryParameterKey( managedIdentityIdType: ManagedIdentityIdType, - imds?: boolean + isImds?: boolean, + usesApi2017?: boolean ): string { switch (managedIdentityIdType) { case ManagedIdentityIdType.USER_ASSIGNED_CLIENT_ID: this.logger.info( - "[Managed Identity] Adding user assigned client id to the request." + `[Managed Identity] [API version ${ + usesApi2017 ? "2017+" : "2019+" + }] Adding user assigned client id to the request.` ); - return ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID; + // The Machine Learning source uses the 2017-09-01 API version, which uses "clientid" instead of "client_id" + return usesApi2017 + ? ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID_2017 + : ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID; case ManagedIdentityIdType.USER_ASSIGNED_RESOURCE_ID: this.logger.info( "[Managed Identity] Adding user assigned resource id to the request." ); - return imds + return isImds ? ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_RESOURCE_ID_IMDS : ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_RESOURCE_ID_NON_IMDS; diff --git a/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts b/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts index 55d99cabfd..38db56978f 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/CloudShell.ts @@ -10,11 +10,11 @@ import { NodeStorage } from "../../cache/NodeStorage.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { HttpMethod, - METADATA_HEADER_NAME, ManagedIdentityEnvironmentVariableNames, + ManagedIdentityHeaders, ManagedIdentityIdType, + ManagedIdentityQueryParameters, ManagedIdentitySourceNames, - RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, } from "../../utils/Constants.js"; import { ManagedIdentityErrorCodes, @@ -109,9 +109,9 @@ export class CloudShell extends BaseManagedIdentitySource { this.msiEndpoint ); - request.headers[METADATA_HEADER_NAME] = "true"; + request.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true"; - request.bodyParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.bodyParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; return request; diff --git a/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts b/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts index 2237782451..69aed1f746 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/Imds.ts @@ -9,26 +9,37 @@ import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRe import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { - API_VERSION_QUERY_PARAMETER_NAME, HttpMethod, - METADATA_HEADER_NAME, ManagedIdentityEnvironmentVariableNames, + ManagedIdentityHeaders, ManagedIdentityIdType, + ManagedIdentityQueryParameters, ManagedIdentitySourceNames, - RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, } from "../../utils/Constants.js"; import { NodeStorage } from "../../cache/NodeStorage.js"; +import { ImdsRetryPolicy } from "../../retry/ImdsRetryPolicy.js"; + +// Documentation for IMDS is available at https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http -// IMDS constants. Docs for IMDS are available here https://docs.microsoft.com/azure/active-directory/managed-identities-azure-resources/how-to-use-vm-token#get-a-token-using-http const IMDS_TOKEN_PATH: string = "/metadata/identity/oauth2/token"; const DEFAULT_IMDS_ENDPOINT: string = `http://169.254.169.254${IMDS_TOKEN_PATH}`; - const IMDS_API_VERSION: string = "2018-02-01"; -// Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/ImdsManagedIdentitySource.cs +/** + * Original source of code: https://github.com/Azure/azure-sdk-for-net/blob/main/sdk/identity/Azure.Identity/src/ImdsManagedIdentitySource.cs + */ export class Imds extends BaseManagedIdentitySource { private identityEndpoint: string; + /** + * Constructs an Imds instance. + * @param logger - Logger instance for logging. + * @param nodeStorage - NodeStorage instance for caching. + * @param networkClient - Network client for HTTP requests. + * @param cryptoProvider - CryptoProvider for cryptographic operations. + * @param disableInternalRetries - Whether to disable internal retry logic. + * @param identityEndpoint - The IMDS endpoint to use. + */ constructor( logger: Logger, nodeStorage: NodeStorage, @@ -48,6 +59,18 @@ export class Imds extends BaseManagedIdentitySource { this.identityEndpoint = identityEndpoint; } + /** + * Attempts to create an Imds instance by determining the correct endpoint. + * If the AZURE_POD_IDENTITY_AUTHORITY_HOST environment variable is set, it uses that as the endpoint. + * Otherwise, it falls back to the default IMDS endpoint. + * + * @param logger - Logger instance for logging. + * @param nodeStorage - NodeStorage instance for caching. + * @param networkClient - Network client for HTTP requests. + * @param cryptoProvider - CryptoProvider for cryptographic operations. + * @param disableInternalRetries - Whether to disable internal retry logic. + * @returns An instance of Imds configured with the appropriate endpoint. + */ public static tryCreate( logger: Logger, nodeStorage: NodeStorage, @@ -101,6 +124,14 @@ export class Imds extends BaseManagedIdentitySource { ); } + /** + * Creates a ManagedIdentityRequestParameters object for acquiring a token from IMDS. + * Sets the required headers and query parameters for the IMDS token request. + * + * @param resource - The resource URI for which the token is requested. + * @param managedIdentityId - The managed identity ID (system-assigned or user-assigned). + * @returns A ManagedIdentityRequestParameters object configured for IMDS. + */ public createRequest( resource: string, managedIdentityId: ManagedIdentityId @@ -111,11 +142,11 @@ export class Imds extends BaseManagedIdentitySource { this.identityEndpoint ); - request.headers[METADATA_HEADER_NAME] = "true"; + request.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true"; - request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.API_VERSION] = IMDS_API_VERSION; - request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; if ( @@ -129,7 +160,9 @@ export class Imds extends BaseManagedIdentitySource { ] = managedIdentityId.id; } - // bodyParameters calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity + // The bodyParameters are calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity. + + request.retryPolicy = new ImdsRetryPolicy(); return request; } diff --git a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts index 008a5b18e4..1e5f0a52cd 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/MachineLearning.ts @@ -4,16 +4,17 @@ */ import { INetworkModule, Logger } from "@azure/msal-common/node"; -import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js"; +import { + BaseManagedIdentitySource, + ManagedIdentityUserAssignedIdQueryParameterNames, +} from "./BaseManagedIdentitySource.js"; import { HttpMethod, - API_VERSION_QUERY_PARAMETER_NAME, - RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, ManagedIdentityEnvironmentVariableNames, ManagedIdentitySourceNames, ManagedIdentityIdType, - METADATA_HEADER_NAME, - ML_AND_SF_SECRET_HEADER_NAME, + ManagedIdentityQueryParameters, + ManagedIdentityHeaders, } from "../../utils/Constants.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { ManagedIdentityRequestParameters } from "../../config/ManagedIdentityRequestParameters.js"; @@ -22,6 +23,8 @@ import { NodeStorage } from "../../cache/NodeStorage.js"; const MACHINE_LEARNING_MSI_API_VERSION: string = "2017-09-01"; +export const MANAGED_IDENTITY_MACHINE_LEARNING_UNSUPPORTED_ID_TYPE_ERROR = `Only client id is supported for user-assigned managed identity in ${ManagedIdentitySourceNames.MACHINE_LEARNING}.`; // referenced in unit test + export class MachineLearning extends BaseManagedIdentitySource { private msiEndpoint: string; private secret: string; @@ -107,22 +110,39 @@ export class MachineLearning extends BaseManagedIdentitySource { this.msiEndpoint ); - request.headers[METADATA_HEADER_NAME] = "true"; - request.headers[ML_AND_SF_SECRET_HEADER_NAME] = this.secret; + request.headers[ManagedIdentityHeaders.METADATA_HEADER_NAME] = "true"; + request.headers[ManagedIdentityHeaders.ML_AND_SF_SECRET_HEADER_NAME] = + this.secret; - request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.API_VERSION] = MACHINE_LEARNING_MSI_API_VERSION; - request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; if ( - managedIdentityId.idType !== ManagedIdentityIdType.SYSTEM_ASSIGNED + managedIdentityId.idType === ManagedIdentityIdType.SYSTEM_ASSIGNED + ) { + request.queryParameters[ + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID_2017 + ] = process.env[ + ManagedIdentityEnvironmentVariableNames + .DEFAULT_IDENTITY_CLIENT_ID + ] as string; // this environment variable is always set in an Azure Machine Learning source + } else if ( + managedIdentityId.idType === + ManagedIdentityIdType.USER_ASSIGNED_CLIENT_ID ) { request.queryParameters[ this.getManagedIdentityUserAssignedIdQueryParameterKey( - managedIdentityId.idType + managedIdentityId.idType, + false, // isIMDS + true // uses2017API ) ] = managedIdentityId.id; + } else { + throw new Error( + MANAGED_IDENTITY_MACHINE_LEARNING_UNSUPPORTED_ID_TYPE_ERROR + ); } // bodyParameters calculated in BaseManagedIdentity.acquireTokenWithManagedIdentity diff --git a/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts b/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts index 94bc0b8f5a..a23abef22a 100644 --- a/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts +++ b/lib/msal-node/src/client/ManagedIdentitySources/ServiceFabric.ts @@ -10,16 +10,14 @@ import { BaseManagedIdentitySource } from "./BaseManagedIdentitySource.js"; import { NodeStorage } from "../../cache/NodeStorage.js"; import { CryptoProvider } from "../../crypto/CryptoProvider.js"; import { - API_VERSION_QUERY_PARAMETER_NAME, HttpMethod, ManagedIdentityEnvironmentVariableNames, ManagedIdentityIdType, ManagedIdentitySourceNames, - RESOURCE_BODY_OR_QUERY_PARAMETER_NAME, - ML_AND_SF_SECRET_HEADER_NAME, + ManagedIdentityQueryParameters, + ManagedIdentityHeaders, } from "../../utils/Constants.js"; -// MSI Constants. Docs for MSI are available here https://docs.microsoft.com/azure/app-service/overview-managed-identity const SERVICE_FABRIC_MSI_API_VERSION: string = "2019-07-01-preview"; /** @@ -29,6 +27,16 @@ export class ServiceFabric extends BaseManagedIdentitySource { private identityEndpoint: string; private identityHeader: string; + /** + * Constructs a new ServiceFabric managed identity source. + * @param logger Logger instance for logging + * @param nodeStorage NodeStorage instance for caching + * @param networkClient Network client for HTTP requests + * @param cryptoProvider Crypto provider for cryptographic operations + * @param disableInternalRetries Whether to disable internal retry logic + * @param identityEndpoint The Service Fabric managed identity endpoint + * @param identityHeader The Service Fabric managed identity secret header + */ constructor( logger: Logger, nodeStorage: NodeStorage, @@ -50,6 +58,10 @@ export class ServiceFabric extends BaseManagedIdentitySource { this.identityHeader = identityHeader; } + /** + * Retrieves the environment variables required for Service Fabric managed identity. + * @returns An array containing the identity endpoint, identity header, and identity server thumbprint. + */ public static getEnvironmentVariables(): Array { const identityEndpoint: string | undefined = process.env[ @@ -68,6 +80,16 @@ export class ServiceFabric extends BaseManagedIdentitySource { return [identityEndpoint, identityHeader, identityServerThumbprint]; } + /** + * Attempts to create a ServiceFabric managed identity source if all required environment variables are present. + * @param logger Logger instance for logging + * @param nodeStorage NodeStorage instance for caching + * @param networkClient Network client for HTTP requests + * @param cryptoProvider Crypto provider for cryptographic operations + * @param disableInternalRetries Whether to disable internal retry logic + * @param managedIdentityId Managed identity identifier + * @returns A ServiceFabric instance if environment variables are set, otherwise null + */ public static tryCreate( logger: Logger, nodeStorage: NodeStorage, @@ -79,10 +101,6 @@ export class ServiceFabric extends BaseManagedIdentitySource { const [identityEndpoint, identityHeader, identityServerThumbprint] = ServiceFabric.getEnvironmentVariables(); - /* - * if either of the identity endpoint, identity header, or identity server thumbprint - * environment variables are undefined, this MSI provider is unavailable. - */ if (!identityEndpoint || !identityHeader || !identityServerThumbprint) { logger.info( `[Managed Identity] ${ManagedIdentitySourceNames.SERVICE_FABRIC} managed identity is unavailable because one or all of the '${ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER}', '${ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT}' or '${ManagedIdentityEnvironmentVariableNames.IDENTITY_SERVER_THUMBPRINT}' environment variables are not defined.` @@ -121,6 +139,12 @@ export class ServiceFabric extends BaseManagedIdentitySource { ); } + /** + * Creates the request parameters for acquiring a token from the Service Fabric cluster. + * @param resource - The resource URI for which the token is requested. + * @param managedIdentityId - The managed identity ID (system-assigned or user-assigned). + * @returns A ManagedIdentityRequestParameters object configured for Service Fabric. + */ public createRequest( resource: string, managedIdentityId: ManagedIdentityId @@ -131,11 +155,12 @@ export class ServiceFabric extends BaseManagedIdentitySource { this.identityEndpoint ); - request.headers[ML_AND_SF_SECRET_HEADER_NAME] = this.identityHeader; + request.headers[ManagedIdentityHeaders.ML_AND_SF_SECRET_HEADER_NAME] = + this.identityHeader; - request.queryParameters[API_VERSION_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.API_VERSION] = SERVICE_FABRIC_MSI_API_VERSION; - request.queryParameters[RESOURCE_BODY_OR_QUERY_PARAMETER_NAME] = + request.queryParameters[ManagedIdentityQueryParameters.RESOURCE] = resource; if ( diff --git a/lib/msal-node/src/client/OnBehalfOfClient.ts b/lib/msal-node/src/client/OnBehalfOfClient.ts index a590b06f2a..f14d624730 100644 --- a/lib/msal-node/src/client/OnBehalfOfClient.ts +++ b/lib/msal-node/src/client/OnBehalfOfClient.ts @@ -129,7 +129,8 @@ export class OnBehalfOfClient extends BaseClient { // fetch the idToken from cache const cachedIdToken = this.readIdTokenFromCacheForOBO( - cachedAccessToken.homeAccountId + cachedAccessToken.homeAccountId, + request.correlationId ); let idTokenClaims: TokenClaims | undefined; let cachedAccount: AccountEntity | null = null; @@ -147,7 +148,10 @@ export class OnBehalfOfClient extends BaseClient { localAccountId: localAccountId || Constants.EMPTY_STRING, }; - cachedAccount = this.cacheManager.readAccountFromCache(accountInfo); + cachedAccount = this.cacheManager.readAccountFromCache( + accountInfo, + request.correlationId + ); } // increment telemetry cache hit counter @@ -177,7 +181,8 @@ export class OnBehalfOfClient extends BaseClient { * @param atHomeAccountId - account id */ private readIdTokenFromCacheForOBO( - atHomeAccountId: string + atHomeAccountId: string, + correlationId: string ): IdTokenEntity | null { const idTokenFilter: CredentialFilter = { homeAccountId: atHomeAccountId, @@ -189,7 +194,7 @@ export class OnBehalfOfClient extends BaseClient { }; const idTokenMap: Map = - this.cacheManager.getIdTokensByFilter(idTokenFilter); + this.cacheManager.getIdTokensByFilter(idTokenFilter, correlationId); // When acquiring a token on behalf of an application, there might not be an id token in the cache if (Object.values(idTokenMap).length < 1) { @@ -230,8 +235,10 @@ export class OnBehalfOfClient extends BaseClient { userAssertionHash: this.userAssertionHash, }; - const accessTokens = - this.cacheManager.getAccessTokensByFilter(accessTokenFilter); + const accessTokens = this.cacheManager.getAccessTokensByFilter( + accessTokenFilter, + request.correlationId + ); const numAccessTokens = accessTokens.length; if (numAccessTokens < 1) { diff --git a/lib/msal-node/src/client/PublicClientApplication.ts b/lib/msal-node/src/client/PublicClientApplication.ts index 1f81ec06fb..e2c7864d25 100644 --- a/lib/msal-node/src/client/PublicClientApplication.ts +++ b/lib/msal-node/src/client/PublicClientApplication.ts @@ -21,7 +21,7 @@ import { NativeSignOutRequest, AccountInfo, INativeBrokerPlugin, - ServerAuthorizationCodeResponse, + AuthorizeResponse, AADServerParamKeys, ServerTelemetryManager, } from "@azure/msal-common/node"; @@ -113,14 +113,18 @@ export class PublicClientApplication validRequest.correlationId ); try { - const deviceCodeConfig = await this.buildOauthClientConfiguration( + const discoveredAuthority = await this.createAuthority( validRequest.authority, validRequest.correlationId, - "", - serverTelemetryManager, undefined, request.azureCloudOptions ); + const deviceCodeConfig = await this.buildOauthClientConfiguration( + discoveredAuthority, + validRequest.correlationId, + "", + serverTelemetryManager + ); const deviceCodeClient = new DeviceCodeClient(deviceCodeConfig); this.logger.verbose( "Device code client created", @@ -181,7 +185,7 @@ export class PublicClientApplication const loopbackClient: ILoopbackClient = customLoopbackClient || new LoopbackClient(); - let authCodeResponse: ServerAuthorizationCodeResponse = {}; + let authCodeResponse: AuthorizeResponse = {}; let authCodeListenerError: AuthError | null = null; try { const authCodeListener = loopbackClient @@ -287,7 +291,10 @@ export class PublicClientApplication await this.nativeBrokerPlugin.signOut(signoutRequest); } - await this.getTokenCache().removeAccount(request.account); + await this.getTokenCache().removeAccount( + request.account, + request.correlationId + ); } /** diff --git a/lib/msal-node/src/client/UsernamePasswordClient.ts b/lib/msal-node/src/client/UsernamePasswordClient.ts index b9d12e13cd..fd1e7ff83d 100644 --- a/lib/msal-node/src/client/UsernamePasswordClient.ts +++ b/lib/msal-node/src/client/UsernamePasswordClient.ts @@ -13,6 +13,7 @@ import { CommonUsernamePasswordRequest, GrantType, NetworkResponse, + OAuthResponseType, RequestParameterBuilder, RequestThumbprint, ResponseHandler, @@ -130,7 +131,10 @@ export class UsernamePasswordClient extends BaseClient { RequestParameterBuilder.addScopes(parameters, request.scopes); - RequestParameterBuilder.addResponseTypeForTokenAndIdToken(parameters); + RequestParameterBuilder.addResponseType( + parameters, + OAuthResponseType.IDTOKEN_TOKEN + ); RequestParameterBuilder.addGrantType( parameters, diff --git a/lib/msal-node/src/config/Configuration.ts b/lib/msal-node/src/config/Configuration.ts index b7b7fa12ce..ffc1d3c634 100644 --- a/lib/msal-node/src/config/Configuration.ts +++ b/lib/msal-node/src/config/Configuration.ts @@ -31,6 +31,7 @@ import { NodeAuthError } from "../error/NodeAuthError.js"; * - clientCertificate - Certificate that the application uses when requesting a token. Only used in confidential client applications. Requires hex encoded X.509 SHA-1 or SHA-256 thumbprint of the certificate, and the PEM encoded private key (string should contain -----BEGIN PRIVATE KEY----- ... -----END PRIVATE KEY----- ) * - protocolMode - Enum that represents the protocol that msal follows. Used for configuring proper endpoints. * - skipAuthorityMetadataCache - A flag to choose whether to use or not use the local metadata cache during authority initialization. Defaults to false. + * - encodeExtraQueryParams - A flag to choose whether to encode extra query parameters in the request URL. Defaults to false. * @public */ export type NodeAuthOptions = { @@ -55,6 +56,10 @@ export type NodeAuthOptions = { protocolMode?: ProtocolMode; azureCloudOptions?: AzureCloudOptions; skipAuthorityMetadataCache?: boolean; + /** + * @deprecated This flag is deprecated and will be removed in the next major version where all extra query params will be encoded by default. + */ + encodeExtraQueryParams?: boolean; }; /** @@ -129,6 +134,7 @@ export type ManagedIdentityIdParams = { /** @public */ export type ManagedIdentityConfiguration = { + clientCapabilities?: Array; managedIdentityIdParams?: ManagedIdentityIdParams; system?: NodeSystemOptions; }; @@ -154,6 +160,7 @@ const DEFAULT_AUTH_OPTIONS: Required = { tenant: Constants.EMPTY_STRING, }, skipAuthorityMetadataCache: false, + encodeExtraQueryParams: false, }; const DEFAULT_CACHE_OPTIONS: CacheOptions = { @@ -240,14 +247,16 @@ export function buildAppConfiguration({ /** @internal */ export type ManagedIdentityNodeConfiguration = { + clientCapabilities?: Array; + disableInternalRetries: boolean; managedIdentityId: ManagedIdentityId; system: Required< Pick >; - disableInternalRetries: boolean; }; export function buildManagedIdentityConfiguration({ + clientCapabilities, managedIdentityIdParams, system, }: ManagedIdentityConfiguration): ManagedIdentityNodeConfiguration { @@ -271,6 +280,7 @@ export function buildManagedIdentityConfiguration({ } return { + clientCapabilities: clientCapabilities || [], managedIdentityId: managedIdentityId, system: { loggerOptions, diff --git a/lib/msal-node/src/config/ManagedIdentityId.ts b/lib/msal-node/src/config/ManagedIdentityId.ts index b67a05e8e3..2b0e3bed6d 100644 --- a/lib/msal-node/src/config/ManagedIdentityId.ts +++ b/lib/msal-node/src/config/ManagedIdentityId.ts @@ -11,7 +11,7 @@ import { DEFAULT_MANAGED_IDENTITY_ID, ManagedIdentityIdType, } from "../utils/Constants.js"; -import { ManagedIdentityIdParams } from "./Configuration.js"; +import type { ManagedIdentityIdParams } from "./Configuration.js"; export class ManagedIdentityId { private _id: string; diff --git a/lib/msal-node/src/config/ManagedIdentityRequestParameters.ts b/lib/msal-node/src/config/ManagedIdentityRequestParameters.ts index ebff30819c..93bdeb3e45 100644 --- a/lib/msal-node/src/config/ManagedIdentityRequestParameters.ts +++ b/lib/msal-node/src/config/ManagedIdentityRequestParameters.ts @@ -8,14 +8,8 @@ import { UrlString, UrlUtils, } from "@azure/msal-common/node"; -import { - HttpMethod, - MANAGED_IDENTITY_HTTP_STATUS_CODES_TO_RETRY_ON, - MANAGED_IDENTITY_MAX_RETRIES, - MANAGED_IDENTITY_RETRY_DELAY, - RetryPolicies, -} from "../utils/Constants.js"; -import { LinearRetryPolicy } from "../retry/LinearRetryPolicy.js"; +import { DefaultManagedIdentityRetryPolicy } from "../retry/DefaultManagedIdentityRetryPolicy.js"; +import { HttpMethod, RetryPolicies } from "../utils/Constants.js"; export class ManagedIdentityRequestParameters { private _baseEndpoint: string; @@ -36,12 +30,8 @@ export class ManagedIdentityRequestParameters { this.bodyParameters = {} as Record; this.queryParameters = {} as Record; - const defaultRetryPolicy: LinearRetryPolicy = new LinearRetryPolicy( - MANAGED_IDENTITY_MAX_RETRIES, - MANAGED_IDENTITY_RETRY_DELAY, - MANAGED_IDENTITY_HTTP_STATUS_CODES_TO_RETRY_ON - ); - this.retryPolicy = retryPolicy || defaultRetryPolicy; + this.retryPolicy = + retryPolicy || new DefaultManagedIdentityRetryPolicy(); } public computeUri(): string { diff --git a/lib/msal-node/src/crypto/CryptoProvider.ts b/lib/msal-node/src/crypto/CryptoProvider.ts index c3359b3fbe..d6e06d99e1 100644 --- a/lib/msal-node/src/crypto/CryptoProvider.ts +++ b/lib/msal-node/src/crypto/CryptoProvider.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { ICrypto, PkceCodes } from "@azure/msal-common/node"; +import { EncodingTypes, ICrypto, PkceCodes } from "@azure/msal-common/node"; import { GuidGenerator } from "./GuidGenerator.js"; import { EncodingUtils } from "../utils/EncodingUtils.js"; import { PkceGenerator } from "./PkceGenerator.js"; @@ -83,7 +83,7 @@ export class CryptoProvider implements ICrypto { * Removes cryptographic keypair from key store matching the keyId passed in * @param kid - public key id */ - removeTokenBindingKey(): Promise { + removeTokenBindingKey(): Promise { throw new Error("Method not implemented."); } @@ -106,8 +106,8 @@ export class CryptoProvider implements ICrypto { */ async hashString(plainText: string): Promise { return EncodingUtils.base64EncodeUrl( - this.hashUtils.sha256(plainText).toString("base64"), - "base64" + this.hashUtils.sha256(plainText).toString(EncodingTypes.BASE64), + EncodingTypes.BASE64 ); } } diff --git a/lib/msal-node/src/crypto/PkceGenerator.ts b/lib/msal-node/src/crypto/PkceGenerator.ts index 8c36a6a574..9c74ce2295 100644 --- a/lib/msal-node/src/crypto/PkceGenerator.ts +++ b/lib/msal-node/src/crypto/PkceGenerator.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { Constants, PkceCodes } from "@azure/msal-common/node"; +import { Constants, EncodingTypes, PkceCodes } from "@azure/msal-common/node"; import { CharSet, RANDOM_OCTET_SIZE } from "../utils/Constants.js"; import { EncodingUtils } from "../utils/EncodingUtils.js"; import { HashUtils } from "./HashUtils.js"; @@ -56,8 +56,8 @@ export class PkceGenerator { */ private generateCodeChallengeFromVerifier(codeVerifier: string): string { return EncodingUtils.base64EncodeUrl( - this.hashUtils.sha256(codeVerifier).toString("base64"), - "base64" + this.hashUtils.sha256(codeVerifier).toString(EncodingTypes.BASE64), + EncodingTypes.BASE64 ); } } diff --git a/lib/msal-node/src/index.ts b/lib/msal-node/src/index.ts index 04418ea898..a8f134838c 100644 --- a/lib/msal-node/src/index.ts +++ b/lib/msal-node/src/index.ts @@ -12,7 +12,6 @@ * Warning: This set of exports is purely intended to be used by other MSAL libraries, and should be considered potentially unstable. We strongly discourage using them directly, you do so at your own risk. * Breaking changes to these APIs will be shipped under a minor version, instead of a major version. */ - import * as internals from "./internals.js"; export { internals }; @@ -87,7 +86,11 @@ export { AuthorizationCodePayload, // Response AuthenticationResult, - ServerAuthorizationCodeResponse, + AuthorizeResponse, + /** + * @deprecated Use AuthorizeResponse instead + */ + AuthorizeResponse as ServerAuthorizationCodeResponse, IdTokenClaims, // Cache AccountInfo, diff --git a/lib/msal-node/src/network/HttpClientWithRetries.ts b/lib/msal-node/src/network/HttpClientWithRetries.ts index 61db37ec7d..128654a9b7 100644 --- a/lib/msal-node/src/network/HttpClientWithRetries.ts +++ b/lib/msal-node/src/network/HttpClientWithRetries.ts @@ -6,6 +6,7 @@ import { HeaderNames, INetworkModule, + Logger, NetworkRequestOptions, NetworkResponse, } from "@azure/msal-common/node"; @@ -15,13 +16,16 @@ import { HttpMethod } from "../utils/Constants.js"; export class HttpClientWithRetries implements INetworkModule { private httpClientNoRetries: INetworkModule; private retryPolicy: IHttpRetryPolicy; + private logger: Logger; constructor( httpClientNoRetries: INetworkModule, - retryPolicy: IHttpRetryPolicy + retryPolicy: IHttpRetryPolicy, + logger: Logger ) { this.httpClientNoRetries = httpClientNoRetries; this.retryPolicy = retryPolicy; + this.logger = logger; } private async sendNetworkRequestAsyncHelper( @@ -45,11 +49,16 @@ export class HttpClientWithRetries implements INetworkModule { let response: NetworkResponse = await this.sendNetworkRequestAsyncHelper(httpMethod, url, options); + if ("isNewRequest" in this.retryPolicy) { + this.retryPolicy.isNewRequest = true; + } + let currentRetry: number = 0; while ( await this.retryPolicy.pauseForRetry( response.status, currentRetry, + this.logger, response.headers[HeaderNames.RETRY_AFTER] ) ) { diff --git a/lib/msal-node/src/network/ILoopbackClient.ts b/lib/msal-node/src/network/ILoopbackClient.ts index 2933b787d3..31971e5870 100644 --- a/lib/msal-node/src/network/ILoopbackClient.ts +++ b/lib/msal-node/src/network/ILoopbackClient.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { ServerAuthorizationCodeResponse } from "@azure/msal-common/node"; +import { AuthorizeResponse } from "@azure/msal-common/node"; /** * Interface for LoopbackClient allowing to replace the default loopback server with a custom implementation. @@ -13,7 +13,7 @@ export interface ILoopbackClient { listenForAuthCode( successTemplate?: string, errorTemplate?: string - ): Promise; + ): Promise; getRedirectUri(): string; closeServer(): void; } diff --git a/lib/msal-node/src/network/LoopbackClient.ts b/lib/msal-node/src/network/LoopbackClient.ts index fc6877a56c..52d3277b49 100644 --- a/lib/msal-node/src/network/LoopbackClient.ts +++ b/lib/msal-node/src/network/LoopbackClient.ts @@ -5,7 +5,7 @@ import { Constants as CommonConstants, - ServerAuthorizationCodeResponse, + AuthorizeResponse, HttpStatus, UrlUtils, } from "@azure/msal-common/node"; @@ -26,57 +26,54 @@ export class LoopbackClient implements ILoopbackClient { async listenForAuthCode( successTemplate?: string, errorTemplate?: string - ): Promise { + ): Promise { if (this.server) { throw NodeAuthError.createLoopbackServerAlreadyExistsError(); } - return new Promise( - (resolve, reject) => { - this.server = http.createServer( - (req: http.IncomingMessage, res: http.ServerResponse) => { - const url = req.url; - if (!url) { - res.end( - errorTemplate || - "Error occurred loading redirectUrl" - ); - reject( - NodeAuthError.createUnableToLoadRedirectUrlError() - ); - return; - } else if (url === CommonConstants.FORWARD_SLASH) { - res.end( - successTemplate || - "Auth code was successfully acquired. You can close this window now." - ); - return; - } + return new Promise((resolve, reject) => { + this.server = http.createServer( + (req: http.IncomingMessage, res: http.ServerResponse) => { + const url = req.url; + if (!url) { + res.end( + errorTemplate || + "Error occurred loading redirectUrl" + ); + reject( + NodeAuthError.createUnableToLoadRedirectUrlError() + ); + return; + } else if (url === CommonConstants.FORWARD_SLASH) { + res.end( + successTemplate || + "Auth code was successfully acquired. You can close this window now." + ); + return; + } - const redirectUri = this.getRedirectUri(); - const parsedUrl = new URL(url, redirectUri); - const authCodeResponse = - UrlUtils.getDeserializedResponse( - parsedUrl.search - ) || {}; - if (authCodeResponse.code) { - res.writeHead(HttpStatus.REDIRECT, { - location: redirectUri, - }); // Prevent auth code from being saved in the browser history - res.end(); - } - if (authCodeResponse.error) { - res.end( - errorTemplate || - `Error occurred: ${authCodeResponse.error}` - ); - } - resolve(authCodeResponse); + const redirectUri = this.getRedirectUri(); + const parsedUrl = new URL(url, redirectUri); + const authCodeResponse = + UrlUtils.getDeserializedResponse(parsedUrl.search) || + {}; + if (authCodeResponse.code) { + res.writeHead(HttpStatus.REDIRECT, { + location: redirectUri, + }); // Prevent auth code from being saved in the browser history + res.end(); } - ); - this.server.listen(0, "127.0.0.1"); // Listen on any available port - } - ); + if (authCodeResponse.error) { + res.end( + errorTemplate || + `Error occurred: ${authCodeResponse.error}` + ); + } + resolve(authCodeResponse); + } + ); + this.server.listen(0, "127.0.0.1"); // Listen on any available port + }); } /** diff --git a/lib/msal-node/src/packageMetadata.ts b/lib/msal-node/src/packageMetadata.ts index f3e9a1aaf7..ca3ec9d2ff 100644 --- a/lib/msal-node/src/packageMetadata.ts +++ b/lib/msal-node/src/packageMetadata.ts @@ -1,3 +1,3 @@ /* eslint-disable header/header */ export const name = "@azure/msal-node"; -export const version = "3.3.0"; +export const version = "3.6.3"; diff --git a/lib/msal-node/src/protocol/Authorize.ts b/lib/msal-node/src/protocol/Authorize.ts new file mode 100644 index 0000000000..5d69e70c59 --- /dev/null +++ b/lib/msal-node/src/protocol/Authorize.ts @@ -0,0 +1,74 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { + Authority, + AuthorizeProtocol, + CommonAuthorizationUrlRequest, + Logger, + OAuthResponseType, + ProtocolMode, + RequestParameterBuilder, +} from "@azure/msal-common/node"; +import { NodeConfiguration } from "../config/Configuration.js"; +import { Constants as NodeConstants } from "../utils/Constants.js"; +import { version } from "../packageMetadata.js"; + +/** + * Constructs the full /authorize URL with request parameters + * @param config + * @param authority + * @param request + * @param logger + * @returns + */ +export function getAuthCodeRequestUrl( + config: NodeConfiguration, + authority: Authority, + request: CommonAuthorizationUrlRequest, + logger: Logger +): string { + const parameters = AuthorizeProtocol.getStandardAuthorizeRequestParameters( + { + ...config.auth, + authority: authority, + redirectUri: request.redirectUri || "", + }, + request, + logger + ); + RequestParameterBuilder.addLibraryInfo(parameters, { + sku: NodeConstants.MSAL_SKU, + version: version, + cpu: process.arch || "", + os: process.platform || "", + }); + if (config.auth.protocolMode !== ProtocolMode.OIDC) { + RequestParameterBuilder.addApplicationTelemetry( + parameters, + config.telemetry.application + ); + } + RequestParameterBuilder.addResponseType(parameters, OAuthResponseType.CODE); + if (request.codeChallenge && request.codeChallengeMethod) { + RequestParameterBuilder.addCodeChallengeParams( + parameters, + request.codeChallenge, + request.codeChallengeMethod + ); + } + + RequestParameterBuilder.addExtraQueryParameters( + parameters, + request.extraQueryParameters || {} + ); + + return AuthorizeProtocol.getAuthorizeUrl( + authority, + parameters, + config.auth.encodeExtraQueryParams, + request.extraQueryParameters + ); +} diff --git a/lib/msal-node/src/request/ManagedIdentityRequest.ts b/lib/msal-node/src/request/ManagedIdentityRequest.ts index a99ae12b3a..bac42e7b30 100644 --- a/lib/msal-node/src/request/ManagedIdentityRequest.ts +++ b/lib/msal-node/src/request/ManagedIdentityRequest.ts @@ -8,8 +8,11 @@ import { ManagedIdentityRequestParams } from "./ManagedIdentityRequestParams.js" /** * ManagedIdentityRequest - * - forceRefresh - forces managed identity requests to skip the cache and make network calls if true - * - resource - resource requested to access the protected API. It should be of the form "{ResourceIdUri}" or {ResourceIdUri/.default}. For instance https://management.azure.net or, for Microsoft Graph, https://graph.microsoft.com/.default + * - clientCapabilities - an array of capabilities to be added to all network requests as part of the `xms_cc` claim + * - revokedTokenSha256Hash - a SHA256 hash of the token that was revoked. The managed identity will revoke the token based on the SHA256 hash of the token, not the token itself. This is to prevent the token from being leaked in transit. */ export type ManagedIdentityRequest = ManagedIdentityRequestParams & - CommonClientCredentialRequest; + CommonClientCredentialRequest & { + clientCapabilities?: Array; + revokedTokenSha256Hash?: string; + }; diff --git a/lib/msal-node/src/retry/DefaultManagedIdentityRetryPolicy.ts b/lib/msal-node/src/retry/DefaultManagedIdentityRetryPolicy.ts new file mode 100644 index 0000000000..2688fd4d0e --- /dev/null +++ b/lib/msal-node/src/retry/DefaultManagedIdentityRetryPolicy.ts @@ -0,0 +1,70 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { IncomingHttpHeaders } from "http"; +import { HttpStatus, Logger } from "@azure/msal-common"; +import { IHttpRetryPolicy } from "./IHttpRetryPolicy.js"; +import { LinearRetryStrategy } from "./LinearRetryStrategy.js"; + +export const DEFAULT_MANAGED_IDENTITY_MAX_RETRIES: number = 3; // referenced in unit test +const DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS: number = 1000; +const DEFAULT_MANAGED_IDENTITY_HTTP_STATUS_CODES_TO_RETRY_ON: Array = [ + HttpStatus.NOT_FOUND, + HttpStatus.REQUEST_TIMEOUT, + HttpStatus.TOO_MANY_REQUESTS, + HttpStatus.SERVER_ERROR, + HttpStatus.SERVICE_UNAVAILABLE, + HttpStatus.GATEWAY_TIMEOUT, +]; + +export class DefaultManagedIdentityRetryPolicy implements IHttpRetryPolicy { + /* + * this is defined here as a static variable despite being defined as a constant outside of the + * class because it needs to be overridden in the unit tests so that the unit tests run faster + */ + static get DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS(): number { + return DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS; + } + + private linearRetryStrategy: LinearRetryStrategy = + new LinearRetryStrategy(); + + async pauseForRetry( + httpStatusCode: number, + currentRetry: number, + logger: Logger, + retryAfterHeader: IncomingHttpHeaders["retry-after"] + ): Promise { + if ( + DEFAULT_MANAGED_IDENTITY_HTTP_STATUS_CODES_TO_RETRY_ON.includes( + httpStatusCode + ) && + currentRetry < DEFAULT_MANAGED_IDENTITY_MAX_RETRIES + ) { + const retryAfterDelay: number = + this.linearRetryStrategy.calculateDelay( + retryAfterHeader, + DefaultManagedIdentityRetryPolicy.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS + ); + + logger.verbose( + `Retrying request in ${retryAfterDelay}ms (retry attempt: ${ + currentRetry + 1 + })` + ); + + // pause execution for the calculated delay + await new Promise((resolve) => { + // retryAfterHeader value of 0 evaluates to false, and DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS will be used + return setTimeout(resolve, retryAfterDelay); + }); + + return true; + } + + // if the status code is not retriable or max retries have been reached, do not retry + return false; + } +} diff --git a/lib/msal-node/src/retry/ExponentialRetryStrategy.ts b/lib/msal-node/src/retry/ExponentialRetryStrategy.ts new file mode 100644 index 0000000000..77d3915fba --- /dev/null +++ b/lib/msal-node/src/retry/ExponentialRetryStrategy.ts @@ -0,0 +1,53 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +export class ExponentialRetryStrategy { + // Minimum backoff time in milliseconds + private minExponentialBackoff: number; + // Maximum backoff time in milliseconds + private maxExponentialBackoff: number; + // Maximum backoff time in milliseconds + private exponentialDeltaBackoff: number; + + constructor( + minExponentialBackoff: number, + maxExponentialBackoff: number, + exponentialDeltaBackoff: number + ) { + this.minExponentialBackoff = minExponentialBackoff; + this.maxExponentialBackoff = maxExponentialBackoff; + this.exponentialDeltaBackoff = exponentialDeltaBackoff; + } + + /** + * Calculates the exponential delay based on the current retry attempt. + * + * @param {number} currentRetry - The current retry attempt number. + * @returns {number} - The calculated exponential delay in milliseconds. + * + * The delay is calculated using the formula: + * - If `currentRetry` is 0, it returns the minimum backoff time. + * - Otherwise, it calculates the delay as the minimum of: + * - `(2^(currentRetry - 1)) * deltaBackoff` + * - `maxBackoff` + * + * This ensures that the delay increases exponentially with each retry attempt, + * but does not exceed the maximum backoff time. + */ + public calculateDelay(currentRetry: number): number { + // Attempt 1 + if (currentRetry === 0) { + return this.minExponentialBackoff; + } + + // Attempt 2+ + const exponentialDelay = Math.min( + Math.pow(2, currentRetry - 1) * this.exponentialDeltaBackoff, + this.maxExponentialBackoff + ); + + return exponentialDelay; + } +} diff --git a/lib/msal-node/src/retry/IHttpRetryPolicy.ts b/lib/msal-node/src/retry/IHttpRetryPolicy.ts index 2701b5bd9b..8a8ef06594 100644 --- a/lib/msal-node/src/retry/IHttpRetryPolicy.ts +++ b/lib/msal-node/src/retry/IHttpRetryPolicy.ts @@ -3,16 +3,25 @@ * Licensed under the MIT License. */ -import http from "http"; +import { IncomingHttpHeaders } from "http"; +import { Logger } from "@azure/msal-common"; export interface IHttpRetryPolicy { - /* - * if retry conditions occur, pauses and returns true - * otherwise return false + _isNewRequest?: boolean; + // set isNewRequest(value: boolean); + + /** + * Pauses execution for a specified amount of time before retrying an HTTP request. + * + * @param httpStatusCode - The HTTP status code of the response. + * @param currentRetry - The current retry attempt number. + * @param retryAfterHeader - The value of the `retry-after` HTTP header, if present. + * @returns A promise that resolves to a boolean indicating whether to retry the request. */ pauseForRetry( httpStatusCode: number, currentRetry: number, - retryAfterHeader: http.IncomingHttpHeaders["retry-after"] + logger: Logger, + retryAfterHeader?: IncomingHttpHeaders["retry-after"] ): Promise; } diff --git a/lib/msal-node/src/retry/ImdsRetryPolicy.ts b/lib/msal-node/src/retry/ImdsRetryPolicy.ts new file mode 100644 index 0000000000..d7e819516c --- /dev/null +++ b/lib/msal-node/src/retry/ImdsRetryPolicy.ts @@ -0,0 +1,121 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { HttpStatus, Logger } from "@azure/msal-common"; +import { ExponentialRetryStrategy } from "./ExponentialRetryStrategy.js"; +import { IHttpRetryPolicy } from "./IHttpRetryPolicy.js"; + +const HTTP_STATUS_400_CODES_FOR_EXPONENTIAL_STRATEGY: Array = [ + HttpStatus.NOT_FOUND, + HttpStatus.REQUEST_TIMEOUT, + HttpStatus.GONE, + HttpStatus.TOO_MANY_REQUESTS, +]; + +const EXPONENTIAL_STRATEGY_NUM_RETRIES = 3; +const LINEAR_STRATEGY_NUM_RETRIES = 7; + +const MIN_EXPONENTIAL_BACKOFF_MS: number = 1000; +const MAX_EXPONENTIAL_BACKOFF_MS: number = 4000; +const EXPONENTIAL_DELTA_BACKOFF_MS: number = 2000; + +const HTTP_STATUS_GONE_RETRY_AFTER_MS: number = 10 * 1000; // 10 seconds + +export class ImdsRetryPolicy implements IHttpRetryPolicy { + /* + * these are defined here as static variables despite being defined as constants outside of the + * class because they need to be overridden in the unit tests so that the unit tests run faster + */ + static get MIN_EXPONENTIAL_BACKOFF_MS(): number { + return MIN_EXPONENTIAL_BACKOFF_MS; + } + static get MAX_EXPONENTIAL_BACKOFF_MS(): number { + return MAX_EXPONENTIAL_BACKOFF_MS; + } + static get EXPONENTIAL_DELTA_BACKOFF_MS(): number { + return EXPONENTIAL_DELTA_BACKOFF_MS; + } + static get HTTP_STATUS_GONE_RETRY_AFTER_MS(): number { + return HTTP_STATUS_GONE_RETRY_AFTER_MS; + } + + public _isNewRequest: boolean; + set isNewRequest(value: boolean) { + this._isNewRequest = value; + } + + private maxRetries: number; + + private exponentialRetryStrategy: ExponentialRetryStrategy = + new ExponentialRetryStrategy( + ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS, + ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS, + ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS + ); + + /** + * Pauses execution for a calculated delay before retrying a request. + * + * @param httpStatusCode - The HTTP status code of the response. + * @param currentRetry - The current retry attempt number. + * @param retryAfterHeader - The value of the "retry-after" header from the response. + * @returns A promise that resolves to a boolean indicating whether a retry should be attempted. + */ + async pauseForRetry( + httpStatusCode: number, + currentRetry: number, + logger: Logger + ): Promise { + if (this._isNewRequest) { + this._isNewRequest = false; + + // calculate the maxRetries based on the status code, once per request + this.maxRetries = + httpStatusCode === HttpStatus.GONE + ? LINEAR_STRATEGY_NUM_RETRIES + : EXPONENTIAL_STRATEGY_NUM_RETRIES; + } + + /** + * (status code is one of the retriable 400 status code + * or + * status code is >= 500 and <= 599) + * and + * current count of retries is less than the max number of retries + */ + if ( + (HTTP_STATUS_400_CODES_FOR_EXPONENTIAL_STRATEGY.includes( + httpStatusCode + ) || + (httpStatusCode >= HttpStatus.SERVER_ERROR_RANGE_START && + httpStatusCode <= HttpStatus.SERVER_ERROR_RANGE_END && + currentRetry < this.maxRetries)) && + currentRetry < this.maxRetries + ) { + const retryAfterDelay: number = + httpStatusCode === HttpStatus.GONE + ? ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS + : this.exponentialRetryStrategy.calculateDelay( + currentRetry + ); + + logger.verbose( + `Retrying request in ${retryAfterDelay}ms (retry attempt: ${ + currentRetry + 1 + })` + ); + + // pause execution for the calculated delay + await new Promise((resolve) => { + return setTimeout(resolve, retryAfterDelay); + }); + + return true; + } + + // if the status code is not retriable or max retries have been reached, do not retry + return false; + } +} diff --git a/lib/msal-node/src/retry/LinearRetryPolicy.ts b/lib/msal-node/src/retry/LinearRetryPolicy.ts deleted file mode 100644 index cabbd6d4f1..0000000000 --- a/lib/msal-node/src/retry/LinearRetryPolicy.ts +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import http from "http"; -import { IHttpRetryPolicy } from "./IHttpRetryPolicy.js"; - -export class LinearRetryPolicy implements IHttpRetryPolicy { - maxRetries: number; - retryDelay: number; - httpStatusCodesToRetryOn: Array; - - constructor( - maxRetries: number, - retryDelay: number, - httpStatusCodesToRetryOn: Array - ) { - this.maxRetries = maxRetries; - this.retryDelay = retryDelay; - this.httpStatusCodesToRetryOn = httpStatusCodesToRetryOn; - } - - private retryAfterMillisecondsToSleep( - retryHeader: http.IncomingHttpHeaders["retry-after"] - ): number { - if (!retryHeader) { - return 0; - } - - // retry-after header is in seconds - let millisToSleep = Math.round(parseFloat(retryHeader) * 1000); - - /* - * retry-after header is in HTTP Date format - * , :: GMT - */ - if (isNaN(millisToSleep)) { - millisToSleep = Math.max( - 0, - // .valueOf() is needed to subtract dates in TypeScript - new Date(retryHeader).valueOf() - new Date().valueOf() - ); - } - - return millisToSleep; - } - - async pauseForRetry( - httpStatusCode: number, - currentRetry: number, - retryAfterHeader: http.IncomingHttpHeaders["retry-after"] - ): Promise { - if ( - this.httpStatusCodesToRetryOn.includes(httpStatusCode) && - currentRetry < this.maxRetries - ) { - const retryAfterDelay: number = - this.retryAfterMillisecondsToSleep(retryAfterHeader); - - await new Promise((resolve) => { - // retryAfterHeader value of 0 evaluates to false, and this.retryDelay will be used - return setTimeout(resolve, retryAfterDelay || this.retryDelay); - }); - - return true; - } - - return false; - } -} diff --git a/lib/msal-node/src/retry/LinearRetryStrategy.ts b/lib/msal-node/src/retry/LinearRetryStrategy.ts new file mode 100644 index 0000000000..47ad130dc5 --- /dev/null +++ b/lib/msal-node/src/retry/LinearRetryStrategy.ts @@ -0,0 +1,40 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { IncomingHttpHeaders } from "http"; + +export class LinearRetryStrategy { + /** + * Calculates the number of milliseconds to sleep based on the `retry-after` HTTP header. + * + * @param retryHeader - The value of the `retry-after` HTTP header. This can be either a number of seconds + * or an HTTP date string. + * @returns The number of milliseconds to sleep before retrying the request. If the `retry-after` header is not + * present or cannot be parsed, returns 0. + */ + public calculateDelay( + retryHeader: IncomingHttpHeaders["retry-after"], + minimumDelay: number + ): number { + if (!retryHeader) { + return minimumDelay; + } + + // retry-after header is in seconds + let millisToSleep = Math.round(parseFloat(retryHeader) * 1000); + + /* + * retry-after header is in HTTP Date format + * , :: GMT + */ + if (isNaN(millisToSleep)) { + // .valueOf() is needed to subtract dates in TypeScript + millisToSleep = + new Date(retryHeader).valueOf() - new Date().valueOf(); + } + + return Math.max(minimumDelay, millisToSleep); + } +} diff --git a/lib/msal-node/src/utils/Constants.ts b/lib/msal-node/src/utils/Constants.ts index f71344125e..c539567fe6 100644 --- a/lib/msal-node/src/utils/Constants.ts +++ b/lib/msal-node/src/utils/Constants.ts @@ -4,24 +4,44 @@ */ import { HttpStatus } from "@azure/msal-common/node"; -import { LinearRetryPolicy } from "../retry/LinearRetryPolicy.js"; +import { DefaultManagedIdentityRetryPolicy } from "../retry/DefaultManagedIdentityRetryPolicy.js"; +import { ImdsRetryPolicy } from "../retry/ImdsRetryPolicy.js"; // MSI Constants. Docs for MSI are available here https://docs.microsoft.com/azure/app-service/overview-managed-identity -export const AUTHORIZATION_HEADER_NAME: string = "Authorization"; -export const METADATA_HEADER_NAME: string = "Metadata"; -export const APP_SERVICE_SECRET_HEADER_NAME: string = "X-IDENTITY-HEADER"; -export const ML_AND_SF_SECRET_HEADER_NAME: string = "secret"; -export const API_VERSION_QUERY_PARAMETER_NAME: string = "api-version"; -export const RESOURCE_BODY_OR_QUERY_PARAMETER_NAME: string = "resource"; export const DEFAULT_MANAGED_IDENTITY_ID = "system_assigned_managed_identity"; export const MANAGED_IDENTITY_DEFAULT_TENANT = "managed_identity"; export const DEFAULT_AUTHORITY_FOR_MANAGED_IDENTITY = `https://login.microsoftonline.com/${MANAGED_IDENTITY_DEFAULT_TENANT}/`; +/** + * Managed Identity Headers - used in network requests + */ +export const ManagedIdentityHeaders = { + AUTHORIZATION_HEADER_NAME: "Authorization", + METADATA_HEADER_NAME: "Metadata", + APP_SERVICE_SECRET_HEADER_NAME: "X-IDENTITY-HEADER", + ML_AND_SF_SECRET_HEADER_NAME: "secret", +} as const; +export type ManagedIdentityHeaders = + (typeof ManagedIdentityHeaders)[keyof typeof ManagedIdentityHeaders]; + +/** + * Managed Identity Query Parameters - used in network requests + */ +export const ManagedIdentityQueryParameters = { + API_VERSION: "api-version", + RESOURCE: "resource", + SHA256_TOKEN_TO_REFRESH: "token_sha256_to_refresh", + XMS_CC: "xms_cc", +} as const; +export type ManagedIdentityQueryParameters = + (typeof ManagedIdentityQueryParameters)[keyof typeof ManagedIdentityQueryParameters]; + /** * Managed Identity Environment Variable Names */ export const ManagedIdentityEnvironmentVariableNames = { AZURE_POD_IDENTITY_AUTHORITY_HOST: "AZURE_POD_IDENTITY_AUTHORITY_HOST", + DEFAULT_IDENTITY_CLIENT_ID: "DEFAULT_IDENTITY_CLIENT_ID", IDENTITY_ENDPOINT: "IDENTITY_ENDPOINT", IDENTITY_HEADER: "IDENTITY_HEADER", IDENTITY_SERVER_THUMBPRINT: "IDENTITY_SERVER_THUMBPRINT", @@ -167,20 +187,6 @@ export const LOOPBACK_SERVER_CONSTANTS = { TIMEOUT_MS: 5000, }; -export const AZURE_ARC_SECRET_FILE_MAX_SIZE_BYTES = 4096; // 4 KB - -export const MANAGED_IDENTITY_MAX_RETRIES = 3; -export const MANAGED_IDENTITY_RETRY_DELAY = 1000; -export const MANAGED_IDENTITY_HTTP_STATUS_CODES_TO_RETRY_ON = [ - HttpStatus.NOT_FOUND, - HttpStatus.REQUEST_TIMEOUT, - HttpStatus.TOO_MANY_REQUESTS, - HttpStatus.SERVER_ERROR, - HttpStatus.SERVICE_UNAVAILABLE, - HttpStatus.GATEWAY_TIMEOUT, -]; +export const AZURE_ARC_SECRET_FILE_MAX_SIZE_BYTES: number = 4096; // 4 KB -/** - * Retry Policy Types - */ -export type RetryPolicies = LinearRetryPolicy; +export type RetryPolicies = DefaultManagedIdentityRetryPolicy | ImdsRetryPolicy; diff --git a/lib/msal-node/src/utils/EncodingUtils.ts b/lib/msal-node/src/utils/EncodingUtils.ts index 3ee03a3747..eab750e386 100644 --- a/lib/msal-node/src/utils/EncodingUtils.ts +++ b/lib/msal-node/src/utils/EncodingUtils.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ -import { Constants } from "@azure/msal-common/node"; +import { Constants, EncodingTypes } from "@azure/msal-common/node"; export class EncodingUtils { /** @@ -13,7 +13,7 @@ export class EncodingUtils { * @param str text */ static base64Encode(str: string, encoding?: BufferEncoding): string { - return Buffer.from(str, encoding).toString("base64"); + return Buffer.from(str, encoding).toString(EncodingTypes.BASE64); } /** @@ -34,7 +34,7 @@ export class EncodingUtils { * @param base64Str Base64 encoded text */ static base64Decode(base64Str: string): string { - return Buffer.from(base64Str, "base64").toString("utf8"); + return Buffer.from(base64Str, EncodingTypes.BASE64).toString("utf8"); } /** diff --git a/lib/msal-node/test/cache/TokenCache.spec.ts b/lib/msal-node/test/cache/TokenCache.spec.ts index 607302b5d0..4f02d37973 100644 --- a/lib/msal-node/test/cache/TokenCache.spec.ts +++ b/lib/msal-node/test/cache/TokenCache.spec.ts @@ -9,6 +9,7 @@ import { TokenCacheContext, ICachePlugin, buildStaticAuthorityOptions, + EncodingTypes, } from "@azure/msal-common"; import { NodeStorage } from "../../src/cache/NodeStorage.js"; import { TokenCache } from "../../src/cache/TokenCache.js"; @@ -129,7 +130,7 @@ describe("TokenCache tests", () => { context.tokenCache.deserialize( await promises.readFile( "./test/cache/cache-test-files/cache-unrecognized-entities.json", - "utf-8" + EncodingTypes.UTF8 ) ); }; @@ -194,9 +195,11 @@ describe("TokenCache tests", () => { if (context.hasChanged == true) { throw new Error("hasChanged should be false"); } - return promises.readFile(cachePath, "utf-8").then((data) => { - context.tokenCache.deserialize(data); - }); + return promises + .readFile(cachePath, EncodingTypes.UTF8) + .then((data) => { + context.tokenCache.deserialize(data); + }); } ); @@ -251,7 +254,7 @@ describe("TokenCache tests", () => { const cachePath = "./test/cache/cache-test-files/default-cache.json"; const beforeCacheAccess = async (context: TokenCacheContext) => { context.tokenCache.deserialize( - await promises.readFile(cachePath, "utf-8") + await promises.readFile(cachePath, EncodingTypes.UTF8) ); }; const afterCacheAccess = async (context: TokenCacheContext) => { diff --git a/lib/msal-node/test/client/ClientAssertion.spec.ts b/lib/msal-node/test/client/ClientAssertion.spec.ts index 6adf8268a8..042939de0a 100644 --- a/lib/msal-node/test/client/ClientAssertion.spec.ts +++ b/lib/msal-node/test/client/ClientAssertion.spec.ts @@ -12,7 +12,7 @@ import { CryptoProvider } from "../../src/crypto/CryptoProvider.js"; import { EncodingUtils } from "../../src/utils/EncodingUtils.js"; import { JwtConstants } from "../../src/utils/Constants.js"; import { getClientAssertionCallback } from "./ClientTestUtils.js"; -import { getClientAssertion } from "@azure/msal-common"; +import { EncodingTypes, getClientAssertion } from "@azure/msal-common"; import jwt from "jsonwebtoken"; jest.mock("jsonwebtoken"); @@ -65,7 +65,7 @@ describe("Client assertion test", () => { [JwtConstants.ALGORITHM]: JwtConstants.RSA_256, [JwtConstants.X5T]: EncodingUtils.base64EncodeUrl( TEST_CONSTANTS.THUMBPRINT, - "hex" + EncodingTypes.HEX ), }, }; @@ -95,7 +95,7 @@ describe("Client assertion test", () => { [JwtConstants.ALGORITHM]: JwtConstants.PSS_256, [JwtConstants.X5T_256]: EncodingUtils.base64EncodeUrl( TEST_CONSTANTS.THUMBPRINT256, - "hex" + EncodingTypes.HEX ), }, }; @@ -125,7 +125,7 @@ describe("Client assertion test", () => { [JwtConstants.ALGORITHM]: JwtConstants.RSA_256, [JwtConstants.X5T]: EncodingUtils.base64EncodeUrl( TEST_CONSTANTS.THUMBPRINT, - "hex" + EncodingTypes.HEX ), [JwtConstants.X5C]: TEST_CONSTANTS.X5C_FROM_PUBLIC_CERTIFICATE, }, @@ -157,7 +157,7 @@ describe("Client assertion test", () => { [JwtConstants.ALGORITHM]: JwtConstants.PSS_256, [JwtConstants.X5T_256]: EncodingUtils.base64EncodeUrl( TEST_CONSTANTS.THUMBPRINT256, - "hex" + EncodingTypes.HEX ), [JwtConstants.X5C]: TEST_CONSTANTS.X5C_FROM_PUBLIC_CERTIFICATE, }, diff --git a/lib/msal-node/test/client/ClientCredentialClient.spec.ts b/lib/msal-node/test/client/ClientCredentialClient.spec.ts index dd0219a61a..919a03a16b 100644 --- a/lib/msal-node/test/client/ClientCredentialClient.spec.ts +++ b/lib/msal-node/test/client/ClientCredentialClient.spec.ts @@ -34,6 +34,7 @@ import { DEFAULT_OPENID_CONFIG_RESPONSE, DSTS_CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT, DSTS_OPENID_CONFIG_RESPONSE, + RANDOM_TEST_GUID, TEST_CONFIG, TEST_TOKENS, } from "../test_kit/StringConstants.js"; @@ -817,7 +818,10 @@ describe("ClientCredentialClient unit tests", () => { if (accessTokenKey) { // use it to get the access token (from the cache) const accessTokenFromCache: AccessTokenEntity | null = - cache.getAccessTokenCredential(accessTokenKey); + cache.getAccessTokenCredential( + accessTokenKey, + RANDOM_TEST_GUID + ); // return it and clear the interval resolve(accessTokenFromCache); clearInterval(interval); diff --git a/lib/msal-node/test/client/ClientTestUtils.ts b/lib/msal-node/test/client/ClientTestUtils.ts index 754dc9a15e..d508e8db7a 100644 --- a/lib/msal-node/test/client/ClientTestUtils.ts +++ b/lib/msal-node/test/client/ClientTestUtils.ts @@ -31,6 +31,8 @@ import { ClientAssertionCallback, ClientAssertionConfig, PasswordGrantConstants, + OAuthResponseType, + StubPerformanceClient, } from "@azure/msal-common"; import { AUTHENTICATION_RESULT, @@ -76,8 +78,8 @@ export class MockStorageClass extends CacheManager { } } - async removeAccount(key: string): Promise { - await super.removeAccount(key); + removeAccount(key: string): void { + super.removeAccount(key, RANDOM_TEST_GUID); const currentAccounts = this.getAccountKeys(); const removalIndex = currentAccounts.indexOf(key); if (removalIndex > -1) { @@ -249,8 +251,8 @@ export const mockCrypto = { async getPublicKeyThumbprint(): Promise { return TEST_POP_VALUES.KID; }, - async removeTokenBindingKey(): Promise { - return Promise.resolve(true); + async removeTokenBindingKey(): Promise { + return Promise.resolve(); }, async signJwt(): Promise { return ""; @@ -271,7 +273,8 @@ export class ClientTestUtils { const mockStorage = new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, mockCrypto, - new Logger({}) + new Logger({}), + new StubPerformanceClient() ); const testLoggerCallback = (): void => { @@ -526,7 +529,9 @@ export const checkMockedNetworkRequest = ( if (checks.msLibraryCapability !== undefined) { expect( returnVal.includes( - `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE}` + `${AADServerParamKeys.X_MS_LIB_CAPABILITY}=${encodeURIComponent( + ThrottlingConstants.X_MS_LIB_CAPABILITY_VALUE + )}` ) ).toBe(checks.msLibraryCapability); } @@ -574,7 +579,9 @@ export const checkMockedNetworkRequest = ( if (checks.responseType !== undefined) { expect( returnVal.includes( - `${AADServerParamKeys.RESPONSE_TYPE}=${Constants.TOKEN_RESPONSE_TYPE}%20${Constants.ID_TOKEN_RESPONSE_TYPE}` + `${AADServerParamKeys.RESPONSE_TYPE}=${encodeURIComponent( + OAuthResponseType.IDTOKEN_TOKEN + )}` ) ).toBe(checks.responseType); } diff --git a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts index fd93da9748..e4fc599f1c 100644 --- a/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts +++ b/lib/msal-node/test/client/ConfidentialClientApplication.spec.ts @@ -137,7 +137,7 @@ describe("ConfidentialClientApplication", () => { const config: Configuration = await ClientTestUtils.createTestConfidentialClientConfiguration( - ["cp1", "cp2"], + CAE_CONSTANTS.CLIENT_CAPABILITIES, mockNetworkClient( {}, // not needed CONFIDENTIAL_CLIENT_AUTHENTICATION_RESULT @@ -360,7 +360,7 @@ describe("ConfidentialClientApplication", () => { }; let acquireTokenByClientCredentialSpy: jest.SpyInstance; - let buildOauthClientConfigurationSpy: jest.SpyInstance; + let createAuthoritySpy: jest.SpyInstance; let sendPostRequestAsyncSpy: jest.SpyInstance; let client: ConfidentialClientApplication; let request: ClientCredentialRequest; @@ -370,9 +370,9 @@ describe("ConfidentialClientApplication", () => { "acquireTokenByClientCredential" ); - buildOauthClientConfigurationSpy = jest.spyOn( + createAuthoritySpy = jest.spyOn( ConfidentialClientApplication.prototype, - "buildOauthClientConfiguration" + "createAuthority" ); sendPostRequestAsyncSpy = jest.spyOn( @@ -404,10 +404,9 @@ describe("ConfidentialClientApplication", () => { expect(acquireTokenByClientCredentialSpy).toHaveBeenCalledTimes( 1 ); - expect( - buildOauthClientConfigurationSpy.mock.lastCall[4] - .azureRegion - ).toEqual(process.env[MSAL_FORCE_REGION]); + expect(createAuthoritySpy.mock.lastCall[2].azureRegion).toEqual( + process.env[MSAL_FORCE_REGION] + ); checkRegion( sendPostRequestAsyncSpy.mock.lastCall[0], @@ -427,10 +426,9 @@ describe("ConfidentialClientApplication", () => { expect(acquireTokenByClientCredentialSpy).toHaveBeenCalledTimes( 1 ); - expect( - buildOauthClientConfigurationSpy.mock.lastCall[4] - .azureRegion - ).toEqual(region); + expect(createAuthoritySpy.mock.lastCall[2].azureRegion).toEqual( + region + ); checkRegion(sendPostRequestAsyncSpy.mock.lastCall[0], region); }); @@ -446,8 +444,7 @@ describe("ConfidentialClientApplication", () => { 1 ); expect( - buildOauthClientConfigurationSpy.mock.lastCall[4] - .azureRegion + createAuthoritySpy.mock.lastCall[2].azureRegion ).toBeUndefined(); const endpoint: string = diff --git a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts index a64af94a58..ed9679f00a 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AppService.spec.ts @@ -58,6 +58,11 @@ describe("Acquires a token successfully via an App Service Managed Identity", () describe("User Assigned", () => { test("acquires a User Assigned Client Id token", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); + const managedIdentityApplication: ManagedIdentityApplication = new ManagedIdentityApplication(userAssignedClientIdConfig); expect(managedIdentityApplication.getManagedIdentitySource()).toBe( @@ -68,10 +73,23 @@ describe("Acquires a token successfully via an App Service Managed Identity", () await managedIdentityApplication.acquireToken( managedIdentityRequestParams ); - expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); + + const url: URLSearchParams = new URLSearchParams( + sendGetRequestAsyncSpy.mock.lastCall[0] + ); + expect( + url.has( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID + ) + ).toBe(true); + expect( + url.has( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID_2017 + ) + ).toBe(false); }); test("acquires a User Assigned Resource Id token", async () => { diff --git a/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts index 52f1eb6a54..2b72f84435 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/AzureArc.spec.ts @@ -236,7 +236,9 @@ describe("Acquires a token successfully via an Azure Arc Managed Identity", () = ManagedIdentityEnvironmentVariableNames .IDENTITY_ENDPOINT ] - }?api-version=${ARC_API_VERSION}&resource=${MANAGED_IDENTITY_RESOURCE_BASE}`, + }?api-version=${ARC_API_VERSION}&resource=${encodeURIComponent( + MANAGED_IDENTITY_RESOURCE_BASE + )}`, { headers: { Authorization: diff --git a/lib/msal-node/test/client/ManagedIdentitySources/DefaultManagedIdentityRetryPolicy.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/DefaultManagedIdentityRetryPolicy.spec.ts new file mode 100644 index 0000000000..562b0085eb --- /dev/null +++ b/lib/msal-node/test/client/ManagedIdentitySources/DefaultManagedIdentityRetryPolicy.spec.ts @@ -0,0 +1,394 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication.js"; +import { + DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, + MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR, + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE, + ONE_HUNDRED_TIMES_FASTER, +} from "../../test_kit/StringConstants.js"; + +import { + userAssignedClientIdConfig, + managedIdentityRequestParams, + systemAssignedConfig, + ManagedIdentityNetworkErrorClient, + networkClient, +} from "../../test_kit/ManagedIdentityTestUtils.js"; +import { + AuthenticationResult, + HttpStatus, + ServerError, +} from "@azure/msal-common"; +import { ManagedIdentityClient } from "../../../src/client/ManagedIdentityClient.js"; +import { + ManagedIdentityEnvironmentVariableNames, + ManagedIdentitySourceNames, +} from "../../../src/utils/Constants.js"; +import { DefaultManagedIdentityRetryPolicy } from "../../../src/retry/DefaultManagedIdentityRetryPolicy.js"; + +describe("Linear Retry Policy (App Service, Azure Arc, Cloud Shell, Machine Learning, Service Fabric)", () => { + beforeAll(() => { + // The managed identity's source will be set to Service Fabric for these tests + process.env[ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT] = + "fake_IDENTITY_ENDPOINT"; + process.env[ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER] = + "fake_IDENTITY_HEADER"; + process.env[ + ManagedIdentityEnvironmentVariableNames.IDENTITY_SERVER_THUMBPRINT + ] = "fake_IDENTITY_SERVER_THUMBPRINT"; + }); + + afterAll(() => { + delete process.env[ + ManagedIdentityEnvironmentVariableNames.IDENTITY_ENDPOINT + ]; + delete process.env[ + ManagedIdentityEnvironmentVariableNames.IDENTITY_HEADER + ]; + delete process.env[ + ManagedIdentityEnvironmentVariableNames.IDENTITY_SERVER_THUMBPRINT + ]; + }); + + beforeEach(() => { + jest.spyOn( + DefaultManagedIdentityRetryPolicy, + "DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS", + "get" + ).mockReturnValue( + DefaultManagedIdentityRetryPolicy.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS * + ONE_HUNDRED_TIMES_FASTER + ); + }); + + afterEach(() => { + // reset static variables after each test + delete ManagedIdentityClient["identitySource"]; + delete ManagedIdentityApplication["nodeStorage"]; + jest.restoreAllMocks(); + }); + + const managedIdentityNetworkErrorClientDefault500 = + new ManagedIdentityNetworkErrorClient(); + const managedIdentityNetworkErrorClient400 = + new ManagedIdentityNetworkErrorClient( + MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR, + undefined, + HttpStatus.BAD_REQUEST + ); + + describe("User Assigned", () => { + let managedIdentityApplication: ManagedIdentityApplication; + beforeEach(() => { + managedIdentityApplication = new ManagedIdentityApplication( + userAssignedClientIdConfig + ); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.SERVICE_FABRIC + ); + }); + + test("returns a 500 error response from the network request, just the first time", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest + .spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 500. + // after this override, original functionality will be restored + // and the network request will complete successfully + .mockReturnValueOnce( + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() + ); + + const networkManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + + expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(2); + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + }); + + test("returns a 500 error response from the network request permanently", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest + .spyOn(networkClient, "sendGetRequestAsync") + // permanently override the networkClient's sendGetRequestAsync method to return a 500 + .mockReturnValue( + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() + ); + + let serverError: ServerError = new ServerError(); + try { + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + } catch (e) { + serverError = e as ServerError; + } + + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE + ) + ).toBe(true); + + expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(4); // request + 3 retries + }); + }); + + describe("System Assigned", () => { + let managedIdentityApplication: ManagedIdentityApplication; + beforeEach(() => { + managedIdentityApplication = new ManagedIdentityApplication( + systemAssignedConfig + ); + expect(managedIdentityApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.SERVICE_FABRIC + ); + }); + + test("returns a 500 error response from the network request, just the first time, with no retry-after header", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest + .spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 500. + // after this override, original functionality will be restored + // and the network request will complete successfully + .mockReturnValueOnce( + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() + ); + + const timeBeforeNetworkRequest = new Date(); + + const networkManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + + const timeAfterNetworkRequest = new Date(); + + // ensure that no extra time has elapsed between requests, because no retry-after header was sent + expect( + timeAfterNetworkRequest.valueOf() - + timeBeforeNetworkRequest.valueOf() + ).toBeGreaterThanOrEqual( + DefaultManagedIdentityRetryPolicy.DEFAULT_MANAGED_IDENTITY_RETRY_DELAY_MS + ); // only 1 retry out of 3 possible + + expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(2); + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + }); + + test("returns a 500 error response from the network request, just the first time, with a retry-after header of 3 seconds", async () => { + // make it one hundred times faster so the test completes quickly + const RETRY_AFTER_SECONDS: number = 3 * ONE_HUNDRED_TIMES_FASTER; + + const headers: Record = { + "Retry-After": RETRY_AFTER_SECONDS.toString(), + }; + const managedIdentityNetworkErrorClient = + new ManagedIdentityNetworkErrorClient(undefined, headers); + + const sendGetRequestAsyncSpy: jest.SpyInstance = jest + .spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 500. + // after this override, original functionality will be restored + // and the network request will complete successfully + .mockReturnValueOnce( + managedIdentityNetworkErrorClient.sendGetRequestAsync() + ); + + const timeBeforeNetworkRequest = new Date(); + + const networkManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + + const timeAfterNetworkRequest = new Date(); + + // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + expect( + timeAfterNetworkRequest.valueOf() - + timeBeforeNetworkRequest.valueOf() + ).toBeGreaterThanOrEqual(RETRY_AFTER_SECONDS * 1000); // convert to milliseconds, it was already defined as 100 times faster + + expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(2); + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + }); + + test("returns a 500 error response from the network request, just the first time, with a retry-after header of 3 seconds (extrapolated from an http-date)", async () => { + // this test can not be made one hundred times faster because it is based on a date + const RETRY_AFTER_SECONDS: number = 3; + + var retryAfterHttpDate = new Date(); + retryAfterHttpDate.setSeconds( + // an extra second has been added to account for this date operation + retryAfterHttpDate.getSeconds() + RETRY_AFTER_SECONDS + 1 + ); + const headers: Record = { + "Retry-After": retryAfterHttpDate.toString(), + }; + const managedIdentityNetworkErrorClient = + new ManagedIdentityNetworkErrorClient(undefined, headers); + + const sendGetRequestAsyncSpy: jest.SpyInstance = jest + .spyOn(networkClient, "sendGetRequestAsync") + // override the networkClient's sendGetRequestAsync method to return a 500. + // after this override, original functionality will be restored + // and the network request will complete successfully + .mockReturnValueOnce( + managedIdentityNetworkErrorClient.sendGetRequestAsync() + ); + + const timeBeforeNetworkRequest = new Date(); + + const networkManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + + const timeAfterNetworkRequest = new Date(); + + // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + expect( + timeAfterNetworkRequest.valueOf() - + timeBeforeNetworkRequest.valueOf() + ).toBeGreaterThanOrEqual(RETRY_AFTER_SECONDS * 1000); // convert to milliseconds + + expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(2); + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + }); + + test("returns a 500 error response from the network request permanently", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest + .spyOn(networkClient, "sendGetRequestAsync") + // permanently override the networkClient's sendGetRequestAsync method to return a 500 + .mockReturnValue( + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() + ); + + let serverError: ServerError = new ServerError(); + try { + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + } catch (e) { + serverError = e as ServerError; + } + + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE + ) + ).toBe(true); + expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(4); // request + 3 retries + }); + + test("makes three acquireToken calls on the same managed identity application (which returns a 500 error response from the network request permanently) to ensure that retry policy lifetime is per request", async () => { + const sendGetRequestAsyncSpyApp: jest.SpyInstance = jest + .spyOn(networkClient, "sendGetRequestAsync") + // permanently override the networkClient's sendGetRequestAsync method to return a 500 + .mockReturnValue( + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() + ); + + try { + await managedIdentityApplication.acquireToken({ + resource: "https://graph.microsoft1.com", + }); + } catch (e) { + // 4 total: request + 3 retries + expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes(4); + } + + try { + await managedIdentityApplication.acquireToken({ + resource: "https://graph.microsoft2.com", + }); + } catch (e) { + // 8 total: 2 x (request + 3 retries) + expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes(8); + } + + try { + await managedIdentityApplication.acquireToken({ + resource: "https://graph.microsoft3.com", + }); + } catch (e) { + // 12 total: 3 x (request + 3 retries) + expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes(12); + } + }); + + test("ensures that a retry does not happen when the http status code from a failed network response is not included in the retry policy", async () => { + const sendGetRequestAsyncSpyApp: jest.SpyInstance = jest + .spyOn(networkClient, "sendGetRequestAsync") + // permanently override the networkClient's sendGetRequestAsync method to return a 400 + .mockReturnValue( + managedIdentityNetworkErrorClient400.sendGetRequestAsync() + ); + + let serverError: ServerError = new ServerError(); + try { + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + } catch (e) { + serverError = e as ServerError; + } + + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE + ) + ).toBe(true); + expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes(1); + }); + + test("ensures that a retry does not happen when the http status code from a failed network response is included in the retry policy, but the retry policy has been disabled", async () => { + const managedIdentityApplicationNoRetry: ManagedIdentityApplication = + new ManagedIdentityApplication({ + system: { + ...systemAssignedConfig.system, + disableInternalRetries: true, + }, + }); + expect( + managedIdentityApplicationNoRetry.getManagedIdentitySource() + ).toBe(ManagedIdentitySourceNames.SERVICE_FABRIC); + + const sendGetRequestAsyncSpy: jest.SpyInstance = jest + .spyOn(networkClient, "sendGetRequestAsync") + // permanently override the networkClient's sendGetRequestAsync method to return a 500 + .mockReturnValue( + managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() + ); + + let serverError: ServerError = new ServerError(); + try { + await managedIdentityApplicationNoRetry.acquireToken( + managedIdentityRequestParams + ); + } catch (e) { + serverError = e as ServerError; + } + + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE + ) + ).toBe(true); + expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(1); + }); + }); +}); diff --git a/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts index 23491cb939..48a1ff8e4f 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/Imds.spec.ts @@ -6,17 +6,22 @@ import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication.js"; import { ManagedIdentityConfiguration } from "../../../src/config/Configuration.js"; import { + CAE_CONSTANTS, DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, + IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS, + IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_NUM_REQUESTS, + IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS, MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR, + MANAGED_IDENTITY_NETWORK_REQUEST_500_ERROR, MANAGED_IDENTITY_RESOURCE, MANAGED_IDENTITY_RESOURCE_BASE, MANAGED_IDENTITY_RESOURCE_ID, MANAGED_IDENTITY_RESOURCE_ID_2, MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE, + ONE_HUNDRED_TIMES_FASTER, TEST_CONFIG, TEST_TOKEN_LIFETIMES, - THREE_SECONDS_IN_MILLI, getCacheKey, } from "../../test_kit/StringConstants.js"; import { @@ -27,9 +32,11 @@ import { managedIdentityRequestParams, systemAssignedConfig, userAssignedResourceIdConfig, + userAssignedObjectIdConfig, } from "../../test_kit/ManagedIdentityTestUtils.js"; import { DEFAULT_MANAGED_IDENTITY_ID, + ManagedIdentityQueryParameters, ManagedIdentitySourceNames, } from "../../../src/utils/Constants.js"; import { @@ -55,6 +62,7 @@ import { ClientCredentialClient } from "../../../src/client/ClientCredentialClie import { NodeStorage } from "../../../src/cache/NodeStorage.js"; import { CacheKVStore } from "../../../src/cache/serializer/SerializerTypes.js"; import { ManagedIdentityUserAssignedIdQueryParameterNames } from "../../../src/client/ManagedIdentitySources/BaseManagedIdentitySource.js"; +import { ImdsRetryPolicy } from "../../../src/retry/ImdsRetryPolicy.js"; describe("Acquires a token successfully via an IMDS Managed Identity", () => { // IMDS doesn't need environment variables because there is a default IMDS endpoint @@ -74,17 +82,13 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { HttpStatus.BAD_REQUEST ); - const userAssignedObjectIdConfig: ManagedIdentityConfiguration = { - system: { - networkClient, - }, - managedIdentityIdParams: { - userAssignedObjectId: MANAGED_IDENTITY_RESOURCE_ID, - }, - }; - describe("User Assigned", () => { test("acquires a User Assigned Client Id token", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); + const managedIdentityApplication: ManagedIdentityApplication = new ManagedIdentityApplication(userAssignedClientIdConfig); expect(managedIdentityApplication.getManagedIdentitySource()).toBe( @@ -95,10 +99,23 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { await managedIdentityApplication.acquireToken( managedIdentityRequestParams ); - expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); + + const url: URLSearchParams = new URLSearchParams( + sendGetRequestAsyncSpy.mock.lastCall[0] + ); + expect( + url.has( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID + ) + ).toBe(true); + expect( + url.has( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID_2017 + ) + ).toBe(false); }); test("acquires a User Assigned Object Id token", async () => { @@ -203,84 +220,85 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { }); describe("Managed Identity Retry Policy", () => { - describe("User Assigned", () => { - let managedIdentityApplication: ManagedIdentityApplication; - beforeEach(() => { - managedIdentityApplication = new ManagedIdentityApplication( - userAssignedClientIdConfig - ); - expect( - managedIdentityApplication.getManagedIdentitySource() - ).toBe(ManagedIdentitySourceNames.DEFAULT_TO_IMDS); - }); + let uamiApplication: ManagedIdentityApplication; // user-assigned + let samiApplication: ManagedIdentityApplication; // system-assigned - test("returns a 500 error response from the network request, just the first time", async () => { - const sendGetRequestAsyncSpy: jest.SpyInstance = jest - .spyOn(networkClient, "sendGetRequestAsync") - // override the networkClient's sendGetRequestAsync method to return a 500. - // after this override, original functionality will be restored - // and the network request will complete successfully - .mockReturnValueOnce( - managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() - ); - - const networkManagedIdentityResult: AuthenticationResult = - await managedIdentityApplication.acquireToken( - managedIdentityRequestParams - ); + beforeEach(() => { + jest.spyOn( + ImdsRetryPolicy, + "MIN_EXPONENTIAL_BACKOFF_MS", + "get" + ).mockReturnValue( + ImdsRetryPolicy.MIN_EXPONENTIAL_BACKOFF_MS * + ONE_HUNDRED_TIMES_FASTER + ); - expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(2); - expect(networkManagedIdentityResult.accessToken).toEqual( - DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken - ); - }); + jest.spyOn( + ImdsRetryPolicy, + "MAX_EXPONENTIAL_BACKOFF_MS", + "get" + ).mockReturnValue( + ImdsRetryPolicy.MAX_EXPONENTIAL_BACKOFF_MS * + ONE_HUNDRED_TIMES_FASTER + ); - test("returns a 500 error response from the network request permanently", async () => { - const sendGetRequestAsyncSpy: jest.SpyInstance = jest - .spyOn(networkClient, "sendGetRequestAsync") - // permanently override the networkClient's sendGetRequestAsync method to return a 500 - .mockReturnValue( - managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() - ); + jest.spyOn( + ImdsRetryPolicy, + "EXPONENTIAL_DELTA_BACKOFF_MS", + "get" + ).mockReturnValue( + ImdsRetryPolicy.EXPONENTIAL_DELTA_BACKOFF_MS * + ONE_HUNDRED_TIMES_FASTER + ); - let serverError: ServerError = new ServerError(); - try { - await managedIdentityApplication.acquireToken( - managedIdentityRequestParams - ); - } catch (e) { - serverError = e as ServerError; - } + jest.spyOn( + ImdsRetryPolicy, + "HTTP_STATUS_GONE_RETRY_AFTER_MS", + "get" + ).mockReturnValue( + ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * + ONE_HUNDRED_TIMES_FASTER + ); - expect( - serverError.errorMessage.includes( - MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE - ) - ).toBe(true); + uamiApplication = new ManagedIdentityApplication( + userAssignedClientIdConfig + ); + expect(uamiApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.DEFAULT_TO_IMDS + ); - expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(4); // request + 3 retries - }); + samiApplication = new ManagedIdentityApplication( + systemAssignedConfig + ); + expect(samiApplication.getManagedIdentitySource()).toBe( + ManagedIdentitySourceNames.DEFAULT_TO_IMDS + ); }); - describe("System Assigned", () => { - let managedIdentityApplication: ManagedIdentityApplication; - beforeEach(() => { - managedIdentityApplication = new ManagedIdentityApplication( - systemAssignedConfig - ); - expect( - managedIdentityApplication.getManagedIdentitySource() - ).toBe(ManagedIdentitySourceNames.DEFAULT_TO_IMDS); - }); + test.each([ + ["UAMI", () => uamiApplication], + ["SAMI", () => samiApplication], + ])( + "%s: returns a 404 error response from the network request, the first two times", + async (_description, getMIA) => { + const managedIdentityApplication = getMIA(); + + const managedIdentityNetworkErrorClient404 = + new ManagedIdentityNetworkErrorClient( + MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR, + undefined, + HttpStatus.NOT_FOUND + ); - test("returns a 500 error response from the network request, just the first time, with no retry-after header", async () => { const sendGetRequestAsyncSpy: jest.SpyInstance = jest .spyOn(networkClient, "sendGetRequestAsync") - // override the networkClient's sendGetRequestAsync method to return a 500. - // after this override, original functionality will be restored - // and the network request will complete successfully .mockReturnValueOnce( - managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() + // initial request, will trigger first retry + managedIdentityNetworkErrorClient404.sendGetRequestAsync() + ) + .mockReturnValueOnce( + // first retry, will trigger second retry + managedIdentityNetworkErrorClient404.sendGetRequestAsync() ); const timeBeforeNetworkRequest = new Date(); @@ -292,32 +310,54 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { const timeAfterNetworkRequest = new Date(); - // ensure that no extra time has elapsed between requests, because no retry-after header was sent + // exponential backoff (1 second -> 2 seconds) expect( timeAfterNetworkRequest.valueOf() - timeBeforeNetworkRequest.valueOf() - ).toBeLessThan(THREE_SECONDS_IN_MILLI); + ).toBeGreaterThanOrEqual( + IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS * + ONE_HUNDRED_TIMES_FASTER + ); - expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(2); + expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(3); // request + 2 retries expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); - }); + } + ); - test("returns a 500 error response from the network request, just the first time, with a retry-after header of 3 seconds", async () => { - const headers: Record = { - "Retry-After": "3", // 3 seconds - }; - const managedIdentityNetworkErrorClient = - new ManagedIdentityNetworkErrorClient(undefined, headers); + test.each([ + ["UAMI", () => uamiApplication], + ["SAMI", () => samiApplication], + ])( + "%s: returns a 410 error response from the network request, the first four times", + async (_description, getMIA) => { + const managedIdentityApplication = getMIA(); + + const managedIdentityNetworkErrorClient410 = + new ManagedIdentityNetworkErrorClient( + MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR, + undefined, + HttpStatus.GONE + ); const sendGetRequestAsyncSpy: jest.SpyInstance = jest .spyOn(networkClient, "sendGetRequestAsync") - // override the networkClient's sendGetRequestAsync method to return a 500. - // after this override, original functionality will be restored - // and the network request will complete successfully .mockReturnValueOnce( - managedIdentityNetworkErrorClient.sendGetRequestAsync() + // initial request, will trigger first retry + managedIdentityNetworkErrorClient410.sendGetRequestAsync() + ) + .mockReturnValueOnce( + // first retry, will trigger second retry + managedIdentityNetworkErrorClient410.sendGetRequestAsync() + ) + .mockReturnValueOnce( + // second retry, will trigger third retry + managedIdentityNetworkErrorClient410.sendGetRequestAsync() + ) + .mockReturnValueOnce( + // third retry, will trigger fourth retry + managedIdentityNetworkErrorClient410.sendGetRequestAsync() ); const timeBeforeNetworkRequest = new Date(); @@ -329,67 +369,101 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { const timeAfterNetworkRequest = new Date(); - // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + // linear backoff (10 seconds * 4 retries) expect( timeAfterNetworkRequest.valueOf() - timeBeforeNetworkRequest.valueOf() - ).toBeGreaterThan(THREE_SECONDS_IN_MILLI); + ).toBeGreaterThanOrEqual( + ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * + 4 * + ONE_HUNDRED_TIMES_FASTER + ); - expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(2); + expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(5); // request + 4 retries expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); - }); + } + ); - test("returns a 500 error response from the network request, just the first time, with a retry-after header of 3 seconds (extrapolated from an http-date)", async () => { - var retryAfterHttpDate = new Date(); - retryAfterHttpDate.setSeconds( - retryAfterHttpDate.getSeconds() + 4 // 4 seconds. An extra second has been added to account for this date operation - ); - const headers: Record = { - "Retry-After": retryAfterHttpDate.toString(), - }; - const managedIdentityNetworkErrorClient = - new ManagedIdentityNetworkErrorClient(undefined, headers); + test.each([ + ["UAMI", () => uamiApplication], + ["SAMI", () => samiApplication], + ])( + "%s: returns a 410 error response from the network request permanently", + async (_description, getMIA) => { + const managedIdentityApplication = getMIA(); + + const managedIdentityNetworkErrorClient410 = + new ManagedIdentityNetworkErrorClient( + MANAGED_IDENTITY_IMDS_NETWORK_REQUEST_400_ERROR, + undefined, + HttpStatus.GONE + ); const sendGetRequestAsyncSpy: jest.SpyInstance = jest .spyOn(networkClient, "sendGetRequestAsync") - // override the networkClient's sendGetRequestAsync method to return a 500. - // after this override, original functionality will be restored - // and the network request will complete successfully - .mockReturnValueOnce( - managedIdentityNetworkErrorClient.sendGetRequestAsync() + // permanently override the networkClient's sendGetRequestAsync method to return a 410 + .mockReturnValue( + managedIdentityNetworkErrorClient410.sendGetRequestAsync() ); const timeBeforeNetworkRequest = new Date(); - const networkManagedIdentityResult: AuthenticationResult = + let serverError: ServerError = new ServerError(); + try { await managedIdentityApplication.acquireToken( managedIdentityRequestParams ); + } catch (e) { + serverError = e as ServerError; + } const timeAfterNetworkRequest = new Date(); - // ensure that the number of seconds in the retry-after header elapsed before the second network request was made + // linear backoff (10 seconds * 7 retries) expect( timeAfterNetworkRequest.valueOf() - timeBeforeNetworkRequest.valueOf() - ).toBeGreaterThan(THREE_SECONDS_IN_MILLI); - - expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(2); - expect(networkManagedIdentityResult.accessToken).toEqual( - DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ).toBeGreaterThanOrEqual( + ImdsRetryPolicy.HTTP_STATUS_GONE_RETRY_AFTER_MS * + 7 * + ONE_HUNDRED_TIMES_FASTER ); - }); - test("returns a 500 error response from the network request permanently", async () => { + expect( + serverError.errorMessage.includes( + MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE + ) + ).toBe(true); + expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(8); // request + 7 retries + } + ); + + test.each([ + ["UAMI", () => uamiApplication], + ["SAMI", () => samiApplication], + ])( + "%s: returns a 5xx (504) error response from the network request permanently", + async (_description, getMIA) => { + const managedIdentityApplication = getMIA(); + + const managedIdentityNetworkErrorClient504 = + new ManagedIdentityNetworkErrorClient( + MANAGED_IDENTITY_NETWORK_REQUEST_500_ERROR, + undefined, + HttpStatus.GATEWAY_TIMEOUT + ); + const sendGetRequestAsyncSpy: jest.SpyInstance = jest .spyOn(networkClient, "sendGetRequestAsync") - // permanently override the networkClient's sendGetRequestAsync method to return a 500 + // permanently override the networkClient's sendGetRequestAsync method to return a 504 .mockReturnValue( - managedIdentityNetworkErrorClientDefault500.sendGetRequestAsync() + managedIdentityNetworkErrorClient504.sendGetRequestAsync() ); + const timeBeforeNetworkRequest = new Date(); + let serverError: ServerError = new ServerError(); try { await managedIdentityApplication.acquireToken( @@ -399,15 +473,34 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { serverError = e as ServerError; } + const timeAfterNetworkRequest = new Date(); + + // exponential backoff (1 second -> 2 seconds -> 4 seconds) + expect( + timeAfterNetworkRequest.valueOf() - + timeBeforeNetworkRequest.valueOf() + ).toBeGreaterThanOrEqual( + IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS * + ONE_HUNDRED_TIMES_FASTER + ); + expect( serverError.errorMessage.includes( MANAGED_IDENTITY_TOKEN_RETRIEVAL_ERROR_MESSAGE ) ).toBe(true); expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(4); // request + 3 retries - }); + } + ); + + test.each([ + ["UAMI", () => uamiApplication], + ["SAMI", () => samiApplication], + ])( + "%s: makes three acquireToken calls on the same managed identity application (which returns a 500 error response from the network request permanently) to ensure that retry policy lifetime is per request", + async (_description, getMIA) => { + const managedIdentityApplication = getMIA(); - test("makes three acquireToken calls on the same managed identity application (which returns a 500 error response from the network request permanently) to ensure that retry policy lifetime is per request", async () => { const sendGetRequestAsyncSpyApp: jest.SpyInstance = jest .spyOn(networkClient, "sendGetRequestAsync") // permanently override the networkClient's sendGetRequestAsync method to return a 500 @@ -420,7 +513,10 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { resource: "https://graph.microsoft1.com", }); } catch (e) { - expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes(4); // request + 3 retries + // 4 total: request + 3 retries + expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes( + IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_NUM_REQUESTS + ); } try { @@ -428,7 +524,10 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { resource: "https://graph.microsoft2.com", }); } catch (e) { - expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes(8); // 8 total, 2 x (request + 3 retries) + // 8 total: 2 x (request + 3 retries) + expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes( + IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_NUM_REQUESTS * 2 + ); } try { @@ -436,11 +535,22 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { resource: "https://graph.microsoft3.com", }); } catch (e) { - expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes(12); // 12 total, 3 x (request + 3 retries) + // 12 total: 3 x (request + 3 retries) + expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes( + IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_NUM_REQUESTS * 3 + ); } - }, 15000); // triple the timeout value for this test because there are 3 acquireToken calls (3 x 1 second in between retries) + } + ); + + test.each([ + ["UAMI", () => uamiApplication], + ["SAMI", () => samiApplication], + ])( + "%s: ensures that a retry does not happen when the http status code from a failed network response (400) is not included in the list of retriable status codes", + async (_description, getMIA) => { + const managedIdentityApplication = getMIA(); - test("ensures that a retry does not happen when the http status code from a failed network response is not included in the retry policy", async () => { const sendGetRequestAsyncSpyApp: jest.SpyInstance = jest .spyOn(networkClient, "sendGetRequestAsync") // permanently override the networkClient's sendGetRequestAsync method to return a 400 @@ -463,19 +573,22 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { ) ).toBe(true); expect(sendGetRequestAsyncSpyApp).toHaveBeenCalledTimes(1); - }); + } + ); - test("ensures that a retry does not happen when the http status code from a failed network response is included in the retry policy, but the retry policy has been disabled", async () => { + test.each([ + ["UAMI", userAssignedClientIdConfig], + ["SAMI", systemAssignedConfig], + ])( + "%s: ensures that a retry does not happen when the http status code from a failed network response (500) is included in the list of retriable status codes, but the retry policy has been disabled", + async (_description, config) => { const managedIdentityApplicationNoRetry: ManagedIdentityApplication = new ManagedIdentityApplication({ system: { - ...systemAssignedConfig.system, + ...config.system, disableInternalRetries: true, }, }); - expect( - managedIdentityApplicationNoRetry.getManagedIdentitySource() - ).toBe(ManagedIdentitySourceNames.DEFAULT_TO_IMDS); const sendGetRequestAsyncSpy: jest.SpyInstance = jest .spyOn(networkClient, "sendGetRequestAsync") @@ -499,8 +612,8 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { ) ).toBe(true); expect(sendGetRequestAsyncSpy).toHaveBeenCalledTimes(1); - }); - }); + } + ); }); describe("Miscellaneous", () => { @@ -545,28 +658,48 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { ); }); - test("ignores a cached token when claims are provided", async () => { + test("ignores a cached token when claims are provided and the Managed Identity does not support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh was not included in the network request to the Managed Identity", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); + + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication({ + ...systemAssignedConfig, + clientCapabilities: CAE_CONSTANTS.CLIENT_CAPABILITIES, + }); + let networkManagedIdentityResult: AuthenticationResult = - await systemAssignedManagedIdentityApplication.acquireToken({ + await managedIdentityApplication.acquireToken({ resource: MANAGED_IDENTITY_RESOURCE, }); expect(networkManagedIdentityResult.fromCache).toBe(false); - expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); + expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(1); + const firstNetworkRequestUrlParams: URLSearchParams = + new URLSearchParams(sendGetRequestAsyncSpy.mock.lastCall[0]); + expect( + firstNetworkRequestUrlParams.get( + ManagedIdentityQueryParameters.XMS_CC + ) + ).toEqual(CAE_CONSTANTS.CLIENT_CAPABILITIES.toString()); + const cachedManagedIdentityResult: AuthenticationResult = - await systemAssignedManagedIdentityApplication.acquireToken({ + await managedIdentityApplication.acquireToken({ resource: MANAGED_IDENTITY_RESOURCE, }); expect(cachedManagedIdentityResult.fromCache).toBe(true); expect(cachedManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); + expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(1); networkManagedIdentityResult = - await systemAssignedManagedIdentityApplication.acquireToken({ + await managedIdentityApplication.acquireToken({ claims: TEST_CONFIG.CLAIMS, resource: MANAGED_IDENTITY_RESOURCE, }); @@ -574,6 +707,15 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); + + expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(2); + const secondNetworkRequestUrlParams: URLSearchParams = + new URLSearchParams(sendGetRequestAsyncSpy.mock.lastCall[0]); + expect( + secondNetworkRequestUrlParams.has( + ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH + ) + ).toBe(false); }); test("ignores a cached token when forceRefresh is set to true", async () => { @@ -582,7 +724,6 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { resource: MANAGED_IDENTITY_RESOURCE, }); expect(networkManagedIdentityResult.fromCache).toBe(false); - expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); @@ -602,7 +743,6 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { resource: MANAGED_IDENTITY_RESOURCE, }); expect(networkManagedIdentityResult.fromCache).toBe(false); - expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); @@ -614,7 +754,6 @@ describe("Acquires a token successfully via an IMDS Managed Identity", () => { resource: MANAGED_IDENTITY_RESOURCE, }); expect(networkManagedIdentityResult.fromCache).toBe(false); - expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); diff --git a/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts index 2cae80d8da..7317ffbbf3 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/MachineLearning.spec.ts @@ -9,7 +9,6 @@ import { DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, MANAGED_IDENTITY_MACHINE_LEARNING_NETWORK_REQUEST_400_ERROR, MANAGED_IDENTITY_RESOURCE, - MANAGED_IDENTITY_RESOURCE_ID, } from "../../test_kit/StringConstants.js"; import { @@ -19,6 +18,7 @@ import { networkClient, ManagedIdentityNetworkErrorClient, userAssignedResourceIdConfig, + userAssignedObjectIdConfig, } from "../../test_kit/ManagedIdentityTestUtils.js"; import { AuthenticationResult, @@ -31,9 +31,13 @@ import { ManagedIdentitySourceNames, } from "../../../src/utils/Constants.js"; import { ManagedIdentityUserAssignedIdQueryParameterNames } from "../../../src/client/ManagedIdentitySources/BaseManagedIdentitySource.js"; +import { MANAGED_IDENTITY_MACHINE_LEARNING_UNSUPPORTED_ID_TYPE_ERROR } from "../../../src/client/ManagedIdentitySources/MachineLearning.js"; describe("Acquires a token successfully via an Machine Learning Managed Identity", () => { beforeAll(() => { + process.env[ + ManagedIdentityEnvironmentVariableNames.DEFAULT_IDENTITY_CLIENT_ID + ] = "fake_DEFAULT_IDENTITY_CLIENT_ID"; process.env[ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT] = "fake_MSI_ENDPOINT"; process.env[ManagedIdentityEnvironmentVariableNames.MSI_SECRET] = @@ -41,6 +45,9 @@ describe("Acquires a token successfully via an Machine Learning Managed Identity }); afterAll(() => { + delete process.env[ + ManagedIdentityEnvironmentVariableNames.DEFAULT_IDENTITY_CLIENT_ID + ]; delete process.env[ ManagedIdentityEnvironmentVariableNames.MSI_ENDPOINT ]; @@ -57,30 +64,13 @@ describe("Acquires a token successfully via an Machine Learning Managed Identity describe("User Assigned", () => { test("acquires a User Assigned Client Id token", async () => { - const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication(userAssignedClientIdConfig); - expect(managedIdentityApplication.getManagedIdentitySource()).toBe( - ManagedIdentitySourceNames.MACHINE_LEARNING - ); - - const networkManagedIdentityResult: AuthenticationResult = - await managedIdentityApplication.acquireToken( - managedIdentityRequestParams - ); - - expect(networkManagedIdentityResult.accessToken).toEqual( - DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken - ); - }); - - test("acquires a User Assigned Resource Id token", async () => { const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( networkClient, "sendGetRequestAsync" ); const managedIdentityApplication: ManagedIdentityApplication = - new ManagedIdentityApplication(userAssignedResourceIdConfig); + new ManagedIdentityApplication(userAssignedClientIdConfig); expect(managedIdentityApplication.getManagedIdentitySource()).toBe( ManagedIdentitySourceNames.MACHINE_LEARNING ); @@ -99,14 +89,22 @@ describe("Acquires a token successfully via an Machine Learning Managed Identity ); expect( url.has( - ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_RESOURCE_ID_NON_IMDS + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID + ) + ).toBe(false); + expect( + url.has( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID_2017 ) ).toBe(true); expect( url.get( - ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_RESOURCE_ID_NON_IMDS + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID_2017 ) - ).toEqual(MANAGED_IDENTITY_RESOURCE_ID); + ).toEqual( + userAssignedClientIdConfig.managedIdentityIdParams + ?.userAssignedClientId + ); }); test("ensures that App Service is selected as the Managed Identity source when all App Service and Machine Learning environment variables are present", async () => { @@ -144,6 +142,11 @@ describe("Acquires a token successfully via an Machine Learning Managed Identity }); test("acquires a token", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); + const networkManagedIdentityResult: AuthenticationResult = await managedIdentityApplication.acquireToken( managedIdentityRequestParams @@ -153,6 +156,30 @@ describe("Acquires a token successfully via an Machine Learning Managed Identity expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); + + const url: URLSearchParams = new URLSearchParams( + sendGetRequestAsyncSpy.mock.lastCall[0] + ); + expect( + url.has( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID + ) + ).toBe(false); + expect( + url.has( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID_2017 + ) + ).toBe(true); + expect( + url.get( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID_2017 + ) + ).toEqual( + process.env[ + ManagedIdentityEnvironmentVariableNames + .DEFAULT_IDENTITY_CLIENT_ID + ] + ); }); test("returns an already acquired token from the cache", async () => { @@ -218,5 +245,34 @@ describe("Acquires a token successfully via an Machine Learning Managed Identity ) ).toBe(true); }); + + test.each([ + ["a resource", userAssignedResourceIdConfig], + ["an object", userAssignedObjectIdConfig], + ])( + "ensures that providing %s id will throw an error", + async (_description, userAssignedIdConfig) => { + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication(userAssignedIdConfig); + expect( + managedIdentityApplication.getManagedIdentitySource() + ).toBe(ManagedIdentitySourceNames.MACHINE_LEARNING); + + let error: Error = new Error(); + try { + await managedIdentityApplication.acquireToken( + managedIdentityRequestParams + ); + } catch (e) { + error = e as Error; + } + + expect( + error.message.includes( + MANAGED_IDENTITY_MACHINE_LEARNING_UNSUPPORTED_ID_TYPE_ERROR + ) + ).toBe(true); + } + ); }); }); diff --git a/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts b/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts index 2e93cc14bf..dff4109cdd 100644 --- a/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts +++ b/lib/msal-node/test/client/ManagedIdentitySources/ServiceFabric.spec.ts @@ -5,11 +5,14 @@ import { ManagedIdentityApplication } from "../../../src/client/ManagedIdentityApplication.js"; import { + CAE_CONSTANTS, + DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH_IN_HEX, DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT, MANAGED_IDENTITY_RESOURCE, MANAGED_IDENTITY_RESOURCE_ID, MANAGED_IDENTITY_SERVICE_FABRIC_NETWORK_REQUEST_400_ERROR, + TEST_CONFIG, } from "../../test_kit/StringConstants.js"; import { @@ -28,6 +31,7 @@ import { import { ManagedIdentityClient } from "../../../src/client/ManagedIdentityClient.js"; import { ManagedIdentityEnvironmentVariableNames, + ManagedIdentityQueryParameters, ManagedIdentitySourceNames, } from "../../../src/utils/Constants.js"; import { ManagedIdentityUserAssignedIdQueryParameterNames } from "../../../src/client/ManagedIdentitySources/BaseManagedIdentitySource.js"; @@ -64,6 +68,11 @@ describe("Acquires a token successfully via an App Service Managed Identity", () describe("User Assigned", () => { test("acquires a User Assigned Client Id token", async () => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); + const managedIdentityApplication: ManagedIdentityApplication = new ManagedIdentityApplication(userAssignedClientIdConfig); expect(managedIdentityApplication.getManagedIdentitySource()).toBe( @@ -74,10 +83,23 @@ describe("Acquires a token successfully via an App Service Managed Identity", () await managedIdentityApplication.acquireToken( managedIdentityRequestParams ); - expect(networkManagedIdentityResult.accessToken).toEqual( DEFAULT_USER_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken ); + + const url: URLSearchParams = new URLSearchParams( + sendGetRequestAsyncSpy.mock.lastCall[0] + ); + expect( + url.has( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID + ) + ).toBe(true); + expect( + url.has( + ManagedIdentityUserAssignedIdQueryParameterNames.MANAGED_IDENTITY_CLIENT_ID_2017 + ) + ).toBe(false); }); test("acquires a User Assigned Resource Id token", async () => { const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( @@ -163,6 +185,86 @@ describe("Acquires a token successfully via an App Service Managed Identity", () }); }); + describe("Miscellaneous", () => { + it.each([ + [ + CAE_CONSTANTS.CLIENT_CAPABILITIES, + CAE_CONSTANTS.CLIENT_CAPABILITIES.toString(), + ], + [undefined, null], + ])( + "ignores a cached token when claims are provided (regardless of if client capabilities are provided or not) and the Managed Identity does support token revocation, and ensures the token revocation query parameter token_sha256_to_refresh is included in the network request to the Managed Identity", + async (providedCapabilities, capabilitiesOnNetworkRequest) => { + const sendGetRequestAsyncSpy: jest.SpyInstance = jest.spyOn( + networkClient, + "sendGetRequestAsync" + ); + + const managedIdentityApplication: ManagedIdentityApplication = + new ManagedIdentityApplication({ + ...systemAssignedConfig, + clientCapabilities: providedCapabilities, + }); + expect( + managedIdentityApplication.getManagedIdentitySource() + ).toBe(ManagedIdentitySourceNames.SERVICE_FABRIC); + + let networkManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken({ + resource: MANAGED_IDENTITY_RESOURCE, + }); + expect(networkManagedIdentityResult.fromCache).toBe(false); + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + + expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(1); + const firstNetworkRequestUrlParams: URLSearchParams = + new URLSearchParams( + sendGetRequestAsyncSpy.mock.lastCall[0] + ); + expect( + firstNetworkRequestUrlParams.get( + ManagedIdentityQueryParameters.XMS_CC + ) + ).toEqual(capabilitiesOnNetworkRequest); + + const cachedManagedIdentityResult: AuthenticationResult = + await managedIdentityApplication.acquireToken({ + resource: MANAGED_IDENTITY_RESOURCE, + }); + expect(cachedManagedIdentityResult.fromCache).toBe(true); + expect(cachedManagedIdentityResult.accessToken).toEqual( + DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(1); + + networkManagedIdentityResult = + await managedIdentityApplication.acquireToken({ + claims: TEST_CONFIG.CLAIMS, + resource: MANAGED_IDENTITY_RESOURCE, + }); + expect(networkManagedIdentityResult.fromCache).toBe(false); + expect(networkManagedIdentityResult.accessToken).toEqual( + DEFAULT_SYSTEM_ASSIGNED_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken + ); + + expect(sendGetRequestAsyncSpy.mock.calls.length).toEqual(2); + const secondNetworkRequestUrlParams: URLSearchParams = + new URLSearchParams( + sendGetRequestAsyncSpy.mock.lastCall[0] + ); + expect( + secondNetworkRequestUrlParams.get( + ManagedIdentityQueryParameters.SHA256_TOKEN_TO_REFRESH + ) + ).toEqual( + DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH_IN_HEX + ); + } + ); + }); + describe("Errors", () => { test("ensures that the error format is correct", async () => { const managedIdentityNetworkErrorClient400 = diff --git a/lib/msal-node/test/client/PublicClientApplication.spec.ts b/lib/msal-node/test/client/PublicClientApplication.spec.ts index 507562bae7..c9668907d5 100644 --- a/lib/msal-node/test/client/PublicClientApplication.spec.ts +++ b/lib/msal-node/test/client/PublicClientApplication.spec.ts @@ -21,7 +21,7 @@ import { Logger, LogLevel, AccountInfo, - ServerAuthorizationCodeResponse, + AuthorizeResponse, InteractionRequiredAuthError, AccountEntity, AuthToken, @@ -61,7 +61,7 @@ import * as msalNode from "../../src/index.js"; import { setupServerTelemetryManagerMock } from "./test-fixtures.js"; import { getMsalCommonAutoMock, MSALCommonModule } from "../utils/MockUtils.js"; -import { version, name } from "../../package.json"; +import { version, name } from "../../src/packageMetadata.js"; import { MockNativeBrokerPlugin } from "../utils/MockNativeBrokerPlugin.js"; import { SignOutRequest } from "../../src/request/SignOutRequest.js"; import { LoopbackClient } from "../../src/network/LoopbackClient.js"; @@ -80,6 +80,8 @@ import { Constants } from "../../src/utils/Constants.js"; import { NodeStorage } from "../../src/cache/NodeStorage.js"; import { TokenCache } from "../../src/index.js"; import { buildAccountFromIdTokenClaims } from "msal-test-utils"; +import * as AuthorizeProtocol from "../../src/protocol/Authorize.js"; +import { StubPerformanceClient } from "@azure/msal-common"; const msalCommon: MSALCommonModule = jest.requireActual( "@azure/msal-common/node" @@ -652,11 +654,11 @@ describe("PublicClientApplication", () => { ); jest.spyOn( - MockAuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockImplementation((req) => { + AuthorizeProtocol, + "getAuthCodeRequestUrl" + ).mockImplementation((_config, _authority, req, _logger) => { redirectUri = req.redirectUri; - return Promise.resolve(TEST_CONSTANTS.AUTH_CODE_URL); + return TEST_CONSTANTS.AUTH_CODE_URL; }); jest.spyOn( @@ -724,11 +726,11 @@ describe("PublicClientApplication", () => { ); jest.spyOn( - MockAuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockImplementation((req) => { + AuthorizeProtocol, + "getAuthCodeRequestUrl" + ).mockImplementation((_config, _authority, req, _logger) => { redirectUri = req.redirectUri; - return Promise.resolve(TEST_CONSTANTS.AUTH_CODE_URL); + return TEST_CONSTANTS.AUTH_CODE_URL; }); jest.spyOn( @@ -760,18 +762,16 @@ describe("PublicClientApplication", () => { return Promise.resolve(); }; - const testServerCodeResponse: ServerAuthorizationCodeResponse = { + const testServerCodeResponse: AuthorizeResponse = { code: TEST_CONSTANTS.AUTHORIZATION_CODE, client_info: TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO, state: "123", }; const mockListenForAuthCode = jest.fn(() => { - return new Promise( - (resolve) => { - resolve(testServerCodeResponse); - } - ); + return new Promise((resolve) => { + resolve(testServerCodeResponse); + }); }); const mockGetRedirectUri = jest.fn( () => TEST_CONSTANTS.REDIRECT_URI @@ -800,11 +800,11 @@ describe("PublicClientApplication", () => { ); jest.spyOn( - MockAuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockImplementation((req) => { + AuthorizeProtocol, + "getAuthCodeRequestUrl" + ).mockImplementation((_config, _authority, req, _logger) => { expect(req.redirectUri).toEqual(TEST_CONSTANTS.REDIRECT_URI); - return Promise.resolve(TEST_CONSTANTS.AUTH_CODE_URL); + return TEST_CONSTANTS.AUTH_CODE_URL; }); jest.spyOn( @@ -913,7 +913,7 @@ describe("PublicClientApplication", () => { return Promise.reject("Browser open error"); }; - const testServerCodeResponse: ServerAuthorizationCodeResponse = { + const testServerCodeResponse: AuthorizeResponse = { code: TEST_CONSTANTS.AUTHORIZATION_CODE, client_info: TEST_DATA_CLIENT_INFO.TEST_DECODED_CLIENT_INFO, state: "123", @@ -923,11 +923,9 @@ describe("PublicClientApplication", () => { LoopbackClient.prototype, "listenForAuthCode" ).mockImplementation(() => { - return new Promise( - (resolve) => { - resolve(testServerCodeResponse); - } - ); + return new Promise((resolve) => { + resolve(testServerCodeResponse); + }); }); jest.spyOn( LoopbackClient.prototype, @@ -953,11 +951,11 @@ describe("PublicClientApplication", () => { ); jest.spyOn( - MockAuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockImplementation((req) => { + AuthorizeProtocol, + "getAuthCodeRequestUrl" + ).mockImplementation((_config, _authority, req, _logger) => { expect(req.redirectUri).toEqual(TEST_CONSTANTS.REDIRECT_URI); - return Promise.resolve(TEST_CONSTANTS.AUTH_CODE_URL); + return TEST_CONSTANTS.AUTH_CODE_URL; }); authApp.acquireTokenInteractive(request).catch((e) => { @@ -1018,11 +1016,11 @@ describe("PublicClientApplication", () => { ); jest.spyOn( - MockAuthorizationCodeClient.prototype, - "getAuthCodeUrl" - ).mockImplementation((req) => { + AuthorizeProtocol, + "getAuthCodeRequestUrl" + ).mockImplementation((_config, _authority, req, _logger) => { expect(req.redirectUri).toEqual(TEST_CONSTANTS.REDIRECT_URI); - return Promise.resolve(TEST_CONSTANTS.AUTH_CODE_URL); + return TEST_CONSTANTS.AUTH_CODE_URL; }); authApp.acquireTokenInteractive(request).catch((e) => { @@ -1054,7 +1052,8 @@ describe("PublicClientApplication", () => { new MockStorageClass( TEST_CONFIG.MSAL_CLIENT_ID, cryptoProvider, - new Logger({}) + new Logger({}), + new StubPerformanceClient() ), { protocolMode: ProtocolMode.AAD, diff --git a/lib/msal-node/test/test_kit/ManagedIdentityTestUtils.ts b/lib/msal-node/test/test_kit/ManagedIdentityTestUtils.ts index 0cd263fb4c..0640e7930d 100644 --- a/lib/msal-node/test/test_kit/ManagedIdentityTestUtils.ts +++ b/lib/msal-node/test/test_kit/ManagedIdentityTestUtils.ts @@ -135,6 +135,15 @@ export const userAssignedResourceIdConfig: ManagedIdentityConfiguration = { }, }; +export const userAssignedObjectIdConfig: ManagedIdentityConfiguration = { + system: { + networkClient, + }, + managedIdentityIdParams: { + userAssignedObjectId: MANAGED_IDENTITY_RESOURCE_ID, + }, +}; + export const systemAssignedConfig: ManagedIdentityConfiguration = { system: { networkClient, diff --git a/lib/msal-node/test/test_kit/StringConstants.ts b/lib/msal-node/test/test_kit/StringConstants.ts index b4f8ca5e1a..9ca1a71d3c 100644 --- a/lib/msal-node/test/test_kit/StringConstants.ts +++ b/lib/msal-node/test/test_kit/StringConstants.ts @@ -446,6 +446,10 @@ export const getCacheKey = (resource?: string): string => { return `-${Constants.DEFAULT_AUTHORITY_HOST}-accesstoken-${resourceHelper}-managed_identity-${MANAGED_IDENTITY_RESOURCE_BASE}--`; }; +// SHA256 hash of the DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT.accessToken +export const DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT_ACCESS_TOKEN_SHA256_HASH_IN_HEX = + "d9678c32c96e9d358c4e61d5b230074c04f037405411740d4e8d9123066341af"; + export const DEFAULT_MANAGED_IDENTITY_AUTHENTICATION_RESULT: Omit< AuthenticationResult, "correlationId" @@ -567,7 +571,12 @@ export const CORS_SIMPLE_REQUEST_HEADERS = [ "content-type", ]; -export const THREE_SECONDS_IN_MILLI = 3000; +export const ONE_HUNDRED_TIMES_FASTER: number = 0.01; + +export const IMDS_EXPONENTIAL_STRATEGY_TWO_RETRIES_IN_MS = 3000; // 1 second -> 2 seconds +export const IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_IN_MS = 7000; // 1 second -> 2 seconds -> 4 seconds + +export const IMDS_EXPONENTIAL_STRATEGY_MAX_RETRIES_NUM_REQUESTS = 4; // initial request -> 1 second -> 2 seconds -> 4 seconds export const MOCK_USERNAME = `mock_${PasswordGrantConstants.username}`; export const MOCK_PASSWORD = `mock_${PasswordGrantConstants.password}`; diff --git a/lib/msal-node/test/utils/CryptoKeys.ts b/lib/msal-node/test/utils/CryptoKeys.ts index 7f0f8516c8..b0120249b3 100644 --- a/lib/msal-node/test/utils/CryptoKeys.ts +++ b/lib/msal-node/test/utils/CryptoKeys.ts @@ -3,6 +3,7 @@ * Licensed under the MIT License. */ +import { EncodingTypes } from "@azure/msal-common"; import crypto from "crypto"; export class CryptoKeys { @@ -28,6 +29,6 @@ export class CryptoKeys { this._thumbprint = crypto .createHash("sha512") .update(publicKey) - .digest("hex"); + .digest(EncodingTypes.HEX); } } diff --git a/lib/msal-node/test/utils/TestConstants.ts b/lib/msal-node/test/utils/TestConstants.ts index 5331fcbc4d..cc59752386 100644 --- a/lib/msal-node/test/utils/TestConstants.ts +++ b/lib/msal-node/test/utils/TestConstants.ts @@ -111,7 +111,7 @@ export const DEFAULT_CRYPTO_IMPLEMENTATION: ICrypto = { async getPublicKeyThumbprint(): Promise { throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented); }, - async removeTokenBindingKey(): Promise { + async removeTokenBindingKey(): Promise { throw createClientAuthError(ClientAuthErrorCodes.methodNotImplemented); }, async clearKeystore(): Promise { diff --git a/lib/msal-react/CHANGELOG.json b/lib/msal-react/CHANGELOG.json index 08cd1e40b7..97d880ccdd 100644 --- a/lib/msal-react/CHANGELOG.json +++ b/lib/msal-react/CHANGELOG.json @@ -1,6 +1,269 @@ { "name": "@azure/msal-react", "entries": [ + { + "date": "Tue, 08 Jul 2025 14:21:12 GMT", + "version": "3.0.15", + "tag": "@azure/msal-react_v3.0.15", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump @azure/msal-browser to v4.15.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 01 Jul 2025 14:23:25 GMT", + "version": "3.0.14", + "tag": "@azure/msal-react_v3.0.14", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump @azure/msal-browser to v4.14.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + } + ] + } + }, + { + "date": "Wed, 18 Jun 2025 15:21:55 GMT", + "version": "3.0.13", + "tag": "@azure/msal-react_v3.0.13", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump @azure/msal-browser to v4.13.2", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 06 May 2025 22:47:43 GMT", + "version": "3.0.12", + "tag": "@azure/msal-react_v3.0.12", + "comments": { + "patch": [ + { + "author": "ethan.pearce@rightmove.co.uk", + "package": "@azure/msal-react", + "commit": "b6098464c80f3db124ca3db145446ddb71be936d", + "comment": "chore: Allow react 19 usage" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump @azure/msal-browser to v4.12.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + } + ], + "none": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-react", + "commit": "b6098464c80f3db124ca3db145446ddb71be936d", + "comment": "New dev dependency" + } + ] + } + }, + { + "date": "Tue, 29 Apr 2025 20:25:48 GMT", + "version": "3.0.11", + "tag": "@azure/msal-react_v3.0.11", + "comments": { + "patch": [ + { + "author": "thomas.norling@microsoft.com", + "package": "@azure/msal-react", + "commit": "2b6112d62495ac4e3245d8a0d9be6b541ffb206c", + "comment": "Fix race between cache initialization and state initialization" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump @azure/msal-browser to v4.11.1", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 15 Apr 2025 23:34:14 GMT", + "version": "3.0.10", + "tag": "@azure/msal-react_v3.0.10", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump @azure/msal-browser to v4.11.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 08 Apr 2025 16:56:06 GMT", + "version": "3.0.9", + "tag": "@azure/msal-react_v3.0.9", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump @azure/msal-browser to v4.10.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + } + ] + } + }, + { + "date": "Tue, 25 Mar 2025 22:29:34 GMT", + "version": "3.0.8", + "tag": "@azure/msal-react_v3.0.8", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump @azure/msal-browser to v4.9.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + } + ] + } + }, + { + "date": "Thu, 20 Mar 2025 22:09:04 GMT", + "version": "3.0.7", + "tag": "@azure/msal-react_v3.0.7", + "comments": { + "patch": [ + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump @azure/msal-browser to v4.8.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump eslint-config-msal to v0.0.0", + "commit": "not available" + }, + { + "author": "beachball", + "package": "@azure/msal-react", + "comment": "Bump msal-test-utils to v0.0.1", + "commit": "not available" + } + ] + } + }, { "date": "Tue, 11 Mar 2025 18:51:25 GMT", "version": "3.0.6", diff --git a/lib/msal-react/CHANGELOG.md b/lib/msal-react/CHANGELOG.md index d70f9d2351..05145c0762 100644 --- a/lib/msal-react/CHANGELOG.md +++ b/lib/msal-react/CHANGELOG.md @@ -1,9 +1,101 @@ # Change Log - @azure/msal-react - + +## 3.0.15 + +Tue, 08 Jul 2025 14:21:12 GMT + +### Patches + +- Bump @azure/msal-browser to v4.15.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 + +## 3.0.14 + +Tue, 01 Jul 2025 14:23:25 GMT + +### Patches + +- Bump @azure/msal-browser to v4.14.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 + +## 3.0.13 + +Wed, 18 Jun 2025 15:21:55 GMT + +### Patches + +- Bump @azure/msal-browser to v4.13.2 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 + +## 3.0.12 + +Tue, 06 May 2025 22:47:43 GMT + +### Patches + +- chore: Allow react 19 usage (ethan.pearce@rightmove.co.uk) +- Bump @azure/msal-browser to v4.12.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 + +## 3.0.11 + +Tue, 29 Apr 2025 20:25:48 GMT + +### Patches + +- Fix race between cache initialization and state initialization (thomas.norling@microsoft.com) +- Bump @azure/msal-browser to v4.11.1 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 + +## 3.0.10 + +Tue, 15 Apr 2025 23:34:14 GMT + +### Patches + +- Bump @azure/msal-browser to v4.11.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 + +## 3.0.9 + +Tue, 08 Apr 2025 16:56:06 GMT + +### Patches + +- Bump @azure/msal-browser to v4.10.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 + +## 3.0.8 + +Tue, 25 Mar 2025 22:29:34 GMT + +### Patches + +- Bump @azure/msal-browser to v4.9.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 + +## 3.0.7 + +Thu, 20 Mar 2025 22:09:04 GMT + +### Patches + +- Bump @azure/msal-browser to v4.8.0 +- Bump eslint-config-msal to v0.0.0 +- Bump msal-test-utils to v0.0.1 + ## 3.0.6 Tue, 11 Mar 2025 18:51:25 GMT diff --git a/lib/msal-react/README.md b/lib/msal-react/README.md index b5ed11dfaa..482b85c15e 100644 --- a/lib/msal-react/README.md +++ b/lib/msal-react/README.md @@ -4,8 +4,8 @@ [![npm version](https://img.shields.io/npm/dm/@azure/msal-react.svg)](https://nodei.co/npm/@azure/msal-react/) [![codecov](https://codecov.io/gh/AzureAD/microsoft-authentication-library-for-js/branch/dev/graph/badge.svg?flag=msal-react)](https://codecov.io/gh/AzureAD/microsoft-authentication-library-for-js) -| Getting Started | AAD Docs | Library Reference | Samples -|--- | --- | --- | --- | +| Getting Started | AAD Docs | Library Reference | Samples | +| ------------------------------------------------------------------------------------------------------------------------ | -------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | 1. [About](#about) 1. [FAQ](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/FAQ.md) @@ -32,16 +32,16 @@ The `@azure/msal-react` package described by the code in this folder uses the [` ## Prerequisites -- `@azure/msal-react` is meant to be used in [Single-Page Application scenarios](https://docs.microsoft.com/azure/active-directory/develop/scenario-spa-overview). +- `@azure/msal-react` is meant to be used in [Single-Page Application scenarios](https://docs.microsoft.com/azure/active-directory/develop/scenario-spa-overview). -- Before using `@azure/msal-react` you will need to [register a Single Page Application in Azure AD](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-spa-app-registration) to get a valid `clientId` for configuration, and to register the routes that your app will accept redirect traffic on. +- Before using `@azure/msal-react` you will need to [register a Single Page Application in Azure AD](https://docs.microsoft.com/en-us/azure/active-directory/develop/scenario-spa-app-registration) to get a valid `clientId` for configuration, and to register the routes that your app will accept redirect traffic on. ## Version Support -| MSAL React version | MSAL support status | Supported React versions | -| ------------------ | ------------------- | -------------------------- | -| MSAL React v2 | Active development | 16, 17, 18 | -| MSAL React v1 | In maintenance | 16, 17, 18 | +| MSAL React version | MSAL support status | Supported React versions | +| ------------------ | ------------------- | ------------------------ | +| MSAL React v3 | Active development | 16, 17, 18, 19 | +| MSAL React v1, v2 | In maintenance | 16, 17, 18 | **Note:** There have been no functional changes in the MSAL React v2 release. @@ -111,11 +111,11 @@ Since `@azure/msal-react` is a wrapper around `@azure/msal-browser` many docs fr ### Advanced Topics -- [Configuration Options](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md) -- [Request and Response Details](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md) -- [Events](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/events.md) -- [Cache Storage](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/caching.md) -- [Performance Enhancements](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/performance.md) +- [Configuration Options](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/configuration.md) +- [Request and Response Details](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md) +- [Events](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/events.md) +- [Cache Storage](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/caching.md) +- [Performance Enhancements](https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/performance.md) ### MSAL React Specific Concepts @@ -128,18 +128,18 @@ Since `@azure/msal-react` is a wrapper around `@azure/msal-browser` many docs fr Our [samples directory](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples) contains several example apps you can spin up to see how this library can be used in different contexts. -- [Create React App (JS) Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/react-router-sample) -- [Create React App (TS) Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/typescript-sample) -- [Next.js Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/nextjs-sample) -- [Gatsby Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/gatsby-sample) -- [B2C Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/b2c-sample) +- [Create React App (JS) Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/react-router-sample) +- [Create React App (TS) Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/typescript-sample) +- [Next.js Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/nextjs-sample) +- [Gatsby Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/gatsby-sample) +- [B2C Sample](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/samples/msal-react-samples/b2c-sample) More advanced samples backed with a tutorial can be found in the [Azure Samples](https://github.com/Azure-Samples) space on GitHub: -- [React SPA calling Express.js web API](https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/3-Authorization-II/1-call-api) -- [React SPA calling Express.js web API using App Roles and RBAC](https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/5-AccessControl/1-call-api-roles) -- [React SPA calling Microsoft Graph via Express.js web API using on-behalf-of flow](https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/6-AdvancedScenarios/1-call-api-obo) -- [Deployment tutorial for Azure Static Web Apps](https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/4-Deployment/2-deploy-static) +- [React SPA calling Express.js web API](https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/3-Authorization-II/1-call-api) +- [React SPA calling Express.js web API using App Roles and RBAC](https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/5-AccessControl/1-call-api-roles) +- [React SPA calling Microsoft Graph via Express.js web API using on-behalf-of flow](https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/6-AdvancedScenarios/1-call-api-obo) +- [Deployment tutorial for Azure Static Web Apps](https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/4-Deployment/2-deploy-static) ## Security Reporting @@ -147,7 +147,7 @@ If you find a security issue with our libraries or services please report it to ## License -Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. +Copyright (c) Microsoft Corporation. All rights reserved. Licensed under the MIT License. ## We Value and Adhere to the Microsoft Open Source Code of Conduct diff --git a/lib/msal-react/apiReview/msal-react.api.md b/lib/msal-react/apiReview/msal-react.api.md index 10a90837cf..e7e4eadbfa 100644 --- a/lib/msal-react/apiReview/msal-react.api.md +++ b/lib/msal-react/apiReview/msal-react.api.md @@ -153,7 +153,7 @@ export function useMsalAuthentication(interactionType: InteractionType, authenti // Warning: (ae-missing-release-tag) "version" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) // // @public (undocumented) -export const version = "3.0.6"; +export const version = "3.0.15"; // Warning: (tsdoc-param-tag-missing-hyphen) The @param block should be followed by a parameter name and then a hyphen // Warning: (ae-missing-release-tag) "withMsal" is part of the package's API, but it is missing a release tag (@alpha, @beta, @public, or @internal) diff --git a/lib/msal-react/package.json b/lib/msal-react/package.json index 75246e5365..ed32e5ada2 100644 --- a/lib/msal-react/package.json +++ b/lib/msal-react/package.json @@ -1,6 +1,6 @@ { "name": "@azure/msal-react", - "version": "3.0.6", + "version": "3.0.15", "author": { "name": "Microsoft", "email": "nugetaad@microsoft.com", @@ -56,26 +56,27 @@ "apiExtractor": "api-extractor run" }, "peerDependencies": { - "@azure/msal-browser": "^4.7.0", - "react": "^16.8.0 || ^17 || ^18" + "@azure/msal-browser": "^4.15.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" }, "devDependencies": { - "@azure/msal-browser": "^4.7.0", + "@azure/msal-browser": "^4.15.0", "@microsoft/api-extractor": "^7.43.4", "@rollup/plugin-typescript": "^11.1.5", "@testing-library/jest-dom": "^5.11.5", - "@testing-library/react": "^13.4.0", + "@testing-library/react": "^16.3.0", "@types/jest": "^29.5.0", "@types/node": "^20.5.1", - "@types/react": "^18.2.13", - "@types/react-dom": "^18.2.6", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", "eslint-config-msal": "file:../../shared-configs/eslint-config-msal", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "jest-junit": "^16.0.0", "msal-test-utils": "file:../../shared-test-utils", "prettier": "2.8.7", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "rollup": "^4.22.4", "ts-jest": "^29.1.0", "ts-jest-resolver": "^2.0.1", diff --git a/lib/msal-react/src/MsalProvider.tsx b/lib/msal-react/src/MsalProvider.tsx index cfefb1b18f..2c95247931 100644 --- a/lib/msal-react/src/MsalProvider.tsx +++ b/lib/msal-react/src/MsalProvider.tsx @@ -85,6 +85,11 @@ const reducer = ( throw new Error(`Unknown action type: ${type}`); } + if (newInProgress === InteractionStatus.Startup) { + // Can't start checking accounts until initialization is complete + return previousState; + } + const currentAccounts = payload.instance.getAllAccounts(); if ( newInProgress !== previousState.inProgress && @@ -135,7 +140,7 @@ export function MsalProvider({ // Lazy initialization of the initial state return { inProgress: InteractionStatus.Startup, - accounts: instance.getAllAccounts(), + accounts: [], }; }); diff --git a/lib/msal-react/src/hooks/useAccount.ts b/lib/msal-react/src/hooks/useAccount.ts index 249ce23418..0e1e85c18d 100644 --- a/lib/msal-react/src/hooks/useAccount.ts +++ b/lib/msal-react/src/hooks/useAccount.ts @@ -8,6 +8,7 @@ import { AccountInfo, IPublicClientApplication, AccountEntity, + InteractionStatus, } from "@azure/msal-browser"; import { useMsal } from "./useMsal.js"; import { AccountIdentifiers } from "../types/AccountIdentifiers.js"; @@ -42,26 +43,32 @@ export function useAccount( ): AccountInfo | null { const { instance, inProgress, logger } = useMsal(); - const [account, setAccount] = useState(() => - getAccount(instance, accountIdentifiers) - ); + const [account, setAccount] = useState(() => { + if (inProgress === InteractionStatus.Startup) { + return null; + } else { + return getAccount(instance, accountIdentifiers); + } + }); useEffect(() => { - setAccount((currentAccount: AccountInfo | null) => { - const nextAccount = getAccount(instance, accountIdentifiers); - if ( - !AccountEntity.accountInfoIsEqual( - currentAccount, - nextAccount, - true - ) - ) { - logger.info("useAccount - Updating account"); - return nextAccount; - } + if (inProgress !== InteractionStatus.Startup) { + setAccount((currentAccount: AccountInfo | null) => { + const nextAccount = getAccount(instance, accountIdentifiers); + if ( + !AccountEntity.accountInfoIsEqual( + currentAccount, + nextAccount, + true + ) + ) { + logger.info("useAccount - Updating account"); + return nextAccount; + } - return currentAccount; - }); + return currentAccount; + }); + } }, [inProgress, accountIdentifiers, instance, logger]); return account; diff --git a/lib/msal-react/src/hooks/useMsalAuthentication.ts b/lib/msal-react/src/hooks/useMsalAuthentication.ts index 9a0f02de17..25408e6371 100644 --- a/lib/msal-react/src/hooks/useMsalAuthentication.ts +++ b/lib/msal-react/src/hooks/useMsalAuthentication.ts @@ -253,8 +253,8 @@ export function useMsalAuthentication( shouldAcquireToken.current && inProgress === InteractionStatus.None ) { - shouldAcquireToken.current = false; if (!isAuthenticated) { + shouldAcquireToken.current = false; logger.info( "useMsalAuthentication - No user is authenticated, attempting to login" ); @@ -263,6 +263,7 @@ export function useMsalAuthentication( return; }); } else if (account) { + shouldAcquireToken.current = false; logger.info( "useMsalAuthentication - User is authenticated, attempting to acquire token" ); diff --git a/lib/msal-react/src/packageMetadata.ts b/lib/msal-react/src/packageMetadata.ts index 7c87f5facb..84584d5910 100644 --- a/lib/msal-react/src/packageMetadata.ts +++ b/lib/msal-react/src/packageMetadata.ts @@ -1,3 +1,3 @@ /* eslint-disable header/header */ export const name = "@azure/msal-react"; -export const version = "3.0.6"; +export const version = "3.0.15"; diff --git a/package-lock.json b/package-lock.json index f6c3b3134e..4136bcaf24 100644 --- a/package-lock.json +++ b/package-lock.json @@ -52,12 +52,12 @@ }, "extensions/msal-node-extensions": { "name": "@azure/msal-node-extensions", - "version": "1.5.7", + "version": "1.5.17", "hasInstallScript": true, "license": "MIT", "dependencies": { - "@azure/msal-common": "15.2.1", - "@azure/msal-node-runtime": "^0.17.1", + "@azure/msal-common": "15.8.1", + "@azure/msal-node-runtime": "^0.18.1", "keytar": "^7.8.0" }, "devDependencies": { @@ -67,6 +67,7 @@ "@types/node": "^20.3.1", "eslint-config-msal": "file:../../shared-configs/eslint-config-msal", "jest": "^29.5.0", + "jest-junit": "^16.0.0", "msal-test-utils": "file:../../shared-test-utils", "node-addon-api": "^6.1.0", "rollup": "^4.22.4", @@ -91,11 +92,11 @@ } }, "extensions/samples/electron-webpack": { - "version": "1.0.0", + "version": "1.0.1", "license": "MIT", "dependencies": { - "@azure/msal-node": "^3.0.0", - "@azure/msal-node-extensions": "^1.0.10", + "@azure/msal-node": "^3.6.0", + "@azure/msal-node-extensions": "^1.5.14", "bootstrap": "^5.0.0", "electron-squirrel-startup": "^1.0.0" }, @@ -261,14 +262,14 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@azure/msal-node": "^3.0.0", - "@azure/msal-node-extensions": "^1.0.0", + "@azure/msal-node": "^3.6.0", + "@azure/msal-node-extensions": "^1.5.14", "express": "^4.20.0" } }, "lib/msal-angular": { "name": "@azure/msal-angular", - "version": "4.0.6", + "version": "4.0.15", "license": "MIT", "devDependencies": { "@angular-devkit/build-angular": "^15.1.5", @@ -282,7 +283,7 @@ "@angular/platform-browser": "^15.1.4", "@angular/platform-browser-dynamic": "^15.1.4", "@angular/router": "^15.1.4", - "@azure/msal-browser": "^4.7.0", + "@azure/msal-browser": "^4.15.0", "@types/jasmine": "~3.6.0", "@types/jasminewd2": "~2.0.3", "@types/node": "^12.11.1", @@ -303,7 +304,7 @@ "zone.js": "~0.11.8" }, "peerDependencies": { - "@azure/msal-browser": "^4.7.0", + "@azure/msal-browser": "^4.15.0", "rxjs": "^7.0.0" } }, @@ -350,10 +351,10 @@ }, "lib/msal-browser": { "name": "@azure/msal-browser", - "version": "4.7.0", + "version": "4.15.0", "license": "MIT", "dependencies": { - "@azure/msal-common": "15.2.1" + "@azure/msal-common": "15.8.1" }, "devDependencies": { "@azure/storage-blob": "^12.2.1", @@ -373,6 +374,7 @@ "fake-indexeddb": "^3.1.3", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "jest-junit": "^16.0.0", "msal-test-utils": "file:../../shared-test-utils", "prettier": "^2.8.7", "rimraf": "^3.0.0", @@ -406,7 +408,7 @@ }, "lib/msal-common": { "name": "@azure/msal-common", - "version": "15.2.1", + "version": "15.8.1", "license": "MIT", "devDependencies": { "@babel/core": "^7.7.2", @@ -422,6 +424,7 @@ "@types/node": "^20.3.1", "eslint-config-msal": "file:../../shared-configs/eslint-config-msal", "jest": "^29.5.0", + "jest-junit": "^16.0.0", "lodash": "^4.17.21", "msal-test-utils": "file:../../shared-test-utils", "prettier": "2.8.7", @@ -500,10 +503,10 @@ }, "lib/msal-node": { "name": "@azure/msal-node", - "version": "3.3.0", + "version": "3.6.3", "license": "MIT", "dependencies": { - "@azure/msal-common": "15.2.1", + "@azure/msal-common": "15.8.1", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -517,6 +520,7 @@ "@types/uuid": "^7.0.0", "eslint-config-msal": "file:../../shared-configs/eslint-config-msal", "jest": "^29.5.0", + "jest-junit": "^16.0.0", "prettier": "2.8.7", "rollup": "^4.22.4", "rollup-msal": "file:../../shared-configs/rollup-msal", @@ -546,25 +550,26 @@ }, "lib/msal-react": { "name": "@azure/msal-react", - "version": "3.0.6", + "version": "3.0.15", "license": "MIT", "devDependencies": { - "@azure/msal-browser": "^4.7.0", + "@azure/msal-browser": "^4.15.0", "@microsoft/api-extractor": "^7.43.4", "@rollup/plugin-typescript": "^11.1.5", "@testing-library/jest-dom": "^5.11.5", - "@testing-library/react": "^13.4.0", + "@testing-library/react": "^16.3.0", "@types/jest": "^29.5.0", "@types/node": "^20.5.1", - "@types/react": "^18.2.13", - "@types/react-dom": "^18.2.6", + "@types/react": "^19.1.2", + "@types/react-dom": "^19.1.2", "eslint-config-msal": "file:../../shared-configs/eslint-config-msal", "jest": "^29.5.0", "jest-environment-jsdom": "^29.5.0", + "jest-junit": "^16.0.0", "msal-test-utils": "file:../../shared-test-utils", "prettier": "2.8.7", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "rollup": "^4.22.4", "ts-jest": "^29.1.0", "ts-jest-resolver": "^2.0.1", @@ -575,8 +580,57 @@ "node": ">=10" }, "peerDependencies": { - "@azure/msal-browser": "^4.7.0", - "react": "^16.8.0 || ^17 || ^18" + "@azure/msal-browser": "^4.15.0", + "react": "^16.8.0 || ^17 || ^18 || ^19" + } + }, + "lib/msal-react/node_modules/@testing-library/dom": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-10.4.0.tgz", + "integrity": "sha512-pemlzrSESWbdAloYml3bAJMEfNh1Z7EduzqPKprCH5S341frlpYnUEW0H72dLxa6IsYr+mPno20GiSm+h9dEdQ==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.10.4", + "@babel/runtime": "^7.12.5", + "@types/aria-query": "^5.0.1", + "aria-query": "5.3.0", + "chalk": "^4.1.0", + "dom-accessibility-api": "^0.5.9", + "lz-string": "^1.5.0", + "pretty-format": "^27.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "lib/msal-react/node_modules/@testing-library/react": { + "version": "16.3.0", + "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-16.3.0.tgz", + "integrity": "sha512-kFSyxiEDwv1WLl2fgsq6pPBbw5aWKrsY2/noi1Id0TK0UParSF62oFQFGHXIyaG4pp2tEub/Zlel+fjjZILDsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.12.5" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@testing-library/dom": "^10.0.0", + "@types/react": "^18.0.0 || ^19.0.0", + "@types/react-dom": "^18.0.0 || ^19.0.0", + "react": "^18.0.0 || ^19.0.0", + "react-dom": "^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } } }, "lib/msal-react/node_modules/@types/node": { @@ -588,6 +642,17 @@ "undici-types": "~6.19.2" } }, + "lib/msal-react/node_modules/aria-query": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.3.0.tgz", + "integrity": "sha512-b0P0sZPKtyu8HkeRAfCq0IfURZK+SuwMjY1UXGBU27wpAiTwQAIlq56IbIO+ytk/JjS1fMR14ee5WBBfKi5J6A==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "dequal": "^2.0.3" + } + }, "node_modules/@adobe/css-tools": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/@adobe/css-tools/-/css-tools-4.4.1.tgz", @@ -760,6 +825,26 @@ } } }, + "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/build-webpack": { + "version": "0.1502.11", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.11.tgz", + "integrity": "sha512-OTONIRp770Jfems4+cULmtoeSzjnpx5UjV2EazojnhRXXBSJMWRMPvwD2QvQl9UO/6eOV3d2mgmP2xOZgc/D6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@angular-devkit/architect": "0.1502.11", + "rxjs": "6.6.7" + }, + "engines": { + "node": "^14.20.0 || ^16.13.0 || >=18.10.0", + "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", + "yarn": ">= 1.13.0" + }, + "peerDependencies": { + "webpack": "^5.30.0", + "webpack-dev-server": "^4.0.0" + } + }, "node_modules/@angular-devkit/build-angular/node_modules/@babel/core": { "version": "7.20.12", "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.20.12.tgz", @@ -1175,6 +1260,23 @@ "integrity": "sha512-1hqclzwYwjRDFLjcFxOM5AYkkG0rpFPpr1RLPMEuGczoS7YA8gLhy8SWXYRAA/XwfEHpfo3cw5JGioS32fnMRw==", "dev": true }, + "node_modules/@angular-devkit/build-angular/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, "node_modules/@angular-devkit/build-angular/node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -1413,47 +1515,162 @@ } } }, - "node_modules/@angular-devkit/build-angular/node_modules/yallist": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", - "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true - }, - "node_modules/@angular-devkit/build-webpack": { - "version": "0.1502.11", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1502.11.tgz", - "integrity": "sha512-OTONIRp770Jfems4+cULmtoeSzjnpx5UjV2EazojnhRXXBSJMWRMPvwD2QvQl9UO/6eOV3d2mgmP2xOZgc/D6w==", + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server": { + "version": "4.11.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", + "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1502.11", - "rxjs": "6.6.7" + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.1", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.1", + "ws": "^8.4.2" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": "^14.20.0 || ^16.13.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^4.0.0" + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } } }, - "node_modules/@angular-devkit/build-webpack/node_modules/rxjs": { - "version": "6.6.7", - "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", - "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", "dev": true, + "license": "MIT", "dependencies": { - "tslib": "^1.9.0" + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" }, "engines": { - "npm": ">=2.0.0" + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" } }, - "node_modules/@angular-devkit/build-webpack/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@angular-devkit/build-angular/node_modules/yallist": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", + "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "dev": true }, "node_modules/@angular-devkit/core": { @@ -2813,10 +3030,11 @@ "link": true }, "node_modules/@azure/msal-node-runtime": { - "version": "0.17.1", - "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.17.1.tgz", - "integrity": "sha512-qAfTg+iGJsg+XvD9nmknI63+XuoX32oT+SX4wJdFz7CS6ETVpSHoroHVaUmsTU1H7H0+q1/ZkP988gzPRMYRsg==", - "hasInstallScript": true + "version": "0.18.1", + "resolved": "https://registry.npmjs.org/@azure/msal-node-runtime/-/msal-node-runtime-0.18.1.tgz", + "integrity": "sha512-vaUkpSiXD33/iDyZt1VZDEyOxvlNMT5o9D4ruIqkUmULyKgUik0y86DK2dsqZql/LU04T5siuq1AMTus+15SvA==", + "hasInstallScript": true, + "license": "MIT" }, "node_modules/@azure/msal-react": { "resolved": "lib/msal-react", @@ -2847,22 +3065,24 @@ } }, "node_modules/@babel/code-frame": { - "version": "7.26.2", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.26.2.tgz", - "integrity": "sha512-RJlIHRueQgwWitWgF8OdFYGZX328Ax5BCemNGlqHfplnRT9ESi8JkFlvaVYbS+UubVY6dpv87Fs2u5M29iNFVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "license": "MIT", "dependencies": { - "@babel/helper-validator-identifier": "^7.25.9", + "@babel/helper-validator-identifier": "^7.27.1", "js-tokens": "^4.0.0", - "picocolors": "^1.0.0" + "picocolors": "^1.1.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/compat-data": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.26.5.tgz", - "integrity": "sha512-XvcZi1KWf88RVbF9wn8MN6tYFloU5qX8KjuF3E1PVBmJ9eypXfs4GRiJwLuTZL0iSnJUKn1BFPa5BPZZJyFzPg==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.27.5.tgz", + "integrity": "sha512-KiRAp/VoJaWkkte84TvUd9qjdbZAdiqyvMxrGl1N6vzFogKmaLgoM3L1kgtLicp2HP5fBJS8JrZKLVIZGVJAVg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } @@ -3049,12 +3269,13 @@ } }, "node_modules/@babel/helper-compilation-targets": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.26.5.tgz", - "integrity": "sha512-IXuyn5EkouFJscIDuFF5EsiSolseme1s0CZB+QxVugqJLYmKdxI1VfIBOst0SUu4rnk2Z7kqTwmoO1lp3HIfnA==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" @@ -3103,16 +3324,17 @@ } }, "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.25.9.tgz", - "integrity": "sha512-UTZQMvt0d/rSz6KI+qdu7GQze5TIajwTS++GUozlw8VBJDEOAqSXwm1WvmYEZwqdqSGQshRocPDqrt4HBZB3fQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.27.1.tgz", + "integrity": "sha512-QwGAmuvM17btKU5VqXfb+Giw4JcN0hjuufz3DYnpeVDvZLAObloM77bhMXiqry3Iio+Ai4phVRDwl6WU10+r5A==", + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/traverse": "^7.27.1", "semver": "^6.3.1" }, "engines": { @@ -3123,11 +3345,12 @@ } }, "node_modules/@babel/helper-create-class-features-plugin/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -3142,11 +3365,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.26.3.tgz", - "integrity": "sha512-G7ZRb40uUgdKOQqPLjfD12ZmGA54PzqDFUv2BKImnC9QIfGhIHKvVML0oN8IUiDq4iRqpq74ABpvOaerfWdong==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", + "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.1", "regexpu-core": "^6.2.0", "semver": "^6.3.1" }, @@ -3158,11 +3382,12 @@ } }, "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -3229,37 +3454,40 @@ } }, "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.25.9.tgz", - "integrity": "sha512-wbfdZ9w5vk0C0oyHqAJbc62+vet5prjj01jjJ8sKn3j9h3MQQlflEdXYvuqRWjHnM12coDEqiC1IRCi0U/EKwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", + "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-imports": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.25.9.tgz", - "integrity": "sha512-tnUA4RsrmflIM6W6RFTLFSXITtl0wKjgpnLgXyowocVPrbYrLUXSBXDgTs8BlbmIzIdlBySRQjINYs2BAkiLtw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.26.0.tgz", - "integrity": "sha512-xO+xu6B5K2czEnQye6BHA7DolFFmS3LB7stHZFaOLb1pAwO1HWLS8fXA+eh0A2yIvltPVmx3eNNDBJA2SLHXFw==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.27.3.tgz", + "integrity": "sha512-dSOvYwvyLsWBeIRyOeHXp5vPj5l1I011r52FM1+r1jCERv+aFXYk4whgQccYEGYxK2H3ZAIA8nuPkQ0HaUo3qg==", + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -3269,32 +3497,35 @@ } }, "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.25.9.tgz", - "integrity": "sha512-FIpuNaz5ow8VyrYcnXQTDRGvV6tTjkNtCK/RYNDXGSLlUD6cBuQTSw43CShGxjvfBTfcUA/r6UhUCbtYqkhcuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", + "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.26.5.tgz", - "integrity": "sha512-RS+jZcRdZdRFzMyr+wcsaqOmld1/EqTghfaBGQQd/WnRdzdlvSZ//kF7U8VQTxf1ynZ4cjUcYgjVGx13ewNPMg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.25.9.tgz", - "integrity": "sha512-IZtukuUeBbhgOcaW2s06OXTzVNJR0ybm4W5xC1opWFFJMZbwRj5LCk+ByYH7WdZPZTt8KnFwA8pvjN2yqcPlgw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", + "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-wrap-function": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-wrap-function": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3304,24 +3535,26 @@ } }, "node_modules/@babel/helper-remap-async-to-generator/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-replace-supers": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.26.5.tgz", - "integrity": "sha512-bJ6iIVdYX1YooY2X7w1q6VITt+LnUILtNk7zT78ykuwStx8BauCzxvFqFaHjOpW1bVnSUM1PN1f0p5P21wHxvg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", + "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", + "license": "MIT", "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.25.9", - "@babel/helper-optimise-call-expression": "^7.25.9", - "@babel/traverse": "^7.26.5" + "@babel/helper-member-expression-to-functions": "^7.27.1", + "@babel/helper-optimise-call-expression": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3331,12 +3564,13 @@ } }, "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.25.9.tgz", - "integrity": "sha512-K4Du3BFa3gvyhzgPcntrkDgZzQaq6uozzcpGbOO1OEJaI+EJdqWIMTLgFgQf6lrfiDFo5FU+BxKepI9RmZqahA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", + "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", + "license": "MIT", "dependencies": { - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3355,75 +3589,82 @@ } }, "node_modules/@babel/helper-string-parser": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.25.9.tgz", - "integrity": "sha512-4A/SCr/2KLd5jrtOMFzaKjVtAei3+2r/NChoBNoZ3EyP/+GlhoaEGoWOZUmFmoITP7zOJyHIMm+DYRd8o3PvHA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-identifier": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.25.9.tgz", - "integrity": "sha512-Ed61U6XJc3CVRfkERJWDz4dJwKe7iLmmJsbOGu9wSloNSFttHV0I8g6UAgb7qnK5ly5bGLPd4oXZlxCdANBOWQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", + "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-validator-option": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.25.9.tgz", - "integrity": "sha512-e/zv1co8pp55dNdEcCynfj9X7nyUKUXoUEwfXqaZt0omVOmDe9oOTdKStH4GmAw6zxMFs50ZayuMfHDKlO7Tfw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.25.9.tgz", - "integrity": "sha512-ETzz9UTjQSTmw39GboatdymDq4XIQbR8ySgVrylRhPOFpsd+JrKHIuF0de7GCWmem+T4uC5z7EZguod7Wj4A4g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.27.1.tgz", + "integrity": "sha512-NFJK2sHUvrjo8wAU/nQTWU890/zB2jj0qBcCbZbbf+005cAsv6tMjXz31fBign6M5ov1o0Bllu+9nbqkfsjjJQ==", + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-wrap-function/node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.26.7.tgz", - "integrity": "sha512-8NHiL98vsi0mbPQmYAGWwfcFaOy4j2HY49fXJCfuDcdE7fMIsH9a7GdaeXpIBsbT7307WU8KCMp5pUVDNL4f9A==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.27.6.tgz", + "integrity": "sha512-muE8Tt8M22638HU31A3CgfSUciwz1fhATfoVai05aPXGor//CdWDCbnlY1yvBPo07njuVOCNGCSp/GTt12lIug==", + "license": "MIT", "dependencies": { - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7" + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helpers/node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3508,11 +3749,12 @@ } }, "node_modules/@babel/parser": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.26.7.tgz", - "integrity": "sha512-kEvgGGgEjRUutvdVvZhbn/BxVt+5VSpwXz1j3WYXQbXDo8KzFOPNG2GQbdAiNq8g6wn1yKk7C/qrke03a84V+w==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.27.5.tgz", + "integrity": "sha512-OsQd175SxWkGlzbny8J3K8TnnDD0N3lrIUtB92xwyRpzaenGZhxDvxN/JgU00U3CDZNj9tPuDJ5H0WS4Nt3vKg==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.26.7" + "@babel/types": "^7.27.3" }, "bin": { "parser": "bin/babel-parser.js" @@ -3522,12 +3764,13 @@ } }, "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.25.9.tgz", - "integrity": "sha512-ZkRyVkThtxQ/J6nv3JFYv1RYY+JT5BvU0y3k5bWrmuG4woXypRa4PXmm9RhOwodRkYFWqC0C0cqcJ4OqR7kW+g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", + "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3537,11 +3780,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.25.9.tgz", - "integrity": "sha512-MrGRLZxLD/Zjj0gdU15dfs+HH/OXvnw/U4jJD8vpcP2CJQapPEv1IWwjc/qMg7ItBlPwSv1hRBbb7LeuANdcnw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", + "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3551,11 +3795,12 @@ } }, "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.25.9.tgz", - "integrity": "sha512-2qUwwfAFpJLZqxd02YW9btUCZHl+RFvdDkNfZwaIJrvB8Tesjsk8pEQkTvGwZXLqXUx/2oyY3ySRhm6HOXuCug==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", + "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3565,13 +3810,14 @@ } }, "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6xWgLZTJXwilVjlnV7ospI3xi+sl8lN8rXXbBD6vYn3UYDlGsag8wrZkKcSI8G6KgqKP7vNFaDgeDnfAABq61g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", + "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3581,12 +3827,13 @@ } }, "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.25.9.tgz", - "integrity": "sha512-aLnMXYPnzwwqhYSCyXfKkIkYgJ8zv9RK+roo9DkTXz38ynIhd9XCbN08s3MGvqL2MYGVUGdRQLL/JqBIeJhJBg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.27.1.tgz", + "integrity": "sha512-6BpaYGDavZqkI6yT+KSPdpZFfpnd68UKXbcjI9pJ13pvHhPrCKWOOLp+ysvMeA+DxnhuPpgIaRpxRxo5A9t5jw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3970,11 +4217,12 @@ } }, "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.26.0.tgz", - "integrity": "sha512-QCWT5Hh830hK5EQa7XzuqIkQU9tT/whqbDz7kuaZMHFl1inRRg7JnuAEOQ0Ur0QUl0NufCk1msK2BeY79Aj/eg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", + "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -3984,11 +4232,12 @@ } }, "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.26.0.tgz", - "integrity": "sha512-e2dttdsJ1ZTpi3B9UYGLw41hifAubg19AtCu/2I/F1QNVclOBr1dYpTdmdyZ84Xiz43BS/tCUkMAZNLv12Pi+A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", + "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4157,11 +4406,12 @@ } }, "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.25.9.tgz", - "integrity": "sha512-6jmooXYIwn9ca5/RylZADJ+EnSxVUS5sjeJ9UPk6RWRzXCmOJCy6dqItPJFpw2cuCangPK4OYr5uhGKcmrm5Qg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", + "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4171,13 +4421,14 @@ } }, "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.25.9.tgz", - "integrity": "sha512-RXV6QAzTBbhDMO9fWwOmwwTuYaiPbggWQ9INdZqAYeSHyG7FzQ+nOZaUUjNwKv9pV3aE4WFqFm1Hnbci5tBCAw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.27.1.tgz", + "integrity": "sha512-eST9RrwlpaoJBDHShc+DS2SG4ATTi2MYNb4OxYkf3n+7eb49LWpnS+HSpVfW4x927qQwgk8A2hGNVaajAEw0EA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4204,11 +4455,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.26.5.tgz", - "integrity": "sha512-chuTSY+hq09+/f5lMj8ZSYgCFpppV2CbYrhNFJ1BFoXpiWPnnAb7R0MqrafCpN8E1+YRrtM1MXZHJdIx8B6rMQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", + "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4218,11 +4470,12 @@ } }, "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.25.9.tgz", - "integrity": "sha512-1F05O7AYjymAtqbsFETboN1NvBdcnzMerO+zlMyJBEz6WkMdejvGWw9p05iTSjC85RLlBseHHQpYaM4gzJkBGg==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.27.5.tgz", + "integrity": "sha512-JF6uE2s67f0y2RZcm2kpAUEbD50vH62TyWVebxwHAlbSdM49VqPz8t4a1uIjp4NIOIZ4xzLfjY5emt/RCyC7TQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4232,12 +4485,13 @@ } }, "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.25.9.tgz", - "integrity": "sha512-bbMAII8GRSkcd0h0b4X+36GksxuheLFjP65ul9w6C3KgAamI3JqErNgSrosX6ZPj+Mpim5VvEbawXxJCyEUV3Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", + "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4247,12 +4501,13 @@ } }, "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.26.0.tgz", - "integrity": "sha512-6J2APTs7BDDm+UMqP1useWqhcRAXo0WIoVj26N7kPFB6S73Lgvyka4KTZYIxtgYXiN5HTyRObA72N2iu628iTQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.27.1.tgz", + "integrity": "sha512-s734HmYU78MVzZ++joYM+NkJusItbdRcbm+AGRgJCt3iA+yux0QpD9cBVdz3tKyrjVYWRl7j0mHSmv4lhV0aoA==", + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4262,15 +4517,16 @@ } }, "node_modules/@babel/plugin-transform-classes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.25.9.tgz", - "integrity": "sha512-mD8APIXmseE7oZvZgGABDyM34GUmK45Um2TXiBUt7PnuAxrgoSVf123qUzPxEr/+/BHrRn5NMZCdE2m/1F8DGg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.27.1.tgz", + "integrity": "sha512-7iLhfFAubmpeJe/Wo2TVuDrykh/zlWXLzPNdL0Jqn/Xu8R3QQ8h9ff8FQoISZOsw74/HFqFI7NX63HN7QFIHKA==", + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9", - "@babel/traverse": "^7.25.9", + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1", + "@babel/traverse": "^7.27.1", "globals": "^11.1.0" }, "engines": { @@ -4281,11 +4537,12 @@ } }, "node_modules/@babel/plugin-transform-classes/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" @@ -4300,12 +4557,13 @@ } }, "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.25.9.tgz", - "integrity": "sha512-HnBegGqXZR12xbcTHlJ9HGxw1OniltT26J5YpfruGqtUHlz/xKf/G2ak9e+t0rVqrjXa9WOhvYPz1ERfMj23AA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", + "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/template": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/template": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4315,24 +4573,26 @@ } }, "node_modules/@babel/plugin-transform-computed-properties/node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.25.9.tgz", - "integrity": "sha512-WkCGb/3ZxXepmMiX101nnGiU+1CAdut8oHyEOHxkKuS1qKpU2SMXE2uSvfz8PBuLd49V6LEsbtyPhWC7fnkgvQ==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.27.3.tgz", + "integrity": "sha512-s4Jrok82JpiaIprtY2nHsYmrThKvvwgHwjgd7UMiYhZaN0asdXNLr0y+NjTfkA7SyQE5i2Fb7eawUOZmLvyqOA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4342,12 +4602,13 @@ } }, "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.25.9.tgz", - "integrity": "sha512-t7ZQ7g5trIgSRYhI9pIJtRl64KHotutUJsh4Eze5l7olJv+mRSg4/MmbZ0tv1eeqRbdvo/+trvJD/Oc5DmW2cA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", + "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4357,11 +4618,12 @@ } }, "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.25.9.tgz", - "integrity": "sha512-LZxhJ6dvBb/f3x8xwWIuyiAHy56nrRG3PeYTpBkkzkYRRQ6tJLu68lEF5VIqMUZiAV7a8+Tb78nEoMCMcqjXBw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", + "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4371,12 +4633,13 @@ } }, "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-0UfuJS0EsXbRvKnwcLjFtJy/Sxc5J5jhLHnFhy7u4zih97Hz6tJkLU+O+FMMrNZrosUPxDi6sYxJ/EA8jDiAog==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4386,11 +4649,12 @@ } }, "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.25.9.tgz", - "integrity": "sha512-GCggjexbmSLaFhqsojeugBpeaRIgWNTcgKVq/0qIteFEqY2A+b9QidYadrWlnbWQUrW5fn+mCvf3tr7OeBFTyg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", + "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4400,11 +4664,12 @@ } }, "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.26.3.tgz", - "integrity": "sha512-7CAHcQ58z2chuXPWblnn1K6rLDnDWieghSOEmqQsrBenH0P9InCUtOJYD89pvngljmZlJcz3fcmgYsXFNGa1ZQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", + "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4414,11 +4679,12 @@ } }, "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.25.9.tgz", - "integrity": "sha512-2NsEz+CxzJIVOPx2o9UsW1rXLqtChtLoVnwYHHiB04wS5sgn7mrV45fWMBX0Kk+ub9uXytVYfNP2HjbVbCB3Ww==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", + "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4443,12 +4709,13 @@ } }, "node_modules/@babel/plugin-transform-for-of": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.25.9.tgz", - "integrity": "sha512-LqHxduHoaGELJl2uhImHwRQudhCM50pT46rIBNvtT/Oql3nqiS3wOwP+5ten7NpYSXrrVLgtZU3DZmPtWZo16A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", + "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4458,13 +4725,14 @@ } }, "node_modules/@babel/plugin-transform-function-name": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.25.9.tgz", - "integrity": "sha512-8lP+Yxjv14Vc5MuWBpJsoUCd3hD6V9DgBon2FVYL4jJgbnVQ9fTgYmonchzZJOVNgzEgbxp4OwAf6xz6M/14XA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", + "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4474,11 +4742,12 @@ } }, "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.25.9.tgz", - "integrity": "sha512-xoTMk0WXceiiIvsaquQQUaLLXSW1KJ159KP87VilruQm0LNNGxWzahxSS6T6i4Zg3ezp4vA4zuwiNUR53qmQAw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", + "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4488,11 +4757,12 @@ } }, "node_modules/@babel/plugin-transform-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.25.9.tgz", - "integrity": "sha512-9N7+2lFziW8W9pBl2TzaNht3+pgMIRP74zizeCSrtnSKVdUl8mAjjOP2OOVQAfZ881P2cNjDj1uAMEdeD50nuQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", + "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4502,11 +4772,12 @@ } }, "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.25.9.tgz", - "integrity": "sha512-wI4wRAzGko551Y8eVf6iOY9EouIDTtPb0ByZx+ktDGHwv6bHFimrgJM/2T021txPZ2s4c7bqvHbd+vXG6K948Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", + "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4516,11 +4787,12 @@ } }, "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.25.9.tgz", - "integrity": "sha512-PYazBVfofCQkkMzh2P6IdIUaCEWni3iYEerAsRWuVd8+jlM1S9S9cz1dF9hIzyoZ8IA3+OwVYIp9v9e+GbgZhA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", + "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4530,12 +4802,13 @@ } }, "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.25.9.tgz", - "integrity": "sha512-g5T11tnI36jVClQlMlt4qKDLlWnG5pP9CSM4GhdRciTNMRgkfpo5cR6b4rGIOYPgRRuFAvwjPQ/Yk+ql4dyhbw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", + "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4545,12 +4818,13 @@ } }, "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.26.3.tgz", - "integrity": "sha512-MgR55l4q9KddUDITEzEFYn5ZsGDXMSsU9E+kh7fjRXTIC3RHqfCo8RPRbyReYJh44HQ/yomFkqbOFohXvDCiIQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", + "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4560,14 +4834,15 @@ } }, "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.25.9.tgz", - "integrity": "sha512-hyss7iIlH/zLHaehT+xwiymtPOpsiwIIRlCAOwBB04ta5Tt+lNItADdlXw3jAWZ96VJ2jlhl/c+PNIQPKNfvcA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", + "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9", - "@babel/traverse": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4577,12 +4852,13 @@ } }, "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.25.9.tgz", - "integrity": "sha512-bS9MVObUgE7ww36HEfwe6g9WakQ0KF07mQF74uuXdkoziUPfKyu/nIm663kz//e5O1nPInPFx36z7WJmJ4yNEw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", + "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", + "license": "MIT", "dependencies": { - "@babel/helper-module-transforms": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4592,12 +4868,13 @@ } }, "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.25.9.tgz", - "integrity": "sha512-oqB6WHdKTGl3q/ItQhpLSnWWOpjUJLsOCLVyeFgeTktkBSCiurvPOsyt93gibI9CmuKvTUEtWmG5VhZD+5T/KA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", + "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4607,11 +4884,12 @@ } }, "node_modules/@babel/plugin-transform-new-target": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.25.9.tgz", - "integrity": "sha512-U/3p8X1yCSoKyUj2eOBIx3FOn6pElFOKvAAGf8HTtItuPyB+ZeOqfn+mvTtg9ZlOAjsPdK3ayQEjqHjU/yLeVQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", + "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4621,11 +4899,12 @@ } }, "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.26.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.26.6.tgz", - "integrity": "sha512-CKW8Vu+uUZneQCPtXmSBUC6NCAUdya26hWCElAWh5mVSlSRsmiCPUUDKb3Z0szng1hiAJa098Hkhg9o4SE35Qw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", + "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4635,11 +4914,12 @@ } }, "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.25.9.tgz", - "integrity": "sha512-TlprrJ1GBZ3r6s96Yq8gEQv82s8/5HnCVHtEJScUj90thHQbwe+E5MLhi2bbNHBEJuzrvltXSru+BUxHDoog7Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", + "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4649,13 +4929,15 @@ } }, "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.25.9.tgz", - "integrity": "sha512-fSaXafEE9CVHPweLYw4J0emp1t8zYTXyzN3UuG+lylqkvYd7RMrsOQ8TYx5RF231be0vqtFC6jnx3UmpJmKBYg==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.27.3.tgz", + "integrity": "sha512-7ZZtznF9g4l2JCImCo5LNKFHB5eXnN39lLtLY5Tg+VkR0jwOt7TBciMckuiQIOIW7L5tkQOCh3bVGYeXgMx52Q==", + "license": "MIT", "dependencies": { - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9" + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.3", + "@babel/plugin-transform-parameters": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4665,12 +4947,13 @@ } }, "node_modules/@babel/plugin-transform-object-super": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.25.9.tgz", - "integrity": "sha512-Kj/Gh+Rw2RNLbCK1VAWj2U48yxxqL2x0k10nPtSdRa0O2xnHXalD0s+o1A6a0W43gJ00ANo38jxkQreckOzv5A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", + "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-replace-supers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-replace-supers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4680,11 +4963,12 @@ } }, "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.25.9.tgz", - "integrity": "sha512-qM/6m6hQZzDcZF3onzIhZeDHDO43bkNNlOX0i8n3lR6zLbu0GN2d8qfM/IERJZYauhAHSLHy39NF0Ctdvcid7g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", + "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4694,12 +4978,13 @@ } }, "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.25.9.tgz", - "integrity": "sha512-6AvV0FsLULbpnXeBjrY4dmWF8F7gf8QnvTEoO/wX/5xm/xE1Xo8oPuD3MPS+KS9f9XBEAWN7X1aWr4z9HdOr7A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", + "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4709,11 +4994,12 @@ } }, "node_modules/@babel/plugin-transform-parameters": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.25.9.tgz", - "integrity": "sha512-wzz6MKwpnshBAiRmn4jR8LYz/g8Ksg0o80XmwZDlordjwEk9SxBzTWC7F5ef1jhbrbOW2DJ5J6ayRukrJmnr0g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.1.tgz", + "integrity": "sha512-018KRk76HWKeZ5l4oTj2zPpSh+NbGdt0st5S6x0pga6HgrjBOJb24mMDHorFopOOd6YHkLgOZ+zaCjZGPO4aKg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4723,12 +5009,13 @@ } }, "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.25.9.tgz", - "integrity": "sha512-D/JUozNpQLAPUVusvqMxyvjzllRaF8/nSrP1s2YGQT/W4LHK4xxsMcHjhOGTS01mp9Hda8nswb+FblLdJornQw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", + "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", + "license": "MIT", "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4738,13 +5025,14 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.25.9.tgz", - "integrity": "sha512-Evf3kcMqzXA3xfYJmZ9Pg1OvKdtqsDMSWBDzZOPLvHiTt36E75jLDQo5w1gtRU95Q4E5PDttrTf25Fw8d/uWLw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", + "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", + "license": "MIT", "dependencies": { - "@babel/helper-annotate-as-pure": "^7.25.9", - "@babel/helper-create-class-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-annotate-as-pure": "^7.27.1", + "@babel/helper-create-class-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4754,22 +5042,24 @@ } }, "node_modules/@babel/plugin-transform-private-property-in-object/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.3", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", + "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.3" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.25.9.tgz", - "integrity": "sha512-IvIUeV5KrS/VPavfSM/Iu+RE6llrHrYIKY1yfCzyO/lMXHQ+p7uGhonmGVisv6tSBSVgWzMBohTcvkC9vQcQFA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", + "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4876,12 +5166,12 @@ } }, "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.25.9.tgz", - "integrity": "sha512-vwDcDNsgMPDGP0nMqzahDWE5/MLcX8sv96+wfX7as7LoF/kr97Bo/7fI00lXY4wUXYfVmwIIyG80fGZ1uvt2qg==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.27.5.tgz", + "integrity": "sha512-uhB8yHerfe3MWnuLAhEbeQ4afVoqv8BQsPqrTv7e/jZ9y00kJL6l9a/f4OWaKxotmjzewfEyXE1vgDJenkQ2/Q==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "regenerator-transform": "^0.15.2" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4891,12 +5181,13 @@ } }, "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.26.0.tgz", - "integrity": "sha512-vN6saax7lrA2yA/Pak3sCxuD6F5InBjn9IcrIKQPjpsLvuHYLVroTxjdlVRHjjBWxKOqIwpTXDkOssYT4BFdRw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", + "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4906,11 +5197,12 @@ } }, "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.25.9.tgz", - "integrity": "sha512-7DL7DKYjn5Su++4RXu8puKZm2XBPHyjWLUidaPEkCUBbE7IPcsrkRHggAOOKydH1dASWdcUBxrkOGNxUv5P3Jg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", + "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4947,11 +5239,12 @@ } }, "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.25.9.tgz", - "integrity": "sha512-MUv6t0FhO5qHnS/W8XCbHmiRWOphNufpE1IVxhK5kuN3Td9FT1x4rx4K42s3RYdMXCXpfWkGSbCSd0Z64xA7Ng==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", + "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4961,12 +5254,13 @@ } }, "node_modules/@babel/plugin-transform-spread": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.25.9.tgz", - "integrity": "sha512-oNknIB0TbURU5pqJFVbOOFspVlrpVwo2H1+HUIsVDvp5VauGGDP1ZEvO8Nn5xyMEs3dakajOxlmkNW7kNgSm6A==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", + "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-skip-transparent-expression-wrappers": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4976,11 +5270,12 @@ } }, "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.25.9.tgz", - "integrity": "sha512-WqBUSgeVwucYDP9U/xNRQam7xV8W5Zf+6Eo7T2SRVUFlhRiMNFdFz58u0KZmCVVqs2i7SHgpRnAhzRNmKfi2uA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", + "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -4990,11 +5285,12 @@ } }, "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.25.9.tgz", - "integrity": "sha512-o97AE4syN71M/lxrCtQByzphAdlYluKPDBzDVzMmfCobUjjhAryZV0AIpRPrxN0eAkxXO6ZLEScmt+PNhj2OTw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", + "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -5004,11 +5300,12 @@ } }, "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.26.7.tgz", - "integrity": "sha512-jfoTXXZTgGg36BmhqT3cAYK5qkmqvJpvNrPhaK/52Vgjhw4Rq29s9UqpWWV0D6yuRmgiFH/BUVlkl96zJWqnaw==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", + "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.26.5" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -5047,11 +5344,12 @@ } }, "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.25.9.tgz", - "integrity": "sha512-s5EDrE6bW97LtxOcGj1Khcx5AaXwiMmi4toFWRDP9/y0Woo6pXC+iyPu/KuhKtfSrNFd7jJB+/fkOtZy6aIC6Q==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", + "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", + "license": "MIT", "dependencies": { - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -5061,12 +5359,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.25.9.tgz", - "integrity": "sha512-Jt2d8Ga+QwRluxRQ307Vlxa6dMrYEMZCgGxoPR8V52rxPyldHu3hdlHspxaqYmE7oID5+kB+UKUB/eWS+DkkWg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", + "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -5076,12 +5375,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.25.9.tgz", - "integrity": "sha512-yoxstj7Rg9dlNn9UQxzk4fcNivwv4nUYz7fYXBaKxvw/lnmPuOm/ikoELygbYq68Bls3D/D+NBPHiLwZdZZ4HA==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", + "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -5091,12 +5391,13 @@ } }, "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.25.9.tgz", - "integrity": "sha512-8BYqO3GeVNHtx69fdPshN3fnzUNLrWdHhk/icSwigksJGczKSizZ+Z6SBCxTs723Fr5VSNorTIK7a+R2tISvwQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", + "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", + "license": "MIT", "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9" + "@babel/helper-create-regexp-features-plugin": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -5106,78 +5407,79 @@ } }, "node_modules/@babel/preset-env": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.7.tgz", - "integrity": "sha512-Ycg2tnXwixaXOVb29rana8HNPgLVBof8qqtNQ9LE22IoyZboQbGSxI6ZySMdW3K5nAe6gu35IaJefUJflhUFTQ==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.27.2.tgz", + "integrity": "sha512-Ma4zSuYSlGNRlCLO+EAzLnCmJK2vdstgv+n7aUP+/IKZrOfWHOJVdSJtuub8RzHTj3ahD37k5OKJWvzf16TQyQ==", + "license": "MIT", "dependencies": { - "@babel/compat-data": "^7.26.5", - "@babel/helper-compilation-targets": "^7.26.5", - "@babel/helper-plugin-utils": "^7.26.5", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", + "@babel/compat-data": "^7.27.2", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", + "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.27.1", "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", + "@babel/plugin-syntax-import-assertions": "^7.27.1", + "@babel/plugin-syntax-import-attributes": "^7.27.1", "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.25.9", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.26.5", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.26.3", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.25.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.26.3", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.26.6", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.25.9", - "@babel/plugin-transform-typeof-symbol": "^7.26.7", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", + "@babel/plugin-transform-arrow-functions": "^7.27.1", + "@babel/plugin-transform-async-generator-functions": "^7.27.1", + "@babel/plugin-transform-async-to-generator": "^7.27.1", + "@babel/plugin-transform-block-scoped-functions": "^7.27.1", + "@babel/plugin-transform-block-scoping": "^7.27.1", + "@babel/plugin-transform-class-properties": "^7.27.1", + "@babel/plugin-transform-class-static-block": "^7.27.1", + "@babel/plugin-transform-classes": "^7.27.1", + "@babel/plugin-transform-computed-properties": "^7.27.1", + "@babel/plugin-transform-destructuring": "^7.27.1", + "@babel/plugin-transform-dotall-regex": "^7.27.1", + "@babel/plugin-transform-duplicate-keys": "^7.27.1", + "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-dynamic-import": "^7.27.1", + "@babel/plugin-transform-exponentiation-operator": "^7.27.1", + "@babel/plugin-transform-export-namespace-from": "^7.27.1", + "@babel/plugin-transform-for-of": "^7.27.1", + "@babel/plugin-transform-function-name": "^7.27.1", + "@babel/plugin-transform-json-strings": "^7.27.1", + "@babel/plugin-transform-literals": "^7.27.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", + "@babel/plugin-transform-member-expression-literals": "^7.27.1", + "@babel/plugin-transform-modules-amd": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-modules-systemjs": "^7.27.1", + "@babel/plugin-transform-modules-umd": "^7.27.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", + "@babel/plugin-transform-new-target": "^7.27.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", + "@babel/plugin-transform-numeric-separator": "^7.27.1", + "@babel/plugin-transform-object-rest-spread": "^7.27.2", + "@babel/plugin-transform-object-super": "^7.27.1", + "@babel/plugin-transform-optional-catch-binding": "^7.27.1", + "@babel/plugin-transform-optional-chaining": "^7.27.1", + "@babel/plugin-transform-parameters": "^7.27.1", + "@babel/plugin-transform-private-methods": "^7.27.1", + "@babel/plugin-transform-private-property-in-object": "^7.27.1", + "@babel/plugin-transform-property-literals": "^7.27.1", + "@babel/plugin-transform-regenerator": "^7.27.1", + "@babel/plugin-transform-regexp-modifiers": "^7.27.1", + "@babel/plugin-transform-reserved-words": "^7.27.1", + "@babel/plugin-transform-shorthand-properties": "^7.27.1", + "@babel/plugin-transform-spread": "^7.27.1", + "@babel/plugin-transform-sticky-regex": "^7.27.1", + "@babel/plugin-transform-template-literals": "^7.27.1", + "@babel/plugin-transform-typeof-symbol": "^7.27.1", + "@babel/plugin-transform-unicode-escapes": "^7.27.1", + "@babel/plugin-transform-unicode-property-regex": "^7.27.1", + "@babel/plugin-transform-unicode-regex": "^7.27.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", "@babel/preset-modules": "0.1.6-no-external-plugins", "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", + "babel-plugin-polyfill-corejs3": "^0.11.0", "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.38.1", + "core-js-compat": "^3.40.0", "semver": "^6.3.1" }, "engines": { @@ -5214,13 +5516,14 @@ } }, "node_modules/@babel/preset-env/node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", + "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", + "license": "MIT", "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-remap-async-to-generator": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -5243,12 +5546,13 @@ } }, "node_modules/@babel/preset-env/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.11.1.tgz", + "integrity": "sha512-yGCqvBT4rwMczo28xkH/noxJ6MZ4nJfkVYdoDaC/utLtWrXxv27HVrzAeSbqR8SxDsp46n0YF47EbHoixy6rXQ==", + "license": "MIT", "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" + "@babel/helper-define-polyfill-provider": "^0.6.3", + "core-js-compat": "^3.40.0" }, "peerDependencies": { "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" @@ -5383,15 +5687,16 @@ } }, "node_modules/@babel/traverse": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.26.7.tgz", - "integrity": "sha512-1x1sgeyRLC3r5fQOM0/xtQKsYjyxmFjaOrLJNtZ81inNjyJHGIolTULPiSc/2qe1/qfpFLisLQYFnnZl7QoedA==", + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.27.4.tgz", + "integrity": "sha512-oNcu2QbHqts9BtOWJosOVJapWjBDSxGCpFvikNR5TGDYDQf3JwpIoMzIKrvfoti93cLfPJEG4tH9SPVeyCGgdA==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.26.2", - "@babel/generator": "^7.26.5", - "@babel/parser": "^7.26.7", - "@babel/template": "^7.25.9", - "@babel/types": "^7.26.7", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/types": "^7.27.3", "debug": "^4.3.1", "globals": "^11.1.0" }, @@ -5400,12 +5705,13 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/generator": { - "version": "7.26.5", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.5.tgz", - "integrity": "sha512-2caSP6fN9I7HOe6nqhtft7V4g7/V/gfDsC3Ag4W7kEzzvRGKqiv0pu0HogPiZ3KaVSoNDhUws6IJjDjpfmYIXw==", + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.5", - "@babel/types": "^7.26.5", + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -5415,13 +5721,14 @@ } }, "node_modules/@babel/traverse/node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -5431,6 +5738,7 @@ "version": "0.3.8", "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -5468,6 +5776,7 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -5476,12 +5785,13 @@ } }, "node_modules/@babel/types": { - "version": "7.26.7", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.26.7.tgz", - "integrity": "sha512-t8kDRGrKXyp6+tjUh7hw2RLyclsW4TRoRvRHtSyAX9Bb5ldlFh+90YAYY6awRXrlB4G5G2izNeGySpATlFzmOg==", + "version": "7.27.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.27.6.tgz", + "integrity": "sha512-ETyHEk2VHHvl9b9jZP5IHPavHYk57EhanlRRuae9XCpb/j5bDCbPPMOBfCWhnl/7EDJz0jEMCi/RhccCE8r1+Q==", + "license": "MIT", "dependencies": { - "@babel/helper-string-parser": "^7.25.9", - "@babel/helper-validator-identifier": "^7.25.9" + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1" }, "engines": { "node": ">=6.9.0" @@ -6139,6 +6449,122 @@ } } }, + "node_modules/@electron-forge/plugin-webpack/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, + "node_modules/@electron-forge/plugin-webpack/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/@electron-forge/plugin-webpack/node_modules/webpack-dev-server": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.4", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/@electron-forge/plugin-webpack/node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/@electron-forge/publisher-base": { "version": "6.4.2", "resolved": "https://registry.npmjs.org/@electron-forge/publisher-base/-/publisher-base-6.4.2.tgz", @@ -6682,6 +7108,16 @@ "vue": "^3.2.0" } }, + "node_modules/@emnapi/runtime": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.4.3.tgz", + "integrity": "sha512-pBPWdu6MLKROBX05wSNKcNb++m5Er+KQ9QkB+WVM+pW2Kx9hoSrVTnu3BdkI5eBLZoKu/J6mW/B6i6bJB2ytXQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@emotion/babel-plugin": { "version": "11.13.5", "resolved": "https://registry.npmjs.org/@emotion/babel-plugin/-/babel-plugin-11.13.5.tgz", @@ -6843,13 +7279,14 @@ "integrity": "sha512-snKqtPW01tN0ui7yu9rGv69aJXr/a/Ywvl11sUjNtEcRc+ng/mQriFL0wLXMef74iHa/EkftbDzU9F8iFbH+zg==" }, "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.5.tgz", + "integrity": "sha512-9o3TMmpmftaCMepOdA5k/yDw8SfInyzWWTjYTFCX3kPSDJMROQTb8jg+h9Cnwnmm1vOzvxN7gIfB5V2ewpjtGA==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "aix" @@ -7115,13 +7552,14 @@ } }, "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.5.tgz", + "integrity": "sha512-pwHtMP9viAy1oHPvgxtOv+OkduK5ugofNTVDilIzBLpoWAM16r7b/mxBvfpuQDpRQFMfuVr5aLcn4yveGvBZvw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -7147,13 +7585,14 @@ } }, "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.5.tgz", + "integrity": "sha512-7A208+uQKgTxHd0G0uqZO8UjK2R0DDb4fDmERtARjSHWxqMTye4Erz4zZafx7Di9Cv+lNHYuncAkiGFySoD+Mw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -7406,6 +7845,383 @@ "integrity": "sha512-93zYdMES/c1D69yZiKDBj0V24vqNzB/koF26KPaagAfd3P/4gUlh3Dys5ogAK+Exi9QyzlD8x/08Zt7wIKcDcA==", "deprecated": "Use @eslint/object-schema instead" }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.1.tgz", + "integrity": "sha512-pn44xgBtgpEbZsu+lWf2KNb6OAf70X68k+yk69Ic2Xz11zHR/w24/U49XT7AeRwJ0Px+mhALhU5LPci1Aymk7A==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.1.tgz", + "integrity": "sha512-VfuYgG2r8BpYiOUN+BfYeFo69nP/MIwAtSJ7/Zpxc5QF3KS22z8Pvg3FkrSFJBPNQ7mmcUcYQFBmEQp7eu1F8Q==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.1.0.tgz", + "integrity": "sha512-HZ/JUmPwrJSoM4DIQPv/BfNh9yrOA8tlBbqbLz4JZ5uew2+o22Ik+tHQJcih7QJuSa0zo5coHTfD5J8inqj9DA==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.1.0.tgz", + "integrity": "sha512-Xzc2ToEmHN+hfvsl9wja0RlnXEgpKNmftriQp6XzY/RaSfwD9th+MSh0WQKzUreLKKINb3afirxW7A0fz2YWuQ==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.1.0.tgz", + "integrity": "sha512-s8BAd0lwUIvYCJyRdFqvsj+BJIpDBSxs6ivrOPm/R7piTs5UIwY5OjXrP2bqXC9/moGsyRa37eYWYCOGVXxVrA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.1.0.tgz", + "integrity": "sha512-IVfGJa7gjChDET1dK9SekxFFdflarnUB8PwW8aGwEoF3oAsSDuNUTYS+SKDOyOJxQyDC1aPFMuRYLoDInyV9Ew==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.1.0.tgz", + "integrity": "sha512-tiXxFZFbhnkWE2LA8oQj7KYR+bWBkiV2nilRldT7bqoEZ4HiDOcePr9wVDAZPi/Id5fT1oY9iGnDq20cwUz8lQ==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.1.0.tgz", + "integrity": "sha512-xukSwvhguw7COyzvmjydRb3x/09+21HykyapcZchiCUkTThEQEOMtBj9UhkaBRLuBrgLFzQ2wbxdeCCJW/jgJA==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.1.0.tgz", + "integrity": "sha512-yRj2+reB8iMg9W5sULM3S74jVS7zqSzHG3Ol/twnAAkAhnGQnpjj6e4ayUz7V+FpKypwgs82xbRdYtchTTUB+Q==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.1.0.tgz", + "integrity": "sha512-jYZdG+whg0MDK+q2COKbYidaqW/WTz0cc1E+tMAusiDygrM4ypmSCjOJPmFTvHHJ8j/6cAGyeDWZOsK06tP33w==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.1.0.tgz", + "integrity": "sha512-wK7SBdwrAiycjXdkPnGCPLjYb9lD4l6Ze2gSdAGVZrEL05AOUJESWU2lhlC+Ffn5/G+VKuSm6zzbQSzFX/P65A==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.1.tgz", + "integrity": "sha512-anKiszvACti2sGy9CirTlNyk7BjjZPiML1jt2ZkTdcvpLU1YH6CXwRAZCA2UmRXnhiIftXQ7+Oh62Ji25W72jA==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.1.tgz", + "integrity": "sha512-kX2c+vbvaXC6vly1RDf/IWNXxrlxLNpBVWkdpRq5Ka7OOKj6nr66etKy2IENf6FtOgklkg9ZdGpEu9kwdlcwOQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.1.tgz", + "integrity": "sha512-7s0KX2tI9mZI2buRipKIw2X1ufdTeaRgwmRabt5bi9chYfhur+/C1OXg3TKg/eag1W+6CCWLVmSauV1owmRPxA==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.1.0" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.1.tgz", + "integrity": "sha512-wExv7SH9nmoBW3Wr2gvQopX1k8q2g5V5Iag8Zk6AVENsjwd+3adjwxtp3Dcu2QhOXr8W9NusBU6XcQUohBZ5MA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.1.tgz", + "integrity": "sha512-DfvyxzHxw4WGdPiTF0SOHnm11Xv4aQexvqhRDAoD00MzHekAj9a/jADXeXYCDFH/DzYruwHbXU7uz+H+nWmSOQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.1.tgz", + "integrity": "sha512-pax/kTR407vNb9qaSIiWVnQplPcGU8LRIJpDT5o8PdAx5aAA7AS3X9PS8Isw1/WfqgQorPotjrZL3Pqh6C5EBg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.1.0" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.1.tgz", + "integrity": "sha512-YDybQnYrLQfEpzGOQe7OKcyLUCML4YOXl428gOOzBgN6Gw0rv8dpsJ7PqTHxBnXnwXr8S1mYFSLSa727tpz0xg==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.4.0" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.1.tgz", + "integrity": "sha512-WKf/NAZITnonBf3U1LfdjoMgNO5JYRSlhovhRhMxXVdvWYveM4kM3L8m35onYIdh75cOMCo1BexgVQcCDzyoWw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.1.tgz", + "integrity": "sha512-hw1iIAHpNE8q3uMIRCgGOeDoz9KtFNarFLQclLxr/LK1VBkj8nby18RjFvr6aP7USRYAjTZW6yisnBWMX571Tw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, "node_modules/@inquirer/checkbox": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-1.5.2.tgz", @@ -7941,10 +8757,11 @@ } }, "node_modules/@inquirer/figures": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.9.tgz", - "integrity": "sha512-BXvGj0ehzrngHTPTDqUoDT3NXL8U0RxUk2zJm2A66RhCEIWdtU1v6GuUqNAgArW4PQ9CinqIWyHdQgdwOj06zQ==", + "version": "1.0.12", + "resolved": "https://registry.npmjs.org/@inquirer/figures/-/figures-1.0.12.tgz", + "integrity": "sha512-MJttijd8rMFcKJC8NYmprWr6hD3r9Gd9qUC0XwPNwoEPWSMVJwA2MlXxF+nhZZNMY+HXsWa+o7KY2emWYIn0jQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" } @@ -8059,51 +8876,71 @@ } }, "node_modules/@inquirer/number": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.6.tgz", - "integrity": "sha512-xO07lftUHk1rs1gR0KbqB+LJPhkUNkyzV/KhH+937hdkMazmAYHLm1OIrNKpPelppeV1FgWrgFDjdUD8mM+XUg==", + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-3.0.15.tgz", + "integrity": "sha512-xWg+iYfqdhRiM55MvqiTCleHzszpoigUpN5+t1OMcRkJrUrw7va3AzXaxvS+Ak7Gny0j2mFSTv2JJj8sMtbV2g==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2" + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" }, "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/number/node_modules/@inquirer/core": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.4.tgz", - "integrity": "sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==", + "version": "10.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", + "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", - "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/number/node_modules/@inquirer/type": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.2.tgz", - "integrity": "sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/number/node_modules/cli-width": { @@ -8111,6 +8948,7 @@ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, + "license": "ISC", "engines": { "node": ">= 12" } @@ -8120,6 +8958,7 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -8129,6 +8968,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -8141,6 +8981,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8485,14 +9326,15 @@ } }, "node_modules/@inquirer/search": { - "version": "3.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.6.tgz", - "integrity": "sha512-eFZ2hiAq0bZcFPuFFBmZEtXU1EarHLigE+ENCtpO+37NHCl4+Yokq1P/d09kUblObaikwfo97w+0FtG/EXl5Ng==", + "version": "3.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-3.0.15.tgz", + "integrity": "sha512-YBMwPxYBrADqyvP4nNItpwkBnGGglAvCLVW8u4pRmmvOsHUtCAUIMbUrLX5B3tFL1/WsLGdQ2HNzkqswMs5Uaw==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -8500,38 +9342,57 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/search/node_modules/@inquirer/core": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.4.tgz", - "integrity": "sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==", + "version": "10.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", + "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", - "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/search/node_modules/@inquirer/type": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.2.tgz", - "integrity": "sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/@inquirer/search/node_modules/cli-width": { @@ -8539,6 +9400,7 @@ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, + "license": "ISC", "engines": { "node": ">= 12" } @@ -8548,6 +9410,7 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -8557,6 +9420,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -8569,6 +9433,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -8804,6 +9669,7 @@ "resolved": "https://registry.npmjs.org/@isaacs/fs-minipass/-/fs-minipass-4.0.1.tgz", "integrity": "sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^7.0.4" }, @@ -8816,6 +9682,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -9419,7 +10286,7 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/@jsonjoy.com/base64/-/base64-1.1.2.tgz", "integrity": "sha512-q6XAnWQDIMA3+FTiOYajoYqySkO+JSat0ytXGSuRdq9uXE7o92gzuQwQM14xaCRlBLGq3v5miDGC4vkVTn54xA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10.0" }, @@ -9435,7 +10302,7 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/@jsonjoy.com/json-pack/-/json-pack-1.1.1.tgz", "integrity": "sha512-osjeBqMJ2lb/j/M8NCPjs1ylqWIcTRTycIhVB5pt6LgzgeRSb0YRZ7j9RfA8wIUrsr/medIuhVyonXRZWLyfdw==", - "dev": true, + "devOptional": true, "dependencies": { "@jsonjoy.com/base64": "^1.1.1", "@jsonjoy.com/util": "^1.1.2", @@ -9457,7 +10324,7 @@ "version": "1.5.0", "resolved": "https://registry.npmjs.org/@jsonjoy.com/util/-/util-1.5.0.tgz", "integrity": "sha512-ojoNsrIuPI9g6o8UxhraZQSyF2ByJanAY4cTFbc8Mf2AXEF4aQRGY1dJxyJpuyav8r9FGflEt/Ff3u5Nt6YMPA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10.0" }, @@ -9475,10 +10342,11 @@ "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==" }, "node_modules/@listr2/prompt-adapter-inquirer": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.18.tgz", - "integrity": "sha512-0hz44rAcrphyXcA8IS7EJ2SCoaBZD2u5goE8S/e+q/DL+dOGpqpcLidVOFeLG3VgML62SXmfRLAhWt0zL1oW4Q==", + "version": "2.0.22", + "resolved": "https://registry.npmjs.org/@listr2/prompt-adapter-inquirer/-/prompt-adapter-inquirer-2.0.22.tgz", + "integrity": "sha512-hV36ZoY+xKL6pYOt1nPNnkciFkn89KZwqLhAFzJvYysAvL5uBQdiADZx/8bIDXIukzzwG0QlPYolgMzQUtKgpQ==", "dev": true, + "license": "MIT", "dependencies": { "@inquirer/type": "^1.5.5" }, @@ -9490,78 +10358,98 @@ } }, "node_modules/@lmdb/lmdb-darwin-arm64": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.2.2.tgz", - "integrity": "sha512-WBSJT9Z7DTol5viq+DZD2TapeWOw7mlwXxiSBHgAzqVwsaVb0h/ekMD9iu/jDD8MUA20tO9N0WEdnT06fsUp+g==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-arm64/-/lmdb-darwin-arm64-3.3.0.tgz", + "integrity": "sha512-LipbQobyEfQtu8WixasaFUZZ+JCGlho4OWwWIQ5ol0rB1RKkcZvypu7sS1CBvofBGVAa3vbOh8IOGQMrbmL5dg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@lmdb/lmdb-darwin-x64": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.2.2.tgz", - "integrity": "sha512-4S13kUtR7c/j/MzkTIBJCXv52hQ41LG2ukeaqw4Eng9K0pNKLFjo1sDSz96/yKhwykxrWDb13ddJ/ZqD3rAhUA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-darwin-x64/-/lmdb-darwin-x64-3.3.0.tgz", + "integrity": "sha512-yA+9P+ZeA3vg76BLXWeUomIAjxfmSmR2eg8fueHXDg5Xe1Xmkl9JCKuHXUhtJ+mMVcH12d5k4kJBLbyXTadfGQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@lmdb/lmdb-linux-arm": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.2.2.tgz", - "integrity": "sha512-uW31JmfuPAaLUYW7NsEU8gzwgDAzpGPwjvkxnKlcWd8iDutoPKDJi8Wk9lFmPEZRxVSB0j1/wDQ7N2qliR9UFA==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm/-/lmdb-linux-arm-3.3.0.tgz", + "integrity": "sha512-EDYrW9kle+8wI19JCj/PhRnGoCN9bked5cdOPdo1wdgH/HzjgoLPFTn9DHlZccgTEVhp3O+bpWXdN/rWySVvjw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@lmdb/lmdb-linux-arm64": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.2.2.tgz", - "integrity": "sha512-4hdgZtWI1idQlWRp+eleWXD9KLvObgboRaVoBj2POdPEYvsKANllvMW0El8tEQwtw74yB9NT6P8ENBB5UJf5+g==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-arm64/-/lmdb-linux-arm64-3.3.0.tgz", + "integrity": "sha512-OeWvSgjXXZ/zmtLqqL78I3910F6UYpUubmsUU+iBHo6nTtjkpXms95rJtGrjkWQqwswKBD7xSMplbYC4LEsiPA==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@lmdb/lmdb-linux-x64": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.2.2.tgz", - "integrity": "sha512-A0zjf4a2vM4B4GAx78ncuOTZ8Ka1DbTaG1Axf1e00Sa7f5coqlWiLg1PX7Gxvyibc2YqtqB+8tg1KKrE8guZVw==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-linux-x64/-/lmdb-linux-x64-3.3.0.tgz", + "integrity": "sha512-wDd02mt5ScX4+xd6g78zKBr6ojpgCJCTrllCAabjgap5FzuETqOqaQfKhO+tJuGWv/J5q+GIds6uY7rNFueOxg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, + "node_modules/@lmdb/lmdb-win32-arm64": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-arm64/-/lmdb-win32-arm64-3.3.0.tgz", + "integrity": "sha512-COotWhHJgzXULLiEjOgWQwqig6PoA+6ji6W+sDl6M1HhMXWIymEVHGs0edsVSNtsNSCAWMxJgR3asv6FNX/2EA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, "node_modules/@lmdb/lmdb-win32-x64": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.2.2.tgz", - "integrity": "sha512-Y0qoSCAja+xZE7QQ0LCHoYAuyI1n9ZqukQJa8lv9X3yCvWahFF7OYHAgVH1ejp43XWstj3U89/PAAzcowgF/uQ==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/@lmdb/lmdb-win32-x64/-/lmdb-win32-x64-3.3.0.tgz", + "integrity": "sha512-kqUgQH+l8HDbkAapx+aoko7Ez4X4DqkIraOqY/k0QY5EN/iialVlFpBUXh4wFXzirdmEVjbIUMrceUh0Kh8LeA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -11118,6 +12006,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -11131,6 +12020,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -11144,6 +12034,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -11157,6 +12048,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -11170,6 +12062,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -11183,6 +12076,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -11505,6 +12399,7 @@ "resolved": "https://registry.npmjs.org/@napi-rs/nice/-/nice-1.0.1.tgz", "integrity": "sha512-zM0mVWSXE0a0h9aKACLwKmD6nHcRiKrPpCfvaKqG1CqDEyjEawId0ocXxVzPMCAm6kkWr2P025msfxXEnt8UGQ==", "dev": true, + "license": "MIT", "optional": true, "engines": { "node": ">= 10" @@ -11540,6 +12435,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -11556,6 +12452,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -11572,6 +12469,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -11588,6 +12486,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -11604,6 +12503,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -11620,6 +12520,7 @@ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -11636,6 +12537,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -11652,6 +12554,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -11668,6 +12571,7 @@ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -11684,6 +12588,7 @@ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -11700,6 +12605,7 @@ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -11716,6 +12622,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -11732,6 +12639,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -11748,6 +12656,7 @@ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -11764,6 +12673,7 @@ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -11780,6 +12690,7 @@ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -11789,14 +12700,14 @@ } }, "node_modules/@next/env": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.23.tgz", - "integrity": "sha512-CysUC9IO+2Bh0omJ3qrb47S8DtsTKbFidGm6ow4gXIG6reZybqxbkH2nhdEm1tC8SmgzDdpq3BIML0PWsmyUYA==" + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/env/-/env-15.3.3.tgz", + "integrity": "sha512-OdiMrzCl2Xi0VTjiQQUK0Xh7bJHnOuET2s+3V+Y40WJBAXrJeGA3f+I8MZJ/YQ3mVGi5XGR1L66oFlgqXhQ4Vw==" }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.23.tgz", - "integrity": "sha512-WhtEntt6NcbABA8ypEoFd3uzq5iAnrl9AnZt9dXdO+PZLACE32z3a3qA5OoV20JrbJfSJ6Sd6EqGZTrlRnGxQQ==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-15.3.3.tgz", + "integrity": "sha512-WRJERLuH+O3oYB4yZNVahSVFmtxRNjNF1I1c34tYMoJb0Pve+7/RaLAJJizyYiFhjYNGHRAE1Ri2Fd23zgDqhg==", "cpu": [ "arm64" ], @@ -11809,9 +12720,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.23.tgz", - "integrity": "sha512-vwLw0HN2gVclT/ikO6EcE+LcIN+0mddJ53yG4eZd0rXkuEr/RnOaMH8wg/sYl5iz5AYYRo/l6XX7FIo6kwbw1Q==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-15.3.3.tgz", + "integrity": "sha512-XHdzH/yBc55lu78k/XwtuFR/ZXUTcflpRXcsu0nKmF45U96jt1tsOZhVrn5YH+paw66zOANpOnFQ9i6/j+UYvw==", "cpu": [ "x64" ], @@ -11824,9 +12735,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.23.tgz", - "integrity": "sha512-uuAYwD3At2fu5CH1wD7FpP87mnjAv4+DNvLaR9kiIi8DLStWSW304kF09p1EQfhcbUI1Py2vZlBO2VaVqMRtpg==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-15.3.3.tgz", + "integrity": "sha512-VZ3sYL2LXB8znNGcjhocikEkag/8xiLgnvQts41tq6i+wql63SMS1Q6N8RVXHw5pEUjiof+II3HkDd7GFcgkzw==", "cpu": [ "arm64" ], @@ -11839,9 +12750,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.23.tgz", - "integrity": "sha512-Mm5KHd7nGgeJ4EETvVgFuqKOyDh+UMXHXxye6wRRFDr4FdVRI6YTxajoV2aHE8jqC14xeAMVZvLqYqS7isHL+g==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-15.3.3.tgz", + "integrity": "sha512-h6Y1fLU4RWAp1HPNJWDYBQ+e3G7sLckyBXhmH9ajn8l/RSMnhbuPBV/fXmy3muMcVwoJdHL+UtzRzs0nXOf9SA==", "cpu": [ "arm64" ], @@ -11854,9 +12765,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.23.tgz", - "integrity": "sha512-Ybfqlyzm4sMSEQO6lDksggAIxnvWSG2cDWnG2jgd+MLbHYn2pvFA8DQ4pT2Vjk3Cwrv+HIg7vXJ8lCiLz79qoQ==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-15.3.3.tgz", + "integrity": "sha512-jJ8HRiF3N8Zw6hGlytCj5BiHyG/K+fnTKVDEKvUCyiQ/0r5tgwO7OgaRiOjjRoIx2vwLR+Rz8hQoPrnmFbJdfw==", "cpu": [ "x64" ], @@ -11869,9 +12780,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.23.tgz", - "integrity": "sha512-OSQX94sxd1gOUz3jhhdocnKsy4/peG8zV1HVaW6DLEbEmRRtUCUQZcKxUD9atLYa3RZA+YJx+WZdOnTkDuNDNA==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-15.3.3.tgz", + "integrity": "sha512-HrUcTr4N+RgiiGn3jjeT6Oo208UT/7BuTr7K0mdKRBtTbT4v9zJqCDKO97DUqqoBK1qyzP1RwvrWTvU6EPh/Cw==", "cpu": [ "x64" ], @@ -11884,9 +12795,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.23.tgz", - "integrity": "sha512-ezmbgZy++XpIMTcTNd0L4k7+cNI4ET5vMv/oqNfTuSXkZtSA9BURElPFyarjjGtRgZ9/zuKDHoMdZwDZIY3ehQ==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-15.3.3.tgz", + "integrity": "sha512-SxorONgi6K7ZUysMtRF3mIeHC5aA3IQLmKFQzU0OuhuUYwpOBc1ypaLJLP5Bf3M9k53KUUUj4vTPwzGvl/NwlQ==", "cpu": [ "arm64" ], @@ -11898,25 +12809,10 @@ "node": ">= 10" } }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.23.tgz", - "integrity": "sha512-zfHZOGguFCqAJ7zldTKg4tJHPJyJCOFhpoJcVxKL9BSUHScVDnMdDuOU1zPPGdOzr/GWxbhYTjyiEgLEpAoFPA==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">= 10" - } - }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.23.tgz", - "integrity": "sha512-xCtq5BD553SzOgSZ7UH5LH+OATQihydObTrCTvVzOro8QiWYKdBVwcB2Mn2MLMo6DGW9yH1LSPw7jS7HhgJgjw==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-15.3.3.tgz", + "integrity": "sha512-4QZG6F8enl9/S2+yIiOiju0iCTFd93d8VC1q9LZS4p/Xuk81W2QDjCFeoogmrWWkAD59z8ZxepBQap2dKS5ruw==", "cpu": [ "x64" ], @@ -12018,6 +12914,7 @@ "resolved": "https://registry.npmjs.org/@npmcli/agent/-/agent-3.0.0.tgz", "integrity": "sha512-S79NdEgDQd/NGCay6TCoVzXSj74skRZIKJcpJjC5lOq34SZzyI6MqtiiWoiVWoVrTcGjNeC4ipbh1VIHlpfF5Q==", "dev": true, + "license": "ISC", "dependencies": { "agent-base": "^7.1.0", "http-proxy-agent": "^7.0.0", @@ -12030,10 +12927,11 @@ } }, "node_modules/@npmcli/agent/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -12051,6 +12949,7 @@ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -12063,13 +12962,15 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/@npmcli/agent/node_modules/socks-proxy-agent": { "version": "8.0.5", "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", @@ -12183,10 +13084,11 @@ } }, "node_modules/@npmcli/package-json": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.1.1.tgz", - "integrity": "sha512-d5qimadRAUCO4A/Txw71VM7UrRZzV+NPclxz/dc+M6B2oYwjWTjqh8HA/sGQgs9VZuJ6I/P7XIAlJvgrl27ZOw==", + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/@npmcli/package-json/-/package-json-6.2.0.tgz", + "integrity": "sha512-rCNLSB/JzNvot0SEyXqWZ7tX2B5dD2a1br2Dp0vSYVo5jh8Z0EZ7lS9TsZ1UtziddB1UfNUaMCc538/HztnJGA==", "dev": true, + "license": "ISC", "dependencies": { "@npmcli/git": "^6.0.0", "glob": "^10.2.2", @@ -12201,17 +13103,17 @@ } }, "node_modules/@npmcli/package-json/node_modules/@npmcli/git": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.1.tgz", - "integrity": "sha512-BBWMMxeQzalmKadyimwb2/VVQyJB01PH0HhVSNLHNBDZN/M/h/02P6f8fxedIiFhpMj11SO9Ep5tKTBE7zL2nw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", + "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", "dev": true, + "license": "ISC", "dependencies": { "@npmcli/promise-spawn": "^8.0.0", "ini": "^5.0.0", "lru-cache": "^10.0.1", "npm-pick-manifest": "^10.0.0", "proc-log": "^5.0.0", - "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^5.0.0" @@ -12225,6 +13127,7 @@ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.2.tgz", "integrity": "sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ==", "dev": true, + "license": "ISC", "dependencies": { "which": "^5.0.0" }, @@ -12233,10 +13136,11 @@ } }, "node_modules/@npmcli/package-json/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } @@ -12246,6 +13150,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -12262,10 +13167,11 @@ } }, "node_modules/@npmcli/package-json/node_modules/hosted-git-info": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.2.tgz", - "integrity": "sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^10.0.1" }, @@ -12278,6 +13184,7 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -12287,6 +13194,7 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } @@ -12296,6 +13204,7 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-4.0.0.tgz", "integrity": "sha512-lR4MXjGNgkJc7tkQ97kb2nuEMnNCyU//XYVH0MKTGcXEiSudQ5MKGKen3C5QubYy0vmq+JGitUg92uuywGEwIA==", "dev": true, + "license": "MIT", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -12304,13 +13213,15 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "dev": true, + "license": "ISC" }, "node_modules/@npmcli/package-json/node_modules/minimatch": { "version": "9.0.5", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -12326,6 +13237,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -12335,6 +13247,7 @@ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.1.tgz", "integrity": "sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" }, @@ -12347,15 +13260,17 @@ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/package-json/node_modules/npm-package-arg": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.1.tgz", - "integrity": "sha512-aDxjFfPV3Liw0WOBWlyZLMBqtbgbg03rmGvHDJa2Ttv7tIz+1oB5qWec4psCDFZcZi9b5XdGkPdQiJxOPzvQRQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", + "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", "dev": true, + "license": "ISC", "dependencies": { "hosted-git-info": "^8.0.0", "proc-log": "^5.0.0", @@ -12371,6 +13286,7 @@ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", "dev": true, + "license": "ISC", "dependencies": { "npm-install-checks": "^7.1.0", "npm-normalize-package-bin": "^4.0.0", @@ -12386,15 +13302,17 @@ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@npmcli/package-json/node_modules/validate-npm-package-name": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", - "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.1.tgz", + "integrity": "sha512-OaI//3H0J7ZkR1OqlhGA8cA+Cbk/2xFOQpJOt5+s27/ta9eZwpeervh4Mxh4w0im/kdgktowaqVNR7QOrUd7Yg==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -12404,6 +13322,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -12442,10 +13361,11 @@ } }, "node_modules/@npmcli/redact": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.0.0.tgz", - "integrity": "sha512-/1uFzjVcfzqrgCeGW7+SZ4hv0qLWmKXVzFahZGJ6QuJBj6Myt9s17+JL86i76NV9YSnJRcGXJYQbAU0rn1YTCQ==", + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/@npmcli/redact/-/redact-3.2.2.tgz", + "integrity": "sha512-7VmYAmk4csGv08QzrDKScdzn11jHPFGyqJW39FyPgPuAp3zIaUmuCo1yxw9aGs+NEJuTGQ9Gwqpt93vtJubucg==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -13111,9 +14031,10 @@ } }, "node_modules/@puppeteer/browsers/node_modules/tar-fs": { - "version": "3.0.8", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz", - "integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.1.0.tgz", + "integrity": "sha512-5Mty5y/sOF1YWj1J6GiBodjlDc05CUR8PKXrsnFAiSG0xA+GHeWLovaZPYUDXkH/1iKRf2+M5+OrRgzC7O9b7w==", + "license": "MIT", "peer": true, "dependencies": { "pump": "^3.0.0", @@ -13420,261 +14341,295 @@ } }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.32.0.tgz", - "integrity": "sha512-G2fUQQANtBPsNwiVFg4zKiPQyjVKZCUdQUol53R8E71J7AsheRMV/Yv/nB8giOcOVqP7//eB5xPqieBYZe9bGg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.40.2.tgz", + "integrity": "sha512-JkdNEq+DFxZfUwxvB58tHMHBHVgX23ew41g1OQinthJ+ryhdRk67O31S7sYw8u2lTjHUPFxwar07BBt1KHp/hg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.32.0.tgz", - "integrity": "sha512-qhFwQ+ljoymC+j5lXRv8DlaJYY/+8vyvYmVx074zrLsu5ZGWYsJNLjPPVJJjhZQpyAKUGPydOq9hRLLNvh1s3A==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.40.2.tgz", + "integrity": "sha512-13unNoZ8NzUmnndhPTkWPWbX3vtHodYmy+I9kuLxN+F+l+x3LdVF7UCu8TWVMt1POHLh6oDHhnOA04n8oJZhBw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.32.0.tgz", - "integrity": "sha512-44n/X3lAlWsEY6vF8CzgCx+LQaoqWGN7TzUfbJDiTIOjJm4+L2Yq+r5a8ytQRGyPqgJDs3Rgyo8eVL7n9iW6AQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.40.2.tgz", + "integrity": "sha512-Gzf1Hn2Aoe8VZzevHostPX23U7N5+4D36WJNHK88NZHCJr7aVMG4fadqkIf72eqVPGjGc0HJHNuUaUcxiR+N/w==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.32.0.tgz", - "integrity": "sha512-F9ct0+ZX5Np6+ZDztxiGCIvlCaW87HBdHcozUfsHnj1WCUTBUubAoanhHUfnUHZABlElyRikI0mgcw/qdEm2VQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.40.2.tgz", + "integrity": "sha512-47N4hxa01a4x6XnJoskMKTS8XZ0CZMd8YTbINbi+w03A2w4j1RTlnGHOz/P0+Bg1LaVL6ufZyNprSg+fW5nYQQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.32.0.tgz", - "integrity": "sha512-JpsGxLBB2EFXBsTLHfkZDsXSpSmKD3VxXCgBQtlPcuAqB8TlqtLcbeMhxXQkCDv1avgwNjF8uEIbq5p+Cee0PA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.40.2.tgz", + "integrity": "sha512-8t6aL4MD+rXSHHZUR1z19+9OFJ2rl1wGKvckN47XFRVO+QL/dUSpKA2SLRo4vMg7ELA8pzGpC+W9OEd1Z/ZqoQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.32.0.tgz", - "integrity": "sha512-wegiyBT6rawdpvnD9lmbOpx5Sph+yVZKHbhnSP9MqUEDX08G4UzMU+D87jrazGE7lRSyTRs6NEYHtzfkJ3FjjQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.40.2.tgz", + "integrity": "sha512-C+AyHBzfpsOEYRFjztcYUFsH4S7UsE9cDtHCtma5BK8+ydOZYgMmWg1d/4KBytQspJCld8ZIujFMAdKG1xyr4Q==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.32.0.tgz", - "integrity": "sha512-3pA7xecItbgOs1A5H58dDvOUEboG5UfpTq3WzAdF54acBbUM+olDJAPkgj1GRJ4ZqE12DZ9/hNS2QZk166v92A==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.40.2.tgz", + "integrity": "sha512-de6TFZYIvJwRNjmW3+gaXiZ2DaWL5D5yGmSYzkdzjBDS3W+B9JQ48oZEsmMvemqjtAFzE16DIBLqd6IQQRuG9Q==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.32.0.tgz", - "integrity": "sha512-Y7XUZEVISGyge51QbYyYAEHwpGgmRrAxQXO3siyYo2kmaj72USSG8LtlQQgAtlGfxYiOwu+2BdbPjzEpcOpRmQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.40.2.tgz", + "integrity": "sha512-urjaEZubdIkacKc930hUDOfQPysezKla/O9qV+O89enqsqUmQm8Xj8O/vh0gHg4LYfv7Y7UsE3QjzLQzDYN1qg==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.32.0.tgz", - "integrity": "sha512-r7/OTF5MqeBrZo5omPXcTnjvv1GsrdH8a8RerARvDFiDwFpDVDnJyByYM/nX+mvks8XXsgPUxkwe/ltaX2VH7w==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.40.2.tgz", + "integrity": "sha512-KlE8IC0HFOC33taNt1zR8qNlBYHj31qGT1UqWqtvR/+NuCVhfufAq9fxO8BMFC22Wu0rxOwGVWxtCMvZVLmhQg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.32.0.tgz", - "integrity": "sha512-HJbifC9vex9NqnlodV2BHVFNuzKL5OnsV2dvTw6e1dpZKkNjPG6WUq+nhEYV6Hv2Bv++BXkwcyoGlXnPrjAKXw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.40.2.tgz", + "integrity": "sha512-j8CgxvfM0kbnhu4XgjnCWJQyyBOeBI1Zq91Z850aUddUmPeQvuAy6OiMdPS46gNFgy8gN1xkYyLgwLYZG3rBOg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.32.0.tgz", - "integrity": "sha512-VAEzZTD63YglFlWwRj3taofmkV1V3xhebDXffon7msNz4b14xKsz7utO6F8F4cqt8K/ktTl9rm88yryvDpsfOw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.40.2.tgz", + "integrity": "sha512-Ybc/1qUampKuRF4tQXc7G7QY9YRyeVSykfK36Y5Qc5dmrIxwFhrOzqaVTNoZygqZ1ZieSWTibfFhQ5qK8jpWxw==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.32.0.tgz", - "integrity": "sha512-Sts5DST1jXAc9YH/iik1C9QRsLcCoOScf3dfbY5i4kH9RJpKxiTBXqm7qU5O6zTXBTEZry69bGszr3SMgYmMcQ==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.40.2.tgz", + "integrity": "sha512-3FCIrnrt03CCsZqSYAOW/k9n625pjpuMzVfeI+ZBUSDT3MVIFDSPfSUgIl9FqUftxcUXInvFah79hE1c9abD+Q==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.32.0.tgz", - "integrity": "sha512-qhlXeV9AqxIyY9/R1h1hBD6eMvQCO34ZmdYvry/K+/MBs6d1nRFLm6BOiITLVI+nFAAB9kUB6sdJRKyVHXnqZw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.40.2.tgz", + "integrity": "sha512-QNU7BFHEvHMp2ESSY3SozIkBPaPBDTsfVNGx3Xhv+TdvWXFGOSH2NJvhD1zKAT6AyuuErJgbdvaJhYVhVqrWTg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.40.2.tgz", + "integrity": "sha512-5W6vNYkhgfh7URiXTO1E9a0cy4fSgfE4+Hl5agb/U1sa0kjOLMLC1wObxwKxecE17j0URxuTrYZZME4/VH57Hg==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.32.0.tgz", - "integrity": "sha512-8ZGN7ExnV0qjXa155Rsfi6H8M4iBBwNLBM9lcVS+4NcSzOFaNqmt7djlox8pN1lWrRPMRRQ8NeDlozIGx3Omsw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.40.2.tgz", + "integrity": "sha512-B7LKIz+0+p348JoAL4X/YxGx9zOx3sR+o6Hj15Y3aaApNfAshK8+mWZEf759DXfRLeL2vg5LYJBB7DdcleYCoQ==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.32.0.tgz", - "integrity": "sha512-VDzNHtLLI5s7xd/VubyS10mq6TxvZBp+4NRWoW+Hi3tgV05RtVm4qK99+dClwTN1McA6PHwob6DEJ6PlXbY83A==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.40.2.tgz", + "integrity": "sha512-lG7Xa+BmBNwpjmVUbmyKxdQJ3Q6whHjMjzQplOs5Z+Gj7mxPtWakGHqzMqNER68G67kmCX9qX57aRsW5V0VOng==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.32.0.tgz", - "integrity": "sha512-qcb9qYDlkxz9DxJo7SDhWxTWV1gFuwznjbTiov289pASxlfGbaOD54mgbs9+z94VwrXtKTu+2RqwlSTbiOqxGg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.40.2.tgz", + "integrity": "sha512-tD46wKHd+KJvsmije4bUskNuvWKFcTOIM9tZ/RrmIvcXnbi0YK/cKS9FzFtAm7Oxi2EhV5N2OpfFB348vSQRXA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.32.0.tgz", - "integrity": "sha512-pFDdotFDMXW2AXVbfdUEfidPAk/OtwE/Hd4eYMTNVVaCQ6Yl8et0meDaKNL63L44Haxv4UExpv9ydSf3aSayDg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.40.2.tgz", + "integrity": "sha512-Bjv/HG8RRWLNkXwQQemdsWw4Mg+IJ29LK+bJPW2SCzPKOUaMmPEppQlu/Fqk1d7+DX3V7JbFdbkh/NMmurT6Pg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.32.0.tgz", - "integrity": "sha512-/TG7WfrCAjeRNDvI4+0AAMoHxea/USWhAzf9PVDFHbcqrQ7hMMKp4jZIy4VEjk72AAfN5k4TiSMRXRKf/0akSw==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.40.2.tgz", + "integrity": "sha512-dt1llVSGEsGKvzeIO76HToiYPNPYPkmjhMHhP00T9S4rDern8P2ZWvWAQUEJ+R1UdMWJ/42i/QqJ2WV765GZcA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.32.0.tgz", - "integrity": "sha512-5hqO5S3PTEO2E5VjCePxv40gIgyS2KvO7E7/vvC/NbIW4SIRamkMr1hqj+5Y67fbBWv/bQLB6KelBQmXlyCjWA==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.40.2.tgz", + "integrity": "sha512-bwspbWB04XJpeElvsp+DCylKfF4trJDa2Y9Go8O6A7YLX2LIKGcNK/CYImJN6ZP4DcuOHB4Utl3iCbnR62DudA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" ] }, "node_modules/@rollup/wasm-node": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.32.0.tgz", - "integrity": "sha512-Dsr5270xEi7yFW3MjnLp+mBO+EW0qbdp+J+EPm+eCpA0nfYnWv/xExf85uTCBu18VVvGg0RTUqrUlwA/FUe2yQ==", + "version": "4.44.0", + "resolved": "https://registry.npmjs.org/@rollup/wasm-node/-/wasm-node-4.44.0.tgz", + "integrity": "sha512-9PvS/NpzOdVXhPY/qTxcgYyvE0OfiHZFYplMtba0E+vrqm90qq0983VOJEg7exz7By0cBfl0IYaYJfohs9pVQg==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.8" }, "bin": { "rollup": "dist/bin/rollup" @@ -13687,6 +14642,15 @@ "fsevents": "~2.3.2" } }, + "node_modules/@rollup/wasm-node/node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -13912,6 +14876,7 @@ "resolved": "https://registry.npmjs.org/@sigstore/core/-/core-2.0.0.tgz", "integrity": "sha512-nYxaSb/MtlSI+JWcwTHQxyNmWeWrUXJJ/G4liLrGG7+tS4vAz6LF3xRXqLH6wPIVUoZQel2Fs4ddLx4NCpiIYg==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -14087,36 +15052,39 @@ } }, "node_modules/@sigstore/verify": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.0.0.tgz", - "integrity": "sha512-Ggtq2GsJuxFNUvQzLoXqRwS4ceRfLAJnrIHUDrzAD0GgnOhwujJkKkxM/s5Bako07c3WtAs/sZo5PJq7VHjeDg==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/verify/-/verify-2.1.1.tgz", + "integrity": "sha512-hVJD77oT67aowHxwT4+M6PGOp+E2LtLdTK3+FC0lBO9T7sYwItDMXZ7Z07IDCvR1M717a4axbIWckrW67KMP/w==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.0.0", + "@sigstore/bundle": "^3.1.0", "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2" + "@sigstore/protobuf-specs": "^0.4.1" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@sigstore/verify/node_modules/@sigstore/bundle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.0.0.tgz", - "integrity": "sha512-XDUYX56iMPAn/cdgh/DTJxz5RWmqKV4pwvUAEKEWJl+HzKdCd/24wUa9JYNMlDSCb7SUHAdtksxYX779Nne/Zg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", + "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2" + "@sigstore/protobuf-specs": "^0.4.0" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "node_modules/@sigstore/verify/node_modules/@sigstore/protobuf-specs": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.3.tgz", - "integrity": "sha512-RpacQhBlwpBWd7KEJsRKcBQalbV28fvkxwTOJIqhIuDysMMaJW47V4OqW30iJB9uRpqOSxxEAQFdr8tTattReQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.3.tgz", + "integrity": "sha512-fk2zjD9117RL9BjqEwF7fwv7Q/P9yGsMV4MUJZ/DocaQJ6+3pKr+syBq1owU5Q5qGw5CUbXzm+4yJ2JVRDQeSA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -14138,18 +15106,6 @@ "url": "https://github.com/sindresorhus/is?sponsor=1" } }, - "node_modules/@sindresorhus/merge-streams": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/merge-streams/-/merge-streams-2.3.0.tgz", - "integrity": "sha512-LtoMMhxAlorcGhmFYI+LhPgbPZCkgP6ra1YL604EeF6U98pLlQ3iWIGMdWSC+vWmPBWBNgmDBAhnAobLROJmwg==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/@sinonjs/commons": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/@sinonjs/commons/-/commons-3.0.1.tgz", @@ -14425,7 +15381,8 @@ "node_modules/@swc/counter": { "version": "0.1.3", "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", - "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==" + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" }, "node_modules/@swc/helpers": { "version": "0.5.15", @@ -14447,34 +15404,6 @@ "node": ">=10" } }, - "node_modules/@testing-library/dom": { - "version": "8.20.1", - "resolved": "https://registry.npmjs.org/@testing-library/dom/-/dom-8.20.1.tgz", - "integrity": "sha512-/DiOQ5xBxgdYRC8LNk7U+RWat0S3qRLeIw3ZIkMQ9kkVlRmwD/Eg8k8CqIpD6GW7u20JIUOfMKbxtiLutpjQ4g==", - "dev": true, - "dependencies": { - "@babel/code-frame": "^7.10.4", - "@babel/runtime": "^7.12.5", - "@types/aria-query": "^5.0.1", - "aria-query": "5.1.3", - "chalk": "^4.1.0", - "dom-accessibility-api": "^0.5.9", - "lz-string": "^1.5.0", - "pretty-format": "^27.0.2" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@testing-library/dom/node_modules/aria-query": { - "version": "5.1.3", - "resolved": "https://registry.npmjs.org/aria-query/-/aria-query-5.1.3.tgz", - "integrity": "sha512-R5iJ5lkuHybztUfuOAznmboyjWq8O6sqNqtK7CLOqdydi54VNbORp49mb14KbWgG1QD3JFO9hJdZ+y4KutfdOQ==", - "dev": true, - "dependencies": { - "deep-equal": "^2.0.5" - } - }, "node_modules/@testing-library/jest-dom": { "version": "5.17.0", "resolved": "https://registry.npmjs.org/@testing-library/jest-dom/-/jest-dom-5.17.0.tgz", @@ -14522,24 +15451,6 @@ "node": ">=8" } }, - "node_modules/@testing-library/react": { - "version": "13.4.0", - "resolved": "https://registry.npmjs.org/@testing-library/react/-/react-13.4.0.tgz", - "integrity": "sha512-sXOGON+WNTh3MLE9rve97ftaZukN3oNf2KjDy7YTx6hcTO2uuLHuCGynMDhFwGw/jYf4OJ2Qk0i4i79qMNNkyw==", - "dev": true, - "dependencies": { - "@babel/runtime": "^7.12.5", - "@testing-library/dom": "^8.5.0", - "@types/react-dom": "^18.0.0" - }, - "engines": { - "node": ">=12" - }, - "peerDependencies": { - "react": "^18.0.0", - "react-dom": "^18.0.0" - } - }, "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -14639,7 +15550,8 @@ "version": "5.0.4", "resolved": "https://registry.npmjs.org/@types/aria-query/-/aria-query-5.0.4.tgz", "integrity": "sha512-rfT93uj5s0PRL7EzccGMs3brplhcrghnDoV26NqKhCAS1hVo+WdNsPvE/yb6ilfr5hi2MEk6d5EWJTKdxg8jVw==", - "dev": true + "dev": true, + "peer": true }, "node_modules/@types/babel__core": { "version": "7.20.5", @@ -14761,9 +15673,10 @@ } }, "node_modules/@types/estree": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.6.tgz", - "integrity": "sha512-AYnb1nQyY49te+VRAVgmzfcgjYS91mY5P0TKUDCLEM+gNnA+3T6rWITXRLYCpahpqSQbN5cE+gHpnPyXjHWxcw==" + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz", + "integrity": "sha512-w28IoSUCJpidD/TGviZwwMJckNESJZXFu7NBZ5YJ4mEUnNraUn9Pm8HSZm/jDF1pDWYKspWE7oVphigUPRakIQ==", + "license": "MIT" }, "node_modules/@types/express": { "version": "4.17.21", @@ -15109,20 +16022,21 @@ "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==" }, "node_modules/@types/react": { - "version": "18.3.18", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.18.tgz", - "integrity": "sha512-t4yC+vtgnkYjNSKlFx1jkAhH8LgTo2N/7Qvi83kdEaUtMDiwpbLAktKDaAMlRcJ5eSxZkH74eEGt1ky31d7kfQ==", + "version": "19.1.3", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.3.tgz", + "integrity": "sha512-dLWQ+Z0CkIvK1J8+wrDPwGxEYFA4RAyHoZPxHVGspYmFVnwGSNT24cGIhFJrtfRnWVuW8X7NO52gCXmhkVUWGQ==", + "license": "MIT", "dependencies": { - "@types/prop-types": "*", "csstype": "^3.0.2" } }, "node_modules/@types/react-dom": { - "version": "18.3.5", - "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.5.tgz", - "integrity": "sha512-P4t6saawp+b/dFrUr2cvkVsfvPguwsxtH6dNIYRllMsefqFzkZk5UIjzyDOv5g1dXIPdG4Sp1yCR4Z6RCUsG/Q==", + "version": "19.1.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.1.3.tgz", + "integrity": "sha512-rJXC08OG0h3W6wDMFxQrZF00Kq6qQvw0djHRdzl3U5DnIERz0MRce3WVc7IS6JYBwtaP/DwYtRRjVlvivNveKg==", + "license": "MIT", "peerDependencies": { - "@types/react": "^18.0.0" + "@types/react": "^19.0.0" } }, "node_modules/@types/react-router": { @@ -16820,9 +17734,9 @@ } }, "node_modules/autoprefixer": { - "version": "10.4.20", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.20.tgz", - "integrity": "sha512-XY25y5xSv/wEoqzDyXXME4AFfkZI0P23z6Fs3YgymDnKJkCGOnkL0iTxCa85UTqaSgfcqyf3UA6+c7wUvx/16g==", + "version": "10.4.21", + "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", + "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", "funding": [ { "type": "opencollective", @@ -16837,12 +17751,13 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "browserslist": "^4.23.3", - "caniuse-lite": "^1.0.30001646", + "browserslist": "^4.24.4", + "caniuse-lite": "^1.0.30001702", "fraction.js": "^4.3.7", "normalize-range": "^0.1.2", - "picocolors": "^1.0.1", + "picocolors": "^1.1.1", "postcss-value-parser": "^4.2.0" }, "bin": { @@ -16909,9 +17824,10 @@ } }, "node_modules/axios": { - "version": "1.7.9", - "resolved": "https://registry.npmjs.org/axios/-/axios-1.7.9.tgz", - "integrity": "sha512-LhLcE7Hbiryz8oMDdDptSrWowmB4Bl6RCt6sIJKpRB4XtVf0iEgewX3au/pJqm+Py1kCASkb/FFKjxQaLtxJvw==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz", + "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==", + "license": "MIT", "dependencies": { "follow-redirects": "^1.15.6", "form-data": "^4.0.0", @@ -17467,16 +18383,17 @@ } }, "node_modules/beasties": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.2.0.tgz", - "integrity": "sha512-Ljqskqx/tbZagIglYoJIMzH5zgssyp+in9+9sAyh15N22AornBeIDnb8EZ6Rk+6ShfMxd92uO3gfpT0NtZbpow==", + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/beasties/-/beasties-0.3.4.tgz", + "integrity": "sha512-NmzN1zN1cvGccXFyZ73335+ASXwBlVWcUPssiUDIlFdfyatHPRRufjCd5w8oPaQPvVnf9ELklaCGb1gi9FBwIw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "css-select": "^5.1.0", "css-what": "^6.1.0", "dom-serializer": "^2.0.0", "domhandler": "^5.0.3", - "htmlparser2": "^9.1.0", + "htmlparser2": "^10.0.0", "picocolors": "^1.1.1", "postcss": "^8.4.49", "postcss-media-query-parser": "^0.2.3" @@ -17490,6 +18407,7 @@ "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", "css-what": "^6.1.0", @@ -17506,6 +18424,7 @@ "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", "dev": true, + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.2", @@ -17520,6 +18439,7 @@ "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "domelementtype": "^2.3.0" }, @@ -17535,6 +18455,7 @@ "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "dom-serializer": "^2.0.0", "domelementtype": "^2.3.0", @@ -17549,6 +18470,7 @@ "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", "dev": true, + "license": "BSD-2-Clause", "engines": { "node": ">=0.12" }, @@ -17557,9 +18479,9 @@ } }, "node_modules/beasties/node_modules/htmlparser2": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-9.1.0.tgz", - "integrity": "sha512-5zfg6mHUoaer/97TxnGpxmbR7zJtPwIYFMZ/H5ucTlPZhKvtum05yiPK3Mgai3a0DyVxv7qYqoweaEd2nrYQzQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.0.0.tgz", + "integrity": "sha512-TwAZM+zE5Tq3lrEHvOlvwgj1XLWQCtaaibSN11Q+gGBAS7Y1uZSWwXXRe4iF6OXnaq1riyQAPFOBtYc77Mxq0g==", "dev": true, "funding": [ "https://github.com/fb55/htmlparser2?sponsor=1", @@ -17568,11 +18490,25 @@ "url": "https://github.com/sponsors/fb55" } ], + "license": "MIT", "dependencies": { "domelementtype": "^2.3.0", "domhandler": "^5.0.3", - "domutils": "^3.1.0", - "entities": "^4.5.0" + "domutils": "^3.2.1", + "entities": "^6.0.0" + } + }, + "node_modules/beasties/node_modules/htmlparser2/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" } }, "node_modules/before-after-hook": { @@ -17857,7 +18793,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bundle-name/-/bundle-name-4.1.0.tgz", "integrity": "sha512-tjwM5exMg6BGRI+kNmTntNsvdZS1X8BFYS6tnJ2hdH0kVxM6/eVZ2xy+FqStSWvYmtfFMDLIxurorHwDKfDz5Q==", - "dev": true, + "devOptional": true, "dependencies": { "run-applescript": "^7.0.0" }, @@ -18079,9 +19015,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001695", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001695.tgz", - "integrity": "sha512-vHyLade6wTgI2u1ec3WQBxv+2BrTERV28UXQu9LO6lZ9pYeMk34vjXFLOxo1A4UBA8XTL4njRQZdno/yYaSmWw==", + "version": "1.0.30001724", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001724.tgz", + "integrity": "sha512-WqJo7p0TbHDOythNTqYujmaJTvtYRZrjpP8TCvH6Vb9CYJerJNKamKzIWOM4BkQatWj9H2lYulpdAQNBe7QhNA==", "funding": [ { "type": "opencollective", @@ -18095,7 +19031,8 @@ "type": "github", "url": "https://github.com/sponsors/ai" } - ] + ], + "license": "CC-BY-4.0" }, "node_modules/case-sensitive-paths-webpack-plugin": { "version": "2.4.0", @@ -18393,7 +19330,8 @@ "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", - "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==" + "integrity": "sha512-IV3Ou0jSMzZrd3pZ48nLkT9DA7Ag1pnPzaiQhpW7c3RbcqqzvzzVu+L8gfqMp/8IM2MQtSiqaCxrrcfu8I8rMA==", + "license": "MIT" }, "node_modules/cliui": { "version": "8.0.1", @@ -18585,6 +19523,20 @@ "resolved": "https://registry.npmjs.org/collect-v8-coverage/-/collect-v8-coverage-1.0.2.tgz", "integrity": "sha512-lHl4d5/ONEbLlJvaJNtsF/Lz+WvB07u2ycqTYbdrq7UypDXailES4valYb2eWiJFxZlVmpGekfqoxQhzyFdT4Q==" }, + "node_modules/color": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/color/-/color-4.2.3.tgz", + "integrity": "sha512-1rXeuUUiGGrykh+CeBdu5Ie7OJwinCgQY0bc7GCRxy5xVHy+moaqkpL/jqQq0MtQOeYcrqEz4abc5f0KtU7W4A==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-convert": "^2.0.1", + "color-string": "^1.9.0" + }, + "engines": { + "node": ">=12.5.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -18601,6 +19553,17 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "node_modules/color-string": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/color-string/-/color-string-1.9.1.tgz", + "integrity": "sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==", + "license": "MIT", + "optional": true, + "dependencies": { + "color-name": "^1.0.0", + "simple-swizzle": "^0.2.2" + } + }, "node_modules/color-support": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", @@ -19934,38 +20897,6 @@ "node": ">=6" } }, - "node_modules/deep-equal": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-2.2.3.tgz", - "integrity": "sha512-ZIwpnevOurS8bpT4192sqAowWM76JDKSHYzMLty3BZGSswgq6pBaH3DhCSW5xVAZICZyKdOBPjwww5wfgT/6PA==", - "dev": true, - "dependencies": { - "array-buffer-byte-length": "^1.0.0", - "call-bind": "^1.0.5", - "es-get-iterator": "^1.1.3", - "get-intrinsic": "^1.2.2", - "is-arguments": "^1.1.1", - "is-array-buffer": "^3.0.2", - "is-date-object": "^1.0.5", - "is-regex": "^1.1.4", - "is-shared-array-buffer": "^1.0.2", - "isarray": "^2.0.5", - "object-is": "^1.1.5", - "object-keys": "^1.1.1", - "object.assign": "^4.1.4", - "regexp.prototype.flags": "^1.5.1", - "side-channel": "^1.0.4", - "which-boxed-primitive": "^1.0.2", - "which-collection": "^1.0.1", - "which-typed-array": "^1.1.13" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/deep-extend": { "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", @@ -19991,7 +20922,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/default-browser/-/default-browser-5.2.1.tgz", "integrity": "sha512-WY/3TUME0x3KPYdRRxEJJvXRHV4PyPoUsxtZa78lwItwRQRHhd2U9xOscaT/YTf8uCXIAjeJOFBVEh/7FtD8Xg==", - "dev": true, + "devOptional": true, "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" @@ -20007,7 +20938,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/default-browser-id/-/default-browser-id-5.0.0.tgz", "integrity": "sha512-A6p/pu/6fyBcA1TRz/GqWYPViplrftcW2gZC9q79ngNCKAeR/X3gcEdXQHl4KNXV+3wgIJ1CPkJQ3IHM6lcsyA==", - "dev": true, + "devOptional": true, "engines": { "node": ">=18" }, @@ -21005,9 +21936,10 @@ "link": true }, "node_modules/electron-to-chromium": { - "version": "1.5.87", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.87.tgz", - "integrity": "sha512-mPFwmEWmRivw2F8x3w3l2m6htAUN97Gy0kwpO++2m9iT1Gt8RCFVUfv9U/sIbHJ6rY4P6/ooqFL/eL7ock+pPg==" + "version": "1.5.171", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.171.tgz", + "integrity": "sha512-scWpzXEJEMrGJa4Y6m/tVotb0WuvNmasv3wWVzUAeCgKU0ToFOhUW6Z+xWnRQANMYGxN4ngJXIThgBJOqzVPCQ==", + "license": "ISC" }, "node_modules/electron-webpack": { "resolved": "extensions/samples/electron-webpack", @@ -21364,6 +22296,7 @@ "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -21491,26 +22424,6 @@ "node": ">= 0.4" } }, - "node_modules/es-get-iterator": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/es-get-iterator/-/es-get-iterator-1.1.3.tgz", - "integrity": "sha512-sPZmqHBe6JIiTfN5q2pEi//TwxmAFHwj/XEuYjTuse78i8KxaqMTTzxPoFKuzRpDpTJ+0NAbpfenkmH2rePtuw==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.2", - "get-intrinsic": "^1.1.3", - "has-symbols": "^1.0.3", - "is-arguments": "^1.1.1", - "is-map": "^2.0.2", - "is-set": "^2.0.2", - "is-string": "^1.0.7", - "isarray": "^2.0.5", - "stop-iteration-iterator": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/es-iterator-helpers": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/es-iterator-helpers/-/es-iterator-helpers-1.2.1.tgz", @@ -23293,6 +24206,21 @@ "pend": "~1.2.0" } }, + "node_modules/fdir": { + "version": "6.4.6", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.4.6.tgz", + "integrity": "sha512-hiFoqpyZcfNm1yc4u8oWCf9A2c4D3QjCrks3zmoVKVxpQRzmPNar1hUJcBG2RQHvEVGDN+Jm81ZheVLAQMK6+w==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, "node_modules/figures": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", @@ -23480,6 +24408,43 @@ "node": ">=6" } }, + "node_modules/find-cache-directory": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/find-cache-directory/-/find-cache-directory-6.0.0.tgz", + "integrity": "sha512-CvFd5ivA6HcSHbD+59P7CyzINHXzwhuQK8RY7CxJZtgDSAtRlHiCaQpZQ2lMR/WRyUIEmzUvL6G2AGurMfegZA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "common-path-prefix": "^3.0.0", + "pkg-dir": "^8.0.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/find-cache-directory/node_modules/pkg-dir": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-8.0.0.tgz", + "integrity": "sha512-4peoBq4Wks0riS0z8741NVv+/8IiTvqnZAr8QGgtdifrtpdXbNw/FxRS1l6NFqm4EMzuS0EDqNNx4XGaz8cuyQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "find-up-simple": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/find-process": { "version": "1.4.10", "resolved": "https://registry.npmjs.org/find-process/-/find-process-1.4.10.tgz", @@ -23521,13 +24486,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "node_modules/find-up-simple": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/find-up-simple/-/find-up-simple-1.0.1.tgz", + "integrity": "sha512-afd4O7zpqHeRyg4PfDQsXmlDe2PfdHtJt6Akt8jOWaApLOZk5JXs6VMR29lz03pRe9mpykrRCYIYxaJYcfpncQ==", "dev": true, - "bin": { - "flat": "cli.js" + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/flat-cache": { @@ -23925,6 +24896,7 @@ "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.3.0.tgz", "integrity": "sha512-vpeMIQKxczTD/0s2CdEWHcb0eeJe6TFjxb+J5xgX7hScxqrGuyjmv4c1D4A/gelKfyox0gJJwIHF+fLjeaM8kQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -25415,7 +26387,7 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/hyperdyperid/-/hyperdyperid-1.2.0.tgz", "integrity": "sha512-Y93lCzHYgGWdrJ66yIktxiaGULYc6oGiABxhcO5AufBeOyoIdZF7bIfLaOrbM0iGIOXQQgxxRrFEnb+Y6w1n4A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10.18" } @@ -25801,22 +26773,6 @@ "node": ">= 0.10" } }, - "node_modules/is-arguments": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/is-arguments/-/is-arguments-1.2.0.tgz", - "integrity": "sha512-7bVbi0huj/wrIAOzb8U1aszg9kdi3KN/CyU19CTI7tAoZYEZoL9yCDXpbXN+uPsuWnP02cyug1gleqq+TU+YCA==", - "dev": true, - "dependencies": { - "call-bound": "^1.0.2", - "has-tostringtag": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/is-array-buffer": { "version": "3.0.5", "resolved": "https://registry.npmjs.org/is-array-buffer/-/is-array-buffer-3.0.5.tgz", @@ -26052,7 +27008,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "dev": true, + "devOptional": true, "dependencies": { "is-docker": "^3.0.0" }, @@ -26070,7 +27026,7 @@ "version": "3.0.0", "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", - "dev": true, + "devOptional": true, "bin": { "is-docker": "cli.js" }, @@ -26116,7 +27072,7 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-network-error/-/is-network-error-1.1.0.tgz", "integrity": "sha512-tUdRRAnhT+OtCZR/LxZelH/C7QtjtFrTu5tXCA8pl55eTUElUHT+GPYV8MBMBvea/j+NxQqVt3LbWMRir7Gx9g==", - "dev": true, + "devOptional": true, "engines": { "node": ">=16" }, @@ -27584,6 +28540,7 @@ "resolved": "https://registry.npmjs.org/jest-junit/-/jest-junit-16.0.0.tgz", "integrity": "sha512-A94mmw6NfJab4Fg/BlvVOUXzXgF0XIH6EmTgJ5NDPp4xoKq0Kr7sErb+4Xs9nZvu58pJojz5RFGpqnZYJTrRfQ==", "dev": true, + "license": "Apache-2.0", "dependencies": { "mkdirp": "^1.0.4", "strip-ansi": "^6.0.1", @@ -28878,7 +29835,6 @@ "version": "2.9.1", "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", - "dev": true, "dependencies": { "picocolors": "^1.0.0", "shell-quote": "^1.8.1" @@ -28936,7 +29892,6 @@ "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, "optional": true, - "peer": true, "bin": { "mime": "cli.js" }, @@ -28950,7 +29905,6 @@ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, "optional": true, - "peer": true, "engines": { "node": ">=0.10.0" } @@ -29038,11 +29992,12 @@ } }, "node_modules/lmdb": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.2.2.tgz", - "integrity": "sha512-LriG93la4PbmPMwI7Hbv8W+0ncLK7549w4sbZSi4QGDjnnxnmNMgxUkaQTEMzH8TpwsfFvgEjpLX7V8B/I9e3g==", + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/lmdb/-/lmdb-3.3.0.tgz", + "integrity": "sha512-MgJocUI6QEiSXQBFWLeyo1R7eQj8Rke5dlPxX0KFwli8/bsCxpM/KbXO5y0qmV/5llQ3wpneDWcTYxa+4vn8iQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { "msgpackr": "^1.11.2", @@ -29055,12 +30010,13 @@ "download-lmdb-prebuilds": "bin/download-prebuilds.js" }, "optionalDependencies": { - "@lmdb/lmdb-darwin-arm64": "3.2.2", - "@lmdb/lmdb-darwin-x64": "3.2.2", - "@lmdb/lmdb-linux-arm": "3.2.2", - "@lmdb/lmdb-linux-arm64": "3.2.2", - "@lmdb/lmdb-linux-x64": "3.2.2", - "@lmdb/lmdb-win32-x64": "3.2.2" + "@lmdb/lmdb-darwin-arm64": "3.3.0", + "@lmdb/lmdb-darwin-x64": "3.3.0", + "@lmdb/lmdb-linux-arm": "3.3.0", + "@lmdb/lmdb-linux-arm64": "3.3.0", + "@lmdb/lmdb-linux-x64": "3.3.0", + "@lmdb/lmdb-win32-arm64": "3.3.0", + "@lmdb/lmdb-win32-x64": "3.3.0" } }, "node_modules/load-json-file": { @@ -29441,6 +30397,7 @@ "resolved": "https://registry.npmjs.org/lz-string/-/lz-string-1.5.0.tgz", "integrity": "sha512-h5bgJWpxJNswbU7qCrV0tIKQCaS3blPDrqKWx+QxzuzL1zGUzij9XCWLrSLsJPu5t+eWA/ycetzYAO5IOMcWAQ==", "dev": true, + "peer": true, "bin": { "lz-string": "bin/bin.js" } @@ -29884,6 +30841,7 @@ "resolved": "https://registry.npmjs.org/mimic-function/-/mimic-function-5.0.1.tgz", "integrity": "sha512-VP79XUPxV2CigYP3jWwAUFSku2aKqBH7uTAapFWCBqutsbmDo96KY5o8uh6U+/YSIn5OxJnXp73beVkpqMIGhA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -30257,10 +31215,11 @@ } }, "node_modules/mrmime": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.0.tgz", - "integrity": "sha512-eu38+hdgojoyq63s+yTpN4XMBdt5l8HhMhc4VKLO9KM5caLIBvUm4thi7fFaxyTmCKeNnXZ5pAlBwCUnhA09uw==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", + "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", "dev": true, + "license": "MIT", "engines": { "node": ">=10" } @@ -30359,10 +31318,11 @@ "link": true }, "node_modules/msgpackr": { - "version": "1.11.2", - "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.2.tgz", - "integrity": "sha512-F9UngXRlPyWCDEASDpTf6c9uNhGPTqnTeLVt7bN+bU1eajoR/8V9ys2BRaV5C/e5ihE6sJ9uPIKaYt6bFuO32g==", + "version": "1.11.4", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-1.11.4.tgz", + "integrity": "sha512-uaff7RG9VIC4jacFW9xzL3jc0iM32DNHe4jYVycBcjUePT/Klnfj7pqtWJt9khvDFizmjN2TlYniYmSS2LIaZg==", "dev": true, + "license": "MIT", "optional": true, "optionalDependencies": { "msgpackr-extract": "^3.0.2" @@ -30374,6 +31334,7 @@ "integrity": "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA==", "dev": true, "hasInstallScript": true, + "license": "MIT", "optional": true, "dependencies": { "node-gyp-build-optional-packages": "5.2.2" @@ -30535,40 +31496,41 @@ "peer": true }, "node_modules/next": { - "version": "14.2.23", - "resolved": "https://registry.npmjs.org/next/-/next-14.2.23.tgz", - "integrity": "sha512-mjN3fE6u/tynneLiEg56XnthzuYw+kD7mCujgVqioxyPqbmiotUCGJpIZGS/VaPg3ZDT1tvWxiVyRzeqJFm/kw==", + "version": "15.3.3", + "resolved": "https://registry.npmjs.org/next/-/next-15.3.3.tgz", + "integrity": "sha512-JqNj29hHNmCLtNvd090SyRbXJiivQ+58XjCcrC50Crb5g5u2zi7Y2YivbsEfzk6AtVI80akdOQbaMZwWB1Hthw==", "dependencies": { - "@next/env": "14.2.23", - "@swc/helpers": "0.5.5", + "@next/env": "15.3.3", + "@swc/counter": "0.1.3", + "@swc/helpers": "0.5.15", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", - "graceful-fs": "^4.2.11", "postcss": "8.4.31", - "styled-jsx": "5.1.1" + "styled-jsx": "5.1.6" }, "bin": { "next": "dist/bin/next" }, "engines": { - "node": ">=18.17.0" + "node": "^18.18.0 || ^19.8.0 || >= 20.0.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.2.23", - "@next/swc-darwin-x64": "14.2.23", - "@next/swc-linux-arm64-gnu": "14.2.23", - "@next/swc-linux-arm64-musl": "14.2.23", - "@next/swc-linux-x64-gnu": "14.2.23", - "@next/swc-linux-x64-musl": "14.2.23", - "@next/swc-win32-arm64-msvc": "14.2.23", - "@next/swc-win32-ia32-msvc": "14.2.23", - "@next/swc-win32-x64-msvc": "14.2.23" + "@next/swc-darwin-arm64": "15.3.3", + "@next/swc-darwin-x64": "15.3.3", + "@next/swc-linux-arm64-gnu": "15.3.3", + "@next/swc-linux-arm64-musl": "15.3.3", + "@next/swc-linux-x64-gnu": "15.3.3", + "@next/swc-linux-x64-musl": "15.3.3", + "@next/swc-win32-arm64-msvc": "15.3.3", + "@next/swc-win32-x64-msvc": "15.3.3", + "sharp": "^0.34.1" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", "@playwright/test": "^1.41.2", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "babel-plugin-react-compiler": "*", + "react": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", + "react-dom": "^18.2.0 || 19.0.0-rc-de68d2f4-20241204 || ^19.0.0", "sass": "^1.3.0" }, "peerDependenciesMeta": { @@ -30578,6 +31540,9 @@ "@playwright/test": { "optional": true }, + "babel-plugin-react-compiler": { + "optional": true + }, "sass": { "optional": true } @@ -30587,15 +31552,6 @@ "resolved": "samples/msal-react-samples/nextjs-sample", "link": true }, - "node_modules/next/node_modules/@swc/helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", - "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", - "dependencies": { - "@swc/counter": "^0.1.3", - "tslib": "^2.4.0" - } - }, "node_modules/next/node_modules/postcss": { "version": "8.4.31", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.31.tgz", @@ -31007,6 +31963,7 @@ "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", "dev": true, + "license": "MIT", "optional": true, "dependencies": { "detect-libc": "^2.0.1" @@ -31780,22 +32737,6 @@ "url": "https://github.com/sponsors/ljharb" } }, - "node_modules/object-is": { - "version": "1.1.6", - "resolved": "https://registry.npmjs.org/object-is/-/object-is-1.1.6.tgz", - "integrity": "sha512-F8cZ+KfGlSGi09lJT7/Nd6KJZ9ygtvYC0/UYYLI9nmQKLMnydpB9yvbv9K1uSkEu7FU9vYPmVwLg328tX+ot3Q==", - "dev": true, - "dependencies": { - "call-bind": "^1.0.7", - "define-properties": "^1.2.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/object-keys": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", @@ -33321,6 +34262,7 @@ "resolved": "https://registry.npmjs.org/ordered-binary/-/ordered-binary-1.5.3.tgz", "integrity": "sha512-oGFr3T+pYdTGJ+YFEILMpS3es+GiIbs9h/XQrclBXUtd44ey7XwfsMzM31f64I1SQOawDoDr/D823kNCADI8TA==", "dev": true, + "license": "MIT", "optional": true }, "node_modules/os-browserify": { @@ -34226,9 +35168,9 @@ } }, "node_modules/postcss": { - "version": "8.5.1", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.1.tgz", - "integrity": "sha512-6oz2beyjc5VMn/KV1pPw8fliQkhBXrVn1Z3TVyqZxU8kZpzEKhBdmCFqI6ZbmGtamQvQGuU1sgPTk8ZrXDD7jQ==", + "version": "8.5.3", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.3.tgz", + "integrity": "sha512-dle9A3yYxlBSrt8Fu+IpjGT8SY8hN0mlaA6GY8t0P5PjIOZemULz/E2Bnm/2dcUOena75OTNkHI76uZBNUUq3A==", "funding": [ { "type": "opencollective", @@ -34243,6 +35185,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "nanoid": "^3.3.8", "picocolors": "^1.1.1", @@ -34835,7 +35778,8 @@ "version": "0.2.3", "resolved": "https://registry.npmjs.org/postcss-media-query-parser/-/postcss-media-query-parser-0.2.3.tgz", "integrity": "sha512-3sOlxmbKcSHMjlUXQZKQ06jOswE7oVkXPxmZdoB1r5l0q6gTFTQSHxNxOrCccElbW7dxNytifNEo8qidX2Vsig==", - "dev": true + "dev": true, + "license": "MIT" }, "node_modules/postcss-merge-longhand": { "version": "5.1.7", @@ -36335,12 +37279,10 @@ } }, "node_modules/react": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", - "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", - "dependencies": { - "loose-envify": "^1.1.0" - }, + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react/-/react-19.1.0.tgz", + "integrity": "sha512-FS+XFBNvn3GTAWq26joslQgWNoFu08F4kl0J4CgdNKADkdSGXQyTCnKteIAJy96Br6YbpEU1LSzV5dYtjMkMDg==", + "license": "MIT", "engines": { "node": ">=0.10.0" } @@ -36554,15 +37496,15 @@ } }, "node_modules/react-dom": { - "version": "18.3.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", - "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "version": "19.1.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.0.tgz", + "integrity": "sha512-Xs1hdnE+DyKgeHJeJznQmYMIBG3TKIHJJT95Q58nHLSrElKlGQqDTR2HQ9fx5CN/Gk6Vh/kupBTDLU11/nDk/g==", + "license": "MIT", "dependencies": { - "loose-envify": "^1.1.0", - "scheduler": "^0.23.2" + "scheduler": "^0.26.0" }, "peerDependencies": { - "react": "^18.3.1" + "react": "^19.1.0" } }, "node_modules/react-error-overlay": { @@ -37382,6 +38324,15 @@ "node": ">= 6" } }, + "node_modules/react-scripts/node_modules/ipaddr.js": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", + "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "license": "MIT", + "engines": { + "node": ">= 10" + } + }, "node_modules/react-scripts/node_modules/jest": { "version": "27.5.1", "resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz", @@ -38187,6 +39138,12 @@ } } }, + "node_modules/react-scripts/node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "license": "MIT" + }, "node_modules/react-scripts/node_modules/loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", @@ -38581,6 +39538,203 @@ "node": ">=10.4" } }, + "node_modules/react-scripts/node_modules/webpack-dev-middleware": { + "version": "5.3.4", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", + "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "license": "MIT", + "dependencies": { + "colorette": "^2.0.10", + "memfs": "^3.4.3", + "mime-types": "^2.1.31", + "range-parser": "^1.2.1", + "schema-utils": "^4.0.0" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.0.0 || ^5.0.0" + } + }, + "node_modules/react-scripts/node_modules/webpack-dev-middleware/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/react-scripts/node_modules/webpack-dev-middleware/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/react-scripts/node_modules/webpack-dev-middleware/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/react-scripts/node_modules/webpack-dev-server": { + "version": "4.15.2", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", + "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", + "license": "MIT", + "dependencies": { + "@types/bonjour": "^3.5.9", + "@types/connect-history-api-fallback": "^1.3.5", + "@types/express": "^4.17.13", + "@types/serve-index": "^1.9.1", + "@types/serve-static": "^1.13.10", + "@types/sockjs": "^0.3.33", + "@types/ws": "^8.5.5", + "ansi-html-community": "^0.0.8", + "bonjour-service": "^1.0.11", + "chokidar": "^3.5.3", + "colorette": "^2.0.10", + "compression": "^1.7.4", + "connect-history-api-fallback": "^2.0.0", + "default-gateway": "^6.0.3", + "express": "^4.17.3", + "graceful-fs": "^4.2.6", + "html-entities": "^2.3.2", + "http-proxy-middleware": "^2.0.3", + "ipaddr.js": "^2.0.1", + "launch-editor": "^2.6.0", + "open": "^8.0.9", + "p-retry": "^4.5.0", + "rimraf": "^3.0.2", + "schema-utils": "^4.0.0", + "selfsigned": "^2.1.1", + "serve-index": "^1.9.1", + "sockjs": "^0.3.24", + "spdy": "^4.0.2", + "webpack-dev-middleware": "^5.3.4", + "ws": "^8.13.0" + }, + "bin": { + "webpack-dev-server": "bin/webpack-dev-server.js" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^4.37.0 || ^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + }, + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/react-scripts/node_modules/webpack-dev-server/node_modules/ajv": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", + "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/react-scripts/node_modules/webpack-dev-server/node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/react-scripts/node_modules/webpack-dev-server/node_modules/schema-utils": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/react-scripts/node_modules/webpack-dev-server/node_modules/ws": { + "version": "8.18.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz", + "integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, "node_modules/react-scripts/node_modules/whatwg-encoding": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-1.0.5.tgz", @@ -39228,14 +40382,6 @@ "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.11.tgz", "integrity": "sha512-kY1AZVr2Ra+t+piVaJ4gxaFaReZVH40AKNo7UCX6W+dEwBo/2oZJzqfuN1qLq1oL45o56cPaTXELwrTh8Fpggg==" }, - "node_modules/regenerator-transform": { - "version": "0.15.2", - "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", - "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", - "dependencies": { - "@babel/runtime": "^7.8.4" - } - }, "node_modules/regex-parser": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/regex-parser/-/regex-parser-2.3.0.tgz", @@ -39682,12 +40828,13 @@ "optional": true }, "node_modules/rollup": { - "version": "4.32.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.32.0.tgz", - "integrity": "sha512-JmrhfQR31Q4AuNBjjAX4s+a/Pu/Q8Q9iwjWBsjRH1q52SPFE2NqRMK6fUZKKnvKO6id+h7JIRf0oYsph53eATg==", + "version": "4.40.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.40.2.tgz", + "integrity": "sha512-tfUOg6DTP4rhQ3VjOO6B4wyrJnGOX85requAXvqYTHsOgb2TFJdZ3aWpT8W2kPoypSGP7dZUyzxJ9ee4buM5Fg==", "dev": true, + "license": "MIT", "dependencies": { - "@types/estree": "1.0.6" + "@types/estree": "1.0.7" }, "bin": { "rollup": "dist/bin/rollup" @@ -39697,25 +40844,26 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.32.0", - "@rollup/rollup-android-arm64": "4.32.0", - "@rollup/rollup-darwin-arm64": "4.32.0", - "@rollup/rollup-darwin-x64": "4.32.0", - "@rollup/rollup-freebsd-arm64": "4.32.0", - "@rollup/rollup-freebsd-x64": "4.32.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.32.0", - "@rollup/rollup-linux-arm-musleabihf": "4.32.0", - "@rollup/rollup-linux-arm64-gnu": "4.32.0", - "@rollup/rollup-linux-arm64-musl": "4.32.0", - "@rollup/rollup-linux-loongarch64-gnu": "4.32.0", - "@rollup/rollup-linux-powerpc64le-gnu": "4.32.0", - "@rollup/rollup-linux-riscv64-gnu": "4.32.0", - "@rollup/rollup-linux-s390x-gnu": "4.32.0", - "@rollup/rollup-linux-x64-gnu": "4.32.0", - "@rollup/rollup-linux-x64-musl": "4.32.0", - "@rollup/rollup-win32-arm64-msvc": "4.32.0", - "@rollup/rollup-win32-ia32-msvc": "4.32.0", - "@rollup/rollup-win32-x64-msvc": "4.32.0", + "@rollup/rollup-android-arm-eabi": "4.40.2", + "@rollup/rollup-android-arm64": "4.40.2", + "@rollup/rollup-darwin-arm64": "4.40.2", + "@rollup/rollup-darwin-x64": "4.40.2", + "@rollup/rollup-freebsd-arm64": "4.40.2", + "@rollup/rollup-freebsd-x64": "4.40.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.40.2", + "@rollup/rollup-linux-arm-musleabihf": "4.40.2", + "@rollup/rollup-linux-arm64-gnu": "4.40.2", + "@rollup/rollup-linux-arm64-musl": "4.40.2", + "@rollup/rollup-linux-loongarch64-gnu": "4.40.2", + "@rollup/rollup-linux-powerpc64le-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-gnu": "4.40.2", + "@rollup/rollup-linux-riscv64-musl": "4.40.2", + "@rollup/rollup-linux-s390x-gnu": "4.40.2", + "@rollup/rollup-linux-x64-gnu": "4.40.2", + "@rollup/rollup-linux-x64-musl": "4.40.2", + "@rollup/rollup-win32-arm64-msvc": "4.40.2", + "@rollup/rollup-win32-ia32-msvc": "4.40.2", + "@rollup/rollup-win32-x64-msvc": "4.40.2", "fsevents": "~2.3.2" } }, @@ -39727,7 +40875,7 @@ "version": "7.0.0", "resolved": "https://registry.npmjs.org/run-applescript/-/run-applescript-7.0.0.tgz", "integrity": "sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==", - "dev": true, + "devOptional": true, "engines": { "node": ">=18" }, @@ -39867,10 +41015,11 @@ "integrity": "sha512-ZRwKbh/eQ6w9vmTjkuG0Ioi3HBwPFce0O+v//ve+aOq1oeCy7jMV2qzzAlpsNuqpqCBjjriM1lbtZbF/Q8jVyA==" }, "node_modules/sass": { - "version": "1.83.4", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.4.tgz", - "integrity": "sha512-B1bozCeNQiOgDcLd33e2Cs2U60wZwjUUXzh900ZyQF5qUasvMdDZYbQ566LJu7cqR+sAHlAfO6RMkaID5s6qpA==", + "version": "1.88.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.88.0.tgz", + "integrity": "sha512-sF6TWQqjFvr4JILXzG4ucGOLELkESHL+I5QJhh7CNaE+Yge0SI+ehCatsXhJ7ymU1hAFcIS3/PBpjdIbXoyVbg==", "devOptional": true, + "license": "MIT", "dependencies": { "chokidar": "^4.0.0", "immutable": "^5.0.2", @@ -40002,17 +41151,16 @@ } }, "node_modules/scheduler": { - "version": "0.23.2", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", - "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", - "dependencies": { - "loose-envify": "^1.1.0" - } + "version": "0.26.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", + "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", + "license": "MIT" }, "node_modules/schema-utils": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", - "integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", + "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", + "license": "MIT", "dependencies": { "@types/json-schema": "^7.0.9", "ajv": "^8.9.0", @@ -40284,6 +41432,60 @@ "node": ">=8" } }, + "node_modules/sharp": { + "version": "0.34.1", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.1.tgz", + "integrity": "sha512-1j0w61+eVxu7DawFJtnfYcvSv6qPFvfTaqzTQ2BLknVhHTwGS8sc63ZBF4rzkWMBVKybo4S5OBtDdZahh2A1xg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "color": "^4.2.3", + "detect-libc": "^2.0.3", + "semver": "^7.7.1" + }, + "engines": { + "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.34.1", + "@img/sharp-darwin-x64": "0.34.1", + "@img/sharp-libvips-darwin-arm64": "1.1.0", + "@img/sharp-libvips-darwin-x64": "1.1.0", + "@img/sharp-libvips-linux-arm": "1.1.0", + "@img/sharp-libvips-linux-arm64": "1.1.0", + "@img/sharp-libvips-linux-ppc64": "1.1.0", + "@img/sharp-libvips-linux-s390x": "1.1.0", + "@img/sharp-libvips-linux-x64": "1.1.0", + "@img/sharp-libvips-linuxmusl-arm64": "1.1.0", + "@img/sharp-libvips-linuxmusl-x64": "1.1.0", + "@img/sharp-linux-arm": "0.34.1", + "@img/sharp-linux-arm64": "0.34.1", + "@img/sharp-linux-s390x": "0.34.1", + "@img/sharp-linux-x64": "0.34.1", + "@img/sharp-linuxmusl-arm64": "0.34.1", + "@img/sharp-linuxmusl-x64": "0.34.1", + "@img/sharp-wasm32": "0.34.1", + "@img/sharp-win32-ia32": "0.34.1", + "@img/sharp-win32-x64": "0.34.1" + } + }, + "node_modules/sharp/node_modules/semver": { + "version": "7.7.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.1.tgz", + "integrity": "sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==", + "license": "ISC", + "optional": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -40736,6 +41938,23 @@ "simple-concat": "^1.0.0" } }, + "node_modules/simple-swizzle": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/simple-swizzle/-/simple-swizzle-0.2.2.tgz", + "integrity": "sha512-JA//kQgZtbuY83m+xT+tXJkmJncGMTFT+C+g2h2R9uxkYIrE2yy9sgmcLhCnw57/WSD+Eh3J97FPEDFnbXnDUg==", + "license": "MIT", + "optional": true, + "dependencies": { + "is-arrayish": "^0.3.1" + } + }, + "node_modules/simple-swizzle/node_modules/is-arrayish": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.3.2.tgz", + "integrity": "sha512-eVRqCvVlZbuw3GrM63ovNSNAeA1K16kaR/LRY/92w0zxQ5/1YzwblUX652i4Xs9RwAGjW9d9y6X88t8OaAJfWQ==", + "license": "MIT", + "optional": true + }, "node_modules/simple-update-notifier": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/simple-update-notifier/-/simple-update-notifier-1.1.0.tgz", @@ -41339,17 +42558,17 @@ "node": ">= 0.8" } }, - "node_modules/stop-iteration-iterator": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/stop-iteration-iterator/-/stop-iteration-iterator-1.1.0.tgz", - "integrity": "sha512-eLoXW/DHyl62zxY4SCaIgnRhuMr6ri4juEYARS8E6sCEqzKpOiE521Ucofdx+KnDZl5xmvGYaaKCk5FEOxJCoQ==", + "node_modules/stdin-discarder": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/stdin-discarder/-/stdin-discarder-0.2.2.tgz", + "integrity": "sha512-UhDfHmA92YAlNnCfhmq0VeNL5bDbiZGg7sZ2IvPsXubGkiNa9EC+tUTsjBRsYUAz87btI6/1wf4XoVvQ3uRnmQ==", "dev": true, - "dependencies": { - "es-errors": "^1.3.0", - "internal-slot": "^1.1.0" - }, + "license": "MIT", "engines": { - "node": ">= 0.4" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, "node_modules/stoppable": { @@ -41761,9 +42980,10 @@ } }, "node_modules/styled-jsx": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.1.tgz", - "integrity": "sha512-pW7uC1l4mBZ8ugbiZrcIsiIvVx1UmTfw7UkC3Um2tmfUq9Bhk8IiyEIPl6F8agHgjzku6j0xQEZbfA5uSgSaCw==", + "version": "5.1.6", + "resolved": "https://registry.npmjs.org/styled-jsx/-/styled-jsx-5.1.6.tgz", + "integrity": "sha512-qSVyDTeMotdvQYoHWLNGwRFJHC+i+ZvdBRYosOFgC+Wg1vx4frN2/RG/NA7SYqqvKNLf39P2LSRA2pu6n0XYZA==", + "license": "MIT", "dependencies": { "client-only": "0.0.1" }, @@ -41771,7 +42991,7 @@ "node": ">= 12.0.0" }, "peerDependencies": { - "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0" + "react": ">= 16.8.0 || 17.x.x || ^18.0.0-0 || ^19.0.0-0" }, "peerDependenciesMeta": { "@babel/core": { @@ -42506,9 +43726,10 @@ } }, "node_modules/tar-fs": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.2.tgz", - "integrity": "sha512-EsaAXwxmx8UB7FRKqeozqEPop69DXcmYwTQwXvyAPF352HJsPdkVhvTaDPYqfNgruveJIJy3TA2l+2zj8LJIJA==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.3.tgz", + "integrity": "sha512-090nwYJDmlhwFwEW3QQl+vaNnxsO2yVsd45eTKRBzSzu+hlb1w2K9inVq5b0ngXuLVqQ4ApvsUHHnu/zQNkWAg==", + "license": "MIT", "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -42849,7 +44070,7 @@ "version": "1.21.0", "resolved": "https://registry.npmjs.org/thingies/-/thingies-1.21.0.tgz", "integrity": "sha512-hsqsJsFMsV+aD4s3CWKk85ep/3I9XzYV/IXaSouJMYIoDlgyi11cBhsqYe9/geRfB0YIikBQg6raRaM+nIMP9g==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10.18" }, @@ -42909,6 +44130,23 @@ "dev": true, "optional": true }, + "node_modules/tinyglobby": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.13.tgz", + "integrity": "sha512-mEwzpUgrLySlveBwEVDMKk5B57bhLPYovRfPAXD5gA/98Opn0rCDj3GtLwFvCvH5RK9uPCExUROW5NjDwvqkxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.4.4", + "picomatch": "^4.0.2" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, "node_modules/tmp": { "version": "0.0.33", "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz", @@ -43044,7 +44282,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/tree-dump/-/tree-dump-1.0.2.tgz", "integrity": "sha512-dpev9ABuLWdEubk+cIaI9cHwRNNDjkBBLXTwI4UCUFdQ5xXKqNXoK4FEciw/vxf+NQ7Cb7sGUyeUtORvHIdRXQ==", - "dev": true, + "devOptional": true, "engines": { "node": ">=10.0" }, @@ -43842,18 +45080,6 @@ "node": ">=4" } }, - "node_modules/unicorn-magic": { - "version": "0.1.0", - "resolved": "https://registry.npmjs.org/unicorn-magic/-/unicorn-magic-0.1.0.tgz", - "integrity": "sha512-lRfVq8fE8gz6QMBuDM6a+LO3IAzTi05H6gCVaUpir2E1Rwpo4ZUog45KpNXKC/Mn3Yb9UDuHumeFTo9iV/D9FQ==", - "dev": true, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/unique-filename": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-3.0.0.tgz", @@ -43926,9 +45152,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.2.tgz", - "integrity": "sha512-PPypAm5qvlD7XMZC3BujecnaOxwhrtoFR+Dqkk5Aa/6DssiH0ibKoketaj9w8LP7Bont1rYeoV5plxD7RTEPRg==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", + "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", "funding": [ { "type": "opencollective", @@ -43943,6 +45169,7 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" @@ -44247,9 +45474,9 @@ } }, "node_modules/vite": { - "version": "4.5.9", - "resolved": "https://identitydivision.pkgs.visualstudio.com/fac9d424-53d2-45c0-91b5-ef6ba7a6bf26/_packaging/dd15892d-fc68-4d1c-93a5-090f3b303f31/npm/registry/vite/-/vite-4.5.9.tgz", - "integrity": "sha1-9N/UxClXQ7UMPj+Q33mNcN5pnk8=", + "version": "4.5.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-4.5.14.tgz", + "integrity": "sha512-+v57oAaoYNnO3hIu5Z/tJRZjq5aHM2zDve9YZ8HngVHbhk66RStobhb1sqPMIPEleV6cNKYK4eGrAbE9Ulbl2g==", "dev": true, "license": "MIT", "peer": true, @@ -44903,6 +46130,7 @@ "resolved": "https://registry.npmjs.org/weak-lru-cache/-/weak-lru-cache-1.2.2.tgz", "integrity": "sha512-DEAoo25RfSYMuTGc9vPJzZcZullwIqRDSI9LOy+fkCJPi6hykCnfKaXTuPBDuXAUcqHXyOgFtHNp/kB2FjYHbw==", "dev": true, + "license": "MIT", "optional": true }, "node_modules/webidl-conversions": { @@ -44915,12 +46143,14 @@ } }, "node_modules/webpack": { - "version": "5.97.1", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.97.1.tgz", - "integrity": "sha512-EksG6gFY3L1eFMROS/7Wzgrii5mBAFe4rIr3r2BTfo7bcc+DWwFZ4OJ/miOuHJO/A85HwyI4eQ0F6IKXesO7Fg==", + "version": "5.99.8", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.8.tgz", + "integrity": "sha512-lQ3CPiSTpfOnrEGeXDwoq5hIGzSjmwD72GdfVzF7CQAI7t47rJG9eDWvcEkEn3CUQymAElVvDg3YNTlCYj+qUQ==", + "license": "MIT", "dependencies": { "@types/eslint-scope": "^3.7.7", "@types/estree": "^1.0.6", + "@types/json-schema": "^7.0.15", "@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1", @@ -44937,9 +46167,9 @@ "loader-runner": "^4.2.0", "mime-types": "^2.1.27", "neo-async": "^2.6.2", - "schema-utils": "^3.2.0", + "schema-utils": "^4.3.2", "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.10", + "terser-webpack-plugin": "^5.3.11", "watchpack": "^2.4.1", "webpack-sources": "^3.2.3" }, @@ -45077,93 +46307,248 @@ } }, "node_modules/webpack-dev-server": { - "version": "4.11.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.11.1.tgz", - "integrity": "sha512-lILVz9tAUy1zGFwieuaQtYiadImb5M3d+H+L1zDYalYoDl0cksAB1UNyuE5MMWJrG6zR1tXkCP2fitl7yoUJiw==", + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.1.tgz", + "integrity": "sha512-ml/0HIj9NLpVKOMq+SuBPLHcmbG+TGIjXRHsYfZwocUBIqEvws8NnS/V9AFQ5FKP+tgn5adwVwRrTEpGL33QFQ==", + "devOptional": true, + "license": "MIT", "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", + "@types/bonjour": "^3.5.13", + "@types/connect-history-api-fallback": "^1.5.4", + "@types/express": "^4.17.21", + "@types/express-serve-static-core": "^4.17.21", + "@types/serve-index": "^1.9.4", + "@types/serve-static": "^1.15.5", + "@types/sockjs": "^0.3.36", + "@types/ws": "^8.5.10", "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", + "bonjour-service": "^1.2.1", + "chokidar": "^3.6.0", "colorette": "^2.0.10", "compression": "^1.7.4", "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", + "express": "^4.21.2", "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", + "http-proxy-middleware": "^2.0.7", + "ipaddr.js": "^2.1.0", + "launch-editor": "^2.6.1", + "open": "^10.0.3", + "p-retry": "^6.2.0", + "schema-utils": "^4.2.0", + "selfsigned": "^2.4.1", "serve-index": "^1.9.1", "sockjs": "^0.3.24", "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.4.2" + "webpack-dev-middleware": "^7.4.2", + "ws": "^8.18.0" }, "bin": { "webpack-dev-server": "bin/webpack-dev-server.js" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" + "webpack": "^5.0.0" }, "peerDependenciesMeta": { + "webpack": { + "optional": true + }, "webpack-cli": { "optional": true } } }, + "node_modules/webpack-dev-server/node_modules/@types/retry": { + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", + "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", + "devOptional": true, + "license": "MIT" + }, + "node_modules/webpack-dev-server/node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/webpack-dev-server/node_modules/define-lazy-prop": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", + "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "devOptional": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/webpack-dev-server/node_modules/ipaddr.js": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", + "devOptional": true, "engines": { "node": ">= 10" } }, + "node_modules/webpack-dev-server/node_modules/is-wsl": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", + "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "is-inside-container": "^1.0.0" + }, + "engines": { + "node": ">=16" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/memfs": { + "version": "4.17.2", + "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.2.tgz", + "integrity": "sha512-NgYhCOWgovOXSzvYgUW0LQ7Qy72rWQMGGFJDoWg4G30RHd3z77VbYdtJ4fembJXBy8pMIUA31XNAupobOQlwdg==", + "devOptional": true, + "license": "Apache-2.0", + "dependencies": { + "@jsonjoy.com/json-pack": "^1.0.3", + "@jsonjoy.com/util": "^1.3.0", + "tree-dump": "^1.0.1", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">= 4.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/streamich" + } + }, + "node_modules/webpack-dev-server/node_modules/open": { + "version": "10.1.2", + "resolved": "https://registry.npmjs.org/open/-/open-10.1.2.tgz", + "integrity": "sha512-cxN6aIDPz6rm8hbebcP7vrQNhvRcveZoJU72Y7vskh4oIm+BZwBECnx5nTmrlres1Qapvx27Qo1Auukpf8PKXw==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "default-browser": "^5.2.1", + "define-lazy-prop": "^3.0.0", + "is-inside-container": "^1.0.0", + "is-wsl": "^3.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/p-retry": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", + "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", + "devOptional": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.2", + "is-network-error": "^1.0.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=16.17" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/webpack-dev-server/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "devOptional": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/webpack-dev-server/node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", + "version": "7.4.2", + "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", + "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", + "devOptional": true, + "license": "MIT", "dependencies": { "colorette": "^2.0.10", - "memfs": "^3.4.3", + "memfs": "^4.6.0", "mime-types": "^2.1.31", + "on-finished": "^2.4.1", "range-parser": "^1.2.1", "schema-utils": "^4.0.0" }, "engines": { - "node": ">= 12.13.0" + "node": ">= 18.12.0" }, "funding": { "type": "opencollective", "url": "https://opencollective.com/webpack" }, "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "webpack": { + "optional": true + } } }, "node_modules/webpack-dev-server/node_modules/ws": { "version": "8.18.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", + "devOptional": true, "engines": { "node": ">=10.0.0" }, @@ -45308,23 +46693,6 @@ "node": ">=4.0" } }, - "node_modules/webpack/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, "node_modules/websocket-driver": { "version": "0.7.4", "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", @@ -46439,6 +47807,7 @@ "resolved": "https://registry.npmjs.org/yoctocolors-cjs/-/yoctocolors-cjs-2.1.2.tgz", "integrity": "sha512-cYVsTjKl8b+FrnidjibDWskAv7UKOfcwaVZdp/it9n1s9fU3IkgDbhdIRKCW4JDsAlECJY0ytoVPT3sK6kideA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -46478,7 +47847,7 @@ "license": "MIT", "dependencies": { "@azure/identity": "^4.5.0", - "axios": "^1.7.4", + "axios": "^1.9.0", "dotenv": "^8.2.0", "find-process": "^1.4.4" }, @@ -46514,7 +47883,7 @@ }, "devDependencies": { "@angular-devkit/build-angular": "^16.0.5", - "@angular/cli": "~16.0.5", + "@angular/cli": "^16.0.5", "@angular/compiler-cli": "^16.0.4", "@types/jasmine": "~4.0.0", "@types/jasminewd2": "^2.0.10", @@ -46785,14 +48154,15 @@ } }, "samples/msal-angular-samples/angular-b2c-sample/node_modules/@angular-devkit/schematics": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.0.6.tgz", - "integrity": "sha512-Ipd3uEPgR0qz9HYQvY3RpWHO1DH34mQ6AShKiBypCCd/iwJPcJLKUVon2wYEfKlspgg9N8qWIuoMVHZG0Vwqgg==", + "version": "16.2.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.16.tgz", + "integrity": "sha512-pF6fdtJh6yLmgA7Gs45JIdxPl2MsTAhYcZIMrX1a6ID64dfwtF0MP8fDE6vrWInV1zXbzzf7l7PeKuqVtTSzKg==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "16.0.6", + "@angular-devkit/core": "16.2.16", "jsonc-parser": "3.2.0", - "magic-string": "0.30.0", + "magic-string": "0.30.1", "ora": "5.4.1", "rxjs": "7.8.1" }, @@ -46802,44 +48172,6 @@ "yarn": ">= 1.13.0" } }, - "samples/msal-angular-samples/angular-b2c-sample/node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.6.tgz", - "integrity": "sha512-pHbDUwXDMTWTnX/vafkFnzvYDQD8lz+w8FvMQE23Q/vN6/Q0BRf0PWTAGla6Wt+E4HaqqrbQS5P0YBwS4te2Pw==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "samples/msal-angular-samples/angular-b2c-sample/node_modules/@angular-devkit/schematics/node_modules/magic-string": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", - "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, "samples/msal-angular-samples/angular-b2c-sample/node_modules/@angular/animations": { "version": "16.2.12", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.12.tgz", @@ -46871,27 +48203,28 @@ } }, "samples/msal-angular-samples/angular-b2c-sample/node_modules/@angular/cli": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.0.6.tgz", - "integrity": "sha512-um7oOWSu9SIzvwqJ5Aeqcki5/qj4yb6QKi8RkHDWpOdrg1tJfX/BnIzUa4jiCXIwYRIz+PjYJb8W5216wS+7Gg==", + "version": "16.2.16", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.16.tgz", + "integrity": "sha512-aqfNYZ45ndrf36i+7AhQ9R8BCm025j7TtYaUmvvjT4LwiUg6f6KtlZPB/ivBlXmd1g9oXqW4advL0AIi8A/Ozg==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1600.6", - "@angular-devkit/core": "16.0.6", - "@angular-devkit/schematics": "16.0.6", - "@schematics/angular": "16.0.6", + "@angular-devkit/architect": "0.1602.16", + "@angular-devkit/core": "16.2.16", + "@angular-devkit/schematics": "16.2.16", + "@schematics/angular": "16.2.16", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", - "ini": "4.0.0", + "ini": "4.1.1", "inquirer": "8.2.4", "jsonc-parser": "3.2.0", "npm-package-arg": "10.1.0", "npm-pick-manifest": "8.0.1", "open": "8.4.2", "ora": "5.4.1", - "pacote": "15.1.3", + "pacote": "15.2.0", "resolve": "1.22.2", - "semver": "7.4.0", + "semver": "7.5.4", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, @@ -46904,62 +48237,6 @@ "yarn": ">= 1.13.0" } }, - "samples/msal-angular-samples/angular-b2c-sample/node_modules/@angular/cli/node_modules/@angular-devkit/architect": { - "version": "0.1600.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1600.6.tgz", - "integrity": "sha512-Mk/pRujuer5qRMrgC7DPwLQ88wTAEKhbs0yJ/1prm4cx+VkxX9MMf6Y4AHKRmduKmFmd2LmX21/ACiU65acH8w==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "16.0.6", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "samples/msal-angular-samples/angular-b2c-sample/node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.6.tgz", - "integrity": "sha512-pHbDUwXDMTWTnX/vafkFnzvYDQD8lz+w8FvMQE23Q/vN6/Q0BRf0PWTAGla6Wt+E4HaqqrbQS5P0YBwS4te2Pw==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "samples/msal-angular-samples/angular-b2c-sample/node_modules/@angular/cli/node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "samples/msal-angular-samples/angular-b2c-sample/node_modules/@angular/common": { "version": "16.2.12", "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.12.tgz", @@ -47941,13 +49218,14 @@ } }, "samples/msal-angular-samples/angular-b2c-sample/node_modules/@schematics/angular": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.0.6.tgz", - "integrity": "sha512-8naIlMeY9p5iOZqc3D0reoN80xm/fQINrG8mqIOgIY6bDeqfFvMKfaozA3PbPLbZhl5Jyk7VfZnXb6ISN0KnxQ==", + "version": "16.2.16", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.16.tgz", + "integrity": "sha512-V4cE4R5MbusKaNW9DWsisiSRUoQzbAaBIeJh42yCkg5H/lUdf18hUB7DG6Pl7yH6/tjzzz4SqIVD7N64uCDC2A==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "16.0.6", - "@angular-devkit/schematics": "16.0.6", + "@angular-devkit/core": "16.2.16", + "@angular-devkit/schematics": "16.2.16", "jsonc-parser": "3.2.0" }, "engines": { @@ -47956,32 +49234,6 @@ "yarn": ">= 1.13.0" } }, - "samples/msal-angular-samples/angular-b2c-sample/node_modules/@schematics/angular/node_modules/@angular-devkit/core": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.6.tgz", - "integrity": "sha512-pHbDUwXDMTWTnX/vafkFnzvYDQD8lz+w8FvMQE23Q/vN6/Q0BRf0PWTAGla6Wt+E4HaqqrbQS5P0YBwS4te2Pw==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, "samples/msal-angular-samples/angular-b2c-sample/node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -48631,10 +49883,11 @@ "dev": true }, "samples/msal-angular-samples/angular-b2c-sample/node_modules/ini": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.0.0.tgz", - "integrity": "sha512-t0ikzf5qkSFqRl1e6ejKBe+Tk2bsQd8ivEkcisyGXsku2t8NvXZ1Y3RRz5vxrDgOrTBOi13CvGsVoI5wVpd7xg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", "dev": true, + "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -48876,6 +50129,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=8" } @@ -49583,10 +50837,11 @@ } }, "samples/msal-angular-samples/angular-b2c-sample/node_modules/pacote": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.1.3.tgz", - "integrity": "sha512-aRts8cZqxiJVDitmAh+3z+FxuO3tLNWEmwDRPEpDDiZJaRz06clP4XX112ynMT5uF0QNoMPajBBHnaStUEPJXA==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz", + "integrity": "sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==", "dev": true, + "license": "ISC", "dependencies": { "@npmcli/git": "^4.0.0", "@npmcli/installed-package-contents": "^2.0.1", @@ -49757,6 +51012,7 @@ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", "dev": true, + "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -49775,6 +51031,7 @@ "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", "dev": true, + "license": "ISC", "dependencies": { "json-parse-even-better-errors": "^3.0.0", "npm-normalize-package-bin": "^3.0.0" @@ -49788,6 +51045,7 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", "dev": true, + "license": "MIT", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -50372,7 +51630,7 @@ }, "devDependencies": { "@angular-devkit/build-angular": "^16.0.4", - "@angular/cli": "~16.0.4", + "@angular/cli": "^16.0.4", "@angular/compiler-cli": "^16.0.4", "@types/jasmine": "~4.3.0", "@types/jest": "^29.5.0", @@ -50638,14 +51896,15 @@ } }, "samples/msal-angular-samples/angular-modules-sample/node_modules/@angular-devkit/schematics": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.0.6.tgz", - "integrity": "sha512-Ipd3uEPgR0qz9HYQvY3RpWHO1DH34mQ6AShKiBypCCd/iwJPcJLKUVon2wYEfKlspgg9N8qWIuoMVHZG0Vwqgg==", + "version": "16.2.16", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-16.2.16.tgz", + "integrity": "sha512-pF6fdtJh6yLmgA7Gs45JIdxPl2MsTAhYcZIMrX1a6ID64dfwtF0MP8fDE6vrWInV1zXbzzf7l7PeKuqVtTSzKg==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "16.0.6", + "@angular-devkit/core": "16.2.16", "jsonc-parser": "3.2.0", - "magic-string": "0.30.0", + "magic-string": "0.30.1", "ora": "5.4.1", "rxjs": "7.8.1" }, @@ -50655,44 +51914,6 @@ "yarn": ">= 1.13.0" } }, - "samples/msal-angular-samples/angular-modules-sample/node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.6.tgz", - "integrity": "sha512-pHbDUwXDMTWTnX/vafkFnzvYDQD8lz+w8FvMQE23Q/vN6/Q0BRf0PWTAGla6Wt+E4HaqqrbQS5P0YBwS4te2Pw==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "samples/msal-angular-samples/angular-modules-sample/node_modules/@angular-devkit/schematics/node_modules/magic-string": { - "version": "0.30.0", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.0.tgz", - "integrity": "sha512-LA+31JYDJLs82r2ScLrlz1GjSgu66ZV518eyWT+S8VhyQn/JL0u9MeBOvQMGYiPk1DBiSN9DDMOcXvigJZaViQ==", - "dev": true, - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.4.13" - }, - "engines": { - "node": ">=12" - } - }, "samples/msal-angular-samples/angular-modules-sample/node_modules/@angular/animations": { "version": "16.2.12", "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-16.2.12.tgz", @@ -50725,27 +51946,28 @@ } }, "samples/msal-angular-samples/angular-modules-sample/node_modules/@angular/cli": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.0.6.tgz", - "integrity": "sha512-um7oOWSu9SIzvwqJ5Aeqcki5/qj4yb6QKi8RkHDWpOdrg1tJfX/BnIzUa4jiCXIwYRIz+PjYJb8W5216wS+7Gg==", + "version": "16.2.16", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-16.2.16.tgz", + "integrity": "sha512-aqfNYZ45ndrf36i+7AhQ9R8BCm025j7TtYaUmvvjT4LwiUg6f6KtlZPB/ivBlXmd1g9oXqW4advL0AIi8A/Ozg==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1600.6", - "@angular-devkit/core": "16.0.6", - "@angular-devkit/schematics": "16.0.6", - "@schematics/angular": "16.0.6", + "@angular-devkit/architect": "0.1602.16", + "@angular-devkit/core": "16.2.16", + "@angular-devkit/schematics": "16.2.16", + "@schematics/angular": "16.2.16", "@yarnpkg/lockfile": "1.1.0", "ansi-colors": "4.1.3", - "ini": "4.0.0", + "ini": "4.1.1", "inquirer": "8.2.4", "jsonc-parser": "3.2.0", "npm-package-arg": "10.1.0", "npm-pick-manifest": "8.0.1", "open": "8.4.2", "ora": "5.4.1", - "pacote": "15.1.3", + "pacote": "15.2.0", "resolve": "1.22.2", - "semver": "7.4.0", + "semver": "7.5.4", "symbol-observable": "4.0.0", "yargs": "17.7.2" }, @@ -50758,62 +51980,6 @@ "yarn": ">= 1.13.0" } }, - "samples/msal-angular-samples/angular-modules-sample/node_modules/@angular/cli/node_modules/@angular-devkit/architect": { - "version": "0.1600.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1600.6.tgz", - "integrity": "sha512-Mk/pRujuer5qRMrgC7DPwLQ88wTAEKhbs0yJ/1prm4cx+VkxX9MMf6Y4AHKRmduKmFmd2LmX21/ACiU65acH8w==", - "dev": true, - "dependencies": { - "@angular-devkit/core": "16.0.6", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - } - }, - "samples/msal-angular-samples/angular-modules-sample/node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.6.tgz", - "integrity": "sha512-pHbDUwXDMTWTnX/vafkFnzvYDQD8lz+w8FvMQE23Q/vN6/Q0BRf0PWTAGla6Wt+E4HaqqrbQS5P0YBwS4te2Pw==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "samples/msal-angular-samples/angular-modules-sample/node_modules/@angular/cli/node_modules/semver": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.4.0.tgz", - "integrity": "sha512-RgOxM8Mw+7Zus0+zcLEUn8+JfoLpj/huFTItQy2hsM4khuC1HYRDp0cU482Ewn/Fcy6bCjufD8vAj7voC66KQw==", - "dev": true, - "dependencies": { - "lru-cache": "^6.0.0" - }, - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, "samples/msal-angular-samples/angular-modules-sample/node_modules/@angular/common": { "version": "16.2.12", "resolved": "https://registry.npmjs.org/@angular/common/-/common-16.2.12.tgz", @@ -51795,13 +52961,14 @@ } }, "samples/msal-angular-samples/angular-modules-sample/node_modules/@schematics/angular": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.0.6.tgz", - "integrity": "sha512-8naIlMeY9p5iOZqc3D0reoN80xm/fQINrG8mqIOgIY6bDeqfFvMKfaozA3PbPLbZhl5Jyk7VfZnXb6ISN0KnxQ==", + "version": "16.2.16", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-16.2.16.tgz", + "integrity": "sha512-V4cE4R5MbusKaNW9DWsisiSRUoQzbAaBIeJh42yCkg5H/lUdf18hUB7DG6Pl7yH6/tjzzz4SqIVD7N64uCDC2A==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "16.0.6", - "@angular-devkit/schematics": "16.0.6", + "@angular-devkit/core": "16.2.16", + "@angular-devkit/schematics": "16.2.16", "jsonc-parser": "3.2.0" }, "engines": { @@ -51810,32 +52977,6 @@ "yarn": ">= 1.13.0" } }, - "samples/msal-angular-samples/angular-modules-sample/node_modules/@schematics/angular/node_modules/@angular-devkit/core": { - "version": "16.0.6", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-16.0.6.tgz", - "integrity": "sha512-pHbDUwXDMTWTnX/vafkFnzvYDQD8lz+w8FvMQE23Q/vN6/Q0BRf0PWTAGla6Wt+E4HaqqrbQS5P0YBwS4te2Pw==", - "dev": true, - "dependencies": { - "ajv": "8.12.0", - "ajv-formats": "2.1.1", - "jsonc-parser": "3.2.0", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^16.14.0 || >=18.10.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^3.5.2" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, "samples/msal-angular-samples/angular-modules-sample/node_modules/@tootallnate/once": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-1.1.2.tgz", @@ -52475,10 +53616,11 @@ "dev": true }, "samples/msal-angular-samples/angular-modules-sample/node_modules/ini": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.0.0.tgz", - "integrity": "sha512-t0ikzf5qkSFqRl1e6ejKBe+Tk2bsQd8ivEkcisyGXsku2t8NvXZ1Y3RRz5vxrDgOrTBOi13CvGsVoI5wVpd7xg==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", + "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", "dev": true, + "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -52759,6 +53901,7 @@ "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=8" } @@ -53517,10 +54660,11 @@ } }, "samples/msal-angular-samples/angular-modules-sample/node_modules/pacote": { - "version": "15.1.3", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.1.3.tgz", - "integrity": "sha512-aRts8cZqxiJVDitmAh+3z+FxuO3tLNWEmwDRPEpDDiZJaRz06clP4XX112ynMT5uF0QNoMPajBBHnaStUEPJXA==", + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-15.2.0.tgz", + "integrity": "sha512-rJVZeIwHTUta23sIZgEIM62WYwbmGbThdbnkt81ravBplQv+HjyroqnLRNH2+sLJHcGZmLRmhPwACqhfTcOmnA==", "dev": true, + "license": "ISC", "dependencies": { "@npmcli/git": "^4.0.0", "@npmcli/installed-package-contents": "^2.0.1", @@ -53692,6 +54836,7 @@ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-3.0.0.tgz", "integrity": "sha512-++Vn7NS4Xf9NacaU9Xq3URUuqZETPsf8L4j5/ckhaRYsfPeRyzGw+iDjFhV/Jr3uNmTvvddEJFWh5R1gRgUH8A==", "dev": true, + "license": "ISC", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -53710,6 +54855,7 @@ "resolved": "https://registry.npmjs.org/read-package-json-fast/-/read-package-json-fast-3.0.2.tgz", "integrity": "sha512-0J+Msgym3vrLOUB3hzQCuZHII0xkNGCtz/HJH9xZshwv9DbDwkw1KaE3gx/e2J5rpEY5rtOy6cyhKOPrkP7FZw==", "dev": true, + "license": "ISC", "dependencies": { "json-parse-even-better-errors": "^3.0.0", "npm-normalize-package-bin": "^3.0.0" @@ -53723,6 +54869,7 @@ "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-3.0.2.tgz", "integrity": "sha512-fi0NG4bPjCHunUJffmLd0gxssIgkNmArMvis4iNah6Owg1MCJjWhEcDLmsK6iGkJq3tHwbDkTlce70/tmXN4cQ==", "dev": true, + "license": "MIT", "engines": { "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } @@ -54306,15 +55453,15 @@ "samples/msal-angular-samples/angular-standalone-sample": { "version": "0.0.0", "dependencies": { - "@angular/animations": "^19.1.0", - "@angular/common": "^19.1.0", - "@angular/compiler": "^19.1.0", - "@angular/core": "^19.1.0", - "@angular/forms": "^19.1.0", - "@angular/material": "^18.0.0", - "@angular/platform-browser": "^19.1.0", - "@angular/platform-browser-dynamic": "^19.1.0", - "@angular/router": "^19.1.0", + "@angular/animations": "^20.0.4", + "@angular/common": "^20.0.4", + "@angular/compiler": "^20.0.4", + "@angular/core": "^20.0.4", + "@angular/forms": "^20.0.4", + "@angular/material": "^20.0.3", + "@angular/platform-browser": "^20.0.4", + "@angular/platform-browser-dynamic": "^20.0.4", + "@angular/router": "^20.0.4", "@azure/msal-angular": "^4.0.0", "@azure/msal-browser": "^4.0.0", "rxjs": "~7.8.0", @@ -54322,9 +55469,9 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^19.1.0", - "@angular/cli": "^19.1.0", - "@angular/compiler-cli": "^19.1.0", + "@angular/build": "^20.0.3", + "@angular/cli": "^20.0.3", + "@angular/compiler-cli": "^20.0.4", "@types/jasmine": "~5.1.0", "@types/jest": "^29.5.12", "e2e-test-utils": "file:../../e2eTestUtils", @@ -54337,7 +55484,7 @@ "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "ts-jest": "^29.1.2", - "typescript": "~5.7.3" + "typescript": "~5.8.3" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@ampproject/remapping": { @@ -54345,6 +55492,7 @@ "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", "dev": true, + "license": "Apache-2.0", "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" @@ -54354,35 +55502,37 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/architect": { - "version": "0.1901.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1901.4.tgz", - "integrity": "sha512-EoRTN8p7z0YnqOEIJKKu/NwSsCJxFkyGuZOobz7btnUWwlDqG8CNAhJgtlsOXPihwEkHEkzRIm1feDkWEjCYsA==", + "version": "0.2000.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2000.3.tgz", + "integrity": "sha512-37S4dzlwB3C8gnBlwxjjvNUqwSeKnDe2j1XWg7sj94kbg/jLJV0Db/Dvb7zJjKher6Ed1Bnj3pMOM206ALJW2A==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.4", - "rxjs": "7.8.1" + "@angular-devkit/core": "20.0.3", + "rxjs": "7.8.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/architect/node_modules/@angular-devkit/core": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.4.tgz", - "integrity": "sha512-IDvSSiQgaixH2RtZtIpq1+XaHeuzMiTWfDyNF9DuYcU+S8CdG1SWrc8d59tmOrM/q+IRGyFgbBhTU1un52hNHw==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.0.3.tgz", + "integrity": "sha512-XgEIbIky0pMtJSomHRaf16BT1jzJNQCm2geNZ642n3cj8fYLm4jHJX/r738kIfbHWoWXT/hlTmVgIH9TdQPicA==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", - "rxjs": "7.8.1", + "rxjs": "7.8.2", "source-map": "0.7.4" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -54400,6 +55550,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "dependencies": { @@ -54413,211 +55564,11 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/architect/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/build-angular": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-19.1.4.tgz", - "integrity": "sha512-t8qC26Boz1aAMt2xVKthwEXRqMI4ZVwelxRNfHryLdLTujTaehFt3qbjxukMmRGCWmQObauH0UOvDh3pAA24dQ==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1901.4", - "@angular-devkit/build-webpack": "0.1901.4", - "@angular-devkit/core": "19.1.4", - "@angular/build": "19.1.4", - "@babel/core": "7.26.0", - "@babel/generator": "7.26.3", - "@babel/helper-annotate-as-pure": "7.25.9", - "@babel/helper-split-export-declaration": "7.24.7", - "@babel/plugin-transform-async-generator-functions": "7.25.9", - "@babel/plugin-transform-async-to-generator": "7.25.9", - "@babel/plugin-transform-runtime": "7.25.9", - "@babel/preset-env": "7.26.0", - "@babel/runtime": "7.26.0", - "@discoveryjs/json-ext": "0.6.3", - "@ngtools/webpack": "19.1.4", - "@vitejs/plugin-basic-ssl": "1.2.0", - "ansi-colors": "4.1.3", - "autoprefixer": "10.4.20", - "babel-loader": "9.2.1", - "browserslist": "^4.21.5", - "copy-webpack-plugin": "12.0.2", - "css-loader": "7.1.2", - "esbuild-wasm": "0.24.2", - "fast-glob": "3.3.3", - "http-proxy-middleware": "3.0.3", - "istanbul-lib-instrument": "6.0.3", - "jsonc-parser": "3.3.1", - "karma-source-map-support": "1.4.0", - "less": "4.2.1", - "less-loader": "12.2.0", - "license-webpack-plugin": "4.0.2", - "loader-utils": "3.3.1", - "mini-css-extract-plugin": "2.9.2", - "open": "10.1.0", - "ora": "5.4.1", - "picomatch": "4.0.2", - "piscina": "4.8.0", - "postcss": "8.4.49", - "postcss-loader": "8.1.1", - "resolve-url-loader": "5.0.0", - "rxjs": "7.8.1", - "sass": "1.83.1", - "sass-loader": "16.0.4", - "semver": "7.6.3", - "source-map-loader": "5.0.0", - "source-map-support": "0.5.21", - "terser": "5.37.0", - "tree-kill": "1.2.2", - "tslib": "2.8.1", - "webpack": "5.97.1", - "webpack-dev-middleware": "7.4.2", - "webpack-dev-server": "5.2.0", - "webpack-merge": "6.0.1", - "webpack-subresource-integrity": "5.1.0" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "optionalDependencies": { - "esbuild": "0.24.2" - }, - "peerDependencies": { - "@angular/compiler-cli": "^19.0.0", - "@angular/localize": "^19.0.0", - "@angular/platform-server": "^19.0.0", - "@angular/service-worker": "^19.0.0", - "@angular/ssr": "^19.1.4", - "@web/test-runner": "^0.19.0", - "browser-sync": "^3.0.2", - "jest": "^29.5.0", - "jest-environment-jsdom": "^29.5.0", - "karma": "^6.3.0", - "ng-packagr": "^19.0.0", - "protractor": "^7.0.0", - "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.5 <5.8" - }, - "peerDependenciesMeta": { - "@angular/localize": { - "optional": true - }, - "@angular/platform-server": { - "optional": true - }, - "@angular/service-worker": { - "optional": true - }, - "@angular/ssr": { - "optional": true - }, - "@web/test-runner": { - "optional": true - }, - "browser-sync": { - "optional": true - }, - "jest": { - "optional": true - }, - "jest-environment-jsdom": { - "optional": true - }, - "karma": { - "optional": true - }, - "ng-packagr": { - "optional": true - }, - "protractor": { - "optional": true - }, - "tailwindcss": { - "optional": true - } - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/core": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.4.tgz", - "integrity": "sha512-IDvSSiQgaixH2RtZtIpq1+XaHeuzMiTWfDyNF9DuYcU+S8CdG1SWrc8d59tmOrM/q+IRGyFgbBhTU1un52hNHw==", - "dev": true, - "dependencies": { - "ajv": "8.17.1", - "ajv-formats": "3.0.1", - "jsonc-parser": "3.3.1", - "picomatch": "4.0.2", - "rxjs": "7.8.1", - "source-map": "0.7.4" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "chokidar": "^4.0.0" - }, - "peerDependenciesMeta": { - "chokidar": { - "optional": true - } - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/build-angular/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "readdirp": "^4.0.1" - }, - "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/build-angular/node_modules/http-proxy-middleware": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", - "integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", - "dev": true, - "dependencies": { - "@types/http-proxy": "^1.17.15", - "debug": "^4.3.6", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.3", - "is-plain-object": "^5.0.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/build-angular/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "engines": { @@ -54628,58 +55579,41 @@ "url": "https://paulmillr.com/funding/" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/build-webpack": { - "version": "0.1901.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1901.4.tgz", - "integrity": "sha512-C/Cd1JeRTy2P/powIldc5UZObw92TDGATD/LFlfPfi94celLa2DlEL1ybPTpnGs/R5/q5R26F6fbhmAVSeTJ8g==", - "dev": true, - "dependencies": { - "@angular-devkit/architect": "0.1901.4", - "rxjs": "7.8.1" - }, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "webpack": "^5.30.0", - "webpack-dev-server": "^5.0.2" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/schematics": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-19.1.4.tgz", - "integrity": "sha512-EKXBkx6EDcvyO+U68w/eXicRaF92zSSzYNvR3tMZszEKYE6xBr3kZxY99PP54HXQHR4zYwLvFJVp+T6bnvte2w==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-20.0.3.tgz", + "integrity": "sha512-T679AQXenG6e4fdC/HXrps0Dqy1EYKb4pFNLQqZHR9mfyeq/vxFWs3ga/yMiqvqMPUK5W5FucEpFZJQQmc7M+w==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.4", + "@angular-devkit/core": "20.0.3", "jsonc-parser": "3.3.1", "magic-string": "0.30.17", - "ora": "5.4.1", - "rxjs": "7.8.1" + "ora": "8.2.0", + "rxjs": "7.8.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/schematics/node_modules/@angular-devkit/core": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.4.tgz", - "integrity": "sha512-IDvSSiQgaixH2RtZtIpq1+XaHeuzMiTWfDyNF9DuYcU+S8CdG1SWrc8d59tmOrM/q+IRGyFgbBhTU1un52hNHw==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.0.3.tgz", + "integrity": "sha512-XgEIbIky0pMtJSomHRaf16BT1jzJNQCm2geNZ642n3cj8fYLm4jHJX/r738kIfbHWoWXT/hlTmVgIH9TdQPicA==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", - "rxjs": "7.8.1", + "rxjs": "7.8.2", "source-map": "0.7.4" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -54697,6 +55631,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "dependencies": { @@ -54710,10 +55645,11 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular-devkit/schematics/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "engines": { @@ -54725,76 +55661,91 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/animations": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-19.1.3.tgz", - "integrity": "sha512-MI+Tbp9OOisrQtTQH7o+xiQCODXicCs8WHNpGzdCpnXdRkQuVSOb6xAjD9OXJqcQGotLgeyennnkIJGXdz4RTA==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-20.0.4.tgz", + "integrity": "sha512-s0kRcEply2A1ThvFmb0+o+hEpAbPn08lpK8xjWZryM4cMrwjgsUE0OZHZPBANP4I1xT7Z82l+fmQbH+vX48EyA==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "19.1.3" + "@angular/common": "20.0.4", + "@angular/core": "20.0.4" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/build": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-19.1.4.tgz", - "integrity": "sha512-yfvLeUT2a8JTuVBY259vsSv0uLyhikHHgQcWa3VSr0TvCKrwCsBIFDq7vqmhLqIVWi/Z4D7n3J5JQAbDrl38Sg==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-20.0.3.tgz", + "integrity": "sha512-xA5eTGop85SI/+hfiOSJR/xI1w1NK3qylpEZ277YRaw8Ikh7r1DKPJOMGBfXNd8QsZYBSWGHA8SXvCmOh/hvLQ==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1901.4", - "@babel/core": "7.26.0", - "@babel/helper-annotate-as-pure": "7.25.9", + "@angular-devkit/architect": "0.2000.3", + "@babel/core": "7.27.1", + "@babel/helper-annotate-as-pure": "7.27.1", "@babel/helper-split-export-declaration": "7.24.7", - "@babel/plugin-syntax-import-attributes": "7.26.0", - "@inquirer/confirm": "5.1.1", - "@vitejs/plugin-basic-ssl": "1.2.0", - "beasties": "0.2.0", + "@inquirer/confirm": "5.1.10", + "@vitejs/plugin-basic-ssl": "2.0.0", + "beasties": "0.3.4", "browserslist": "^4.23.0", - "esbuild": "0.24.2", - "fast-glob": "3.3.3", + "esbuild": "0.25.5", "https-proxy-agent": "7.0.6", "istanbul-lib-instrument": "6.0.3", - "listr2": "8.2.5", + "jsonc-parser": "3.3.1", + "listr2": "8.3.3", "magic-string": "0.30.17", - "mrmime": "2.0.0", - "parse5-html-rewriting-stream": "7.0.0", + "mrmime": "2.0.1", + "parse5-html-rewriting-stream": "7.1.0", "picomatch": "4.0.2", - "piscina": "4.8.0", - "rollup": "4.30.1", - "sass": "1.83.1", - "semver": "7.6.3", - "vite": "6.0.7", + "piscina": "5.0.0", + "rollup": "4.40.2", + "sass": "1.88.0", + "semver": "7.7.2", + "source-map-support": "0.5.21", + "tinyglobby": "0.2.13", + "vite": "6.3.5", "watchpack": "2.4.2" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, "optionalDependencies": { - "lmdb": "3.2.2" - }, - "peerDependencies": { - "@angular/compiler": "^19.0.0", - "@angular/compiler-cli": "^19.0.0", - "@angular/localize": "^19.0.0", - "@angular/platform-server": "^19.0.0", - "@angular/service-worker": "^19.0.0", - "@angular/ssr": "^19.1.4", + "lmdb": "3.3.0" + }, + "peerDependencies": { + "@angular/compiler": "^20.0.0", + "@angular/compiler-cli": "^20.0.0", + "@angular/core": "^20.0.0", + "@angular/localize": "^20.0.0", + "@angular/platform-browser": "^20.0.0", + "@angular/platform-server": "^20.0.0", + "@angular/service-worker": "^20.0.0", + "@angular/ssr": "^20.0.3", + "karma": "^6.4.0", "less": "^4.2.0", - "ng-packagr": "^19.0.0", + "ng-packagr": "^20.0.0", "postcss": "^8.4.0", - "tailwindcss": "^2.0.0 || ^3.0.0", - "typescript": ">=5.5 <5.8" + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", + "tslib": "^2.3.0", + "typescript": ">=5.8 <5.9", + "vitest": "^3.1.1" }, "peerDependenciesMeta": { + "@angular/core": { + "optional": true + }, "@angular/localize": { "optional": true }, + "@angular/platform-browser": { + "optional": true + }, "@angular/platform-server": { "optional": true }, @@ -54804,6 +55755,9 @@ "@angular/ssr": { "optional": true }, + "karma": { + "optional": true + }, "less": { "optional": true }, @@ -54815,18 +55769,38 @@ }, "tailwindcss": { "optional": true + }, + "vitest": { + "optional": true } } }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/build/node_modules/@vitejs/plugin-basic-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-2.0.0.tgz", + "integrity": "sha512-gc9Tjg8bUxBVSTzeWT3Njc0Cl3PakHFKdNfABnZWiUgbxqmHDEn7uECv3fHVylxoYgNzAcmU7ZrILz+BwSo3sA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "peerDependencies": { + "vite": "^6.0.0" + } + }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/build/node_modules/vite": { - "version": "6.0.7", - "resolved": "https://registry.npmjs.org/vite/-/vite-6.0.7.tgz", - "integrity": "sha512-RDt8r/7qx9940f8FcOIAH9PTViRrghKaK2K1jY3RaAURrEUbm9Du1mJ72G+jlhtG3WwodnfzY8ORQZbBavZEAQ==", + "version": "6.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-6.3.5.tgz", + "integrity": "sha512-cZn6NDFE7wdTpINgs++ZJ4N49W2vRp8LCKrn3Ob1kYNtOo21vfDoaV5GzBfLU4MovSAB8uNRm4jgzVQZ+mBzPQ==", "dev": true, + "license": "MIT", "dependencies": { - "esbuild": "^0.24.2", - "postcss": "^8.4.49", - "rollup": "^4.23.0" + "esbuild": "^0.25.0", + "fdir": "^6.4.4", + "picomatch": "^4.0.2", + "postcss": "^8.5.3", + "rollup": "^4.34.9", + "tinyglobby": "^0.2.13" }, "bin": { "vite": "bin/vite.js" @@ -54890,70 +55864,70 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/cdk": { - "version": "18.2.14", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.14.tgz", - "integrity": "sha512-vDyOh1lwjfVk9OqoroZAP8pf3xxKUvyl+TVR8nJxL4c5fOfUFkD7l94HaanqKSRwJcI2xiztuu92IVoHn8T33Q==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-20.0.3.tgz", + "integrity": "sha512-70KG8GpK4aV9j5hUkpDZJQ6oMgCuaCRY6JX1axPxkNtQaiK6PAmTfQLiGqF2cYhbQneeq3uGvTorAjRfvp8NPQ==", + "license": "MIT", "peer": true, "dependencies": { + "parse5": "^7.1.2", "tslib": "^2.3.0" }, - "optionalDependencies": { - "parse5": "^7.1.2" - }, "peerDependencies": { - "@angular/common": "^18.0.0 || ^19.0.0", - "@angular/core": "^18.0.0 || ^19.0.0", + "@angular/common": "^20.0.0 || ^21.0.0", + "@angular/core": "^20.0.0 || ^21.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/cli": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-19.1.4.tgz", - "integrity": "sha512-C1Z2OTLjUJIkLsay6RJ1rzY0Tdb1Mj/cBh9dZryDstuits8G0Tphe36hnLownnoHspFQfjSRtVzF4NwKiDlQRw==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-20.0.3.tgz", + "integrity": "sha512-tDYcUrxq8Y9wK6EqwJ6Gn+4IF+VpPVikmpuqzqrUtYzqvRTqYtkyhJsAu3Ec6d6941mL2U3ZnMm3sjOxPPNkjA==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1901.4", - "@angular-devkit/core": "19.1.4", - "@angular-devkit/schematics": "19.1.4", - "@inquirer/prompts": "7.2.1", - "@listr2/prompt-adapter-inquirer": "2.0.18", - "@schematics/angular": "19.1.4", + "@angular-devkit/architect": "0.2000.3", + "@angular-devkit/core": "20.0.3", + "@angular-devkit/schematics": "20.0.3", + "@inquirer/prompts": "7.5.1", + "@listr2/prompt-adapter-inquirer": "2.0.22", + "@schematics/angular": "20.0.3", "@yarnpkg/lockfile": "1.1.0", "ini": "5.0.0", "jsonc-parser": "3.3.1", - "listr2": "8.2.5", - "npm-package-arg": "12.0.1", + "listr2": "8.3.3", + "npm-package-arg": "12.0.2", "npm-pick-manifest": "10.0.0", - "pacote": "20.0.0", + "pacote": "21.0.0", "resolve": "1.22.10", - "semver": "7.6.3", - "symbol-observable": "4.0.0", + "semver": "7.7.2", "yargs": "17.7.2" }, "bin": { "ng": "bin/ng.js" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/cli/node_modules/@angular-devkit/core": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.4.tgz", - "integrity": "sha512-IDvSSiQgaixH2RtZtIpq1+XaHeuzMiTWfDyNF9DuYcU+S8CdG1SWrc8d59tmOrM/q+IRGyFgbBhTU1un52hNHw==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.0.3.tgz", + "integrity": "sha512-XgEIbIky0pMtJSomHRaf16BT1jzJNQCm2geNZ642n3cj8fYLm4jHJX/r738kIfbHWoWXT/hlTmVgIH9TdQPicA==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", - "rxjs": "7.8.1", + "rxjs": "7.8.2", "source-map": "0.7.4" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -54971,6 +55945,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "dependencies": { @@ -54984,10 +55959,11 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/cli/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "engines": { @@ -54999,65 +55975,142 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/common": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-19.1.3.tgz", - "integrity": "sha512-r1P0W6FKrON83szIJboF8z6UNCVL4HIxyD+nhmHMMT/iJpu4kDHVugaN/+w2jYLb4oelAJK5xzkzA+1IaHpzLg==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-20.0.4.tgz", + "integrity": "sha512-fWgxe2rgSKgI36ummBYnBN4YUrmp4CHbfEG3RMeJho/vhHKguk2/o6BgL9zvnKybvbWmuaqbkHogi+y0LeJ8Ww==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "19.1.3", + "@angular/core": "20.0.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-19.1.3.tgz", - "integrity": "sha512-omX5Gyt3zlJVTUteO2YxsqYWtAIpkvs8kRYSUsLTi79V1gbGo+J1TawFuyBTrWxj4UtTGvwmDgZxiCIwMtP5KQ==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-20.0.4.tgz", + "integrity": "sha512-1bP3P8Ll/KUYMPiE6TDjkMXkqCDVgSUAUsVCgzAxz4mcMuc9PnlbhQazpWHCkCDIjGFZ5XIAsS49V7tfaTbLDw==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" - }, - "peerDependencies": { - "@angular/core": "19.1.3" - }, - "peerDependenciesMeta": { - "@angular/core": { - "optional": true - } + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-19.1.3.tgz", - "integrity": "sha512-nDBvZenQECcr9CClmTp3iJNilRQ6oDKFgBkhlWffEFBx0Z6kBA36MXKKLuCkf31D+NGmt5VJlAkl8Ax8BJ9qJw==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-20.0.4.tgz", + "integrity": "sha512-2FP1WMRexAMcDPNE3YO3zB++sCgND9O/qJC5rgKbAebpbmOrCDMUBRlftkwiLT+UhTM9PjhTtAGtK7C+2iwx1g==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/core": "7.26.0", + "@babel/core": "7.27.4", "@jridgewell/sourcemap-codec": "^1.4.14", "chokidar": "^4.0.0", "convert-source-map": "^1.5.1", "reflect-metadata": "^0.2.0", "semver": "^7.0.0", "tslib": "^2.3.0", - "yargs": "^17.2.1" + "yargs": "^18.0.0" }, "bin": { "ng-xi18n": "bundles/src/bin/ng_xi18n.js", - "ngc": "bundles/src/bin/ngc.js", - "ngcc": "bundles/ngcc/index.js" + "ngc": "bundles/src/bin/ngc.js" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "19.1.3", - "typescript": ">=5.5 <5.8" + "@angular/compiler": "20.0.4", + "typescript": ">=5.8 <5.9" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/@babel/core": { + "version": "7.27.4", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.4.tgz", + "integrity": "sha512-bXYxrXFubeYdvB0NhD/NBB3Qi6aZeV20GOWVI47t2dkecCEoneR4NPVcb7abpXDEvejgrUfFtG6vG/zxAKmg+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.3", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.4", + "@babel/parser": "^7.27.4", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.27.4", + "@babel/types": "^7.27.3", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/@babel/generator": { + "version": "7.27.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.5.tgz", + "integrity": "sha512-ZGhA37l0e/g2s1Cnzdix0O3aLYm66eF8aufiVteOgnwxgnRP8GoyMj7VWsgWnQbVKXyge7hqrFh2K2TQM6t1Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.27.5", + "@babel/types": "^7.27.3", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/ansi-styles": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", + "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/chokidar": { @@ -55065,6 +56118,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "dependencies": { "readdirp": "^4.0.1" }, @@ -55075,11 +56129,27 @@ "url": "https://paulmillr.com/funding/" } }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/cliui": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-9.0.1.tgz", + "integrity": "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0", + "wrap-ansi": "^9.0.0" + }, + "engines": { + "node": ">=20" + } + }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 14.18.0" }, @@ -55088,69 +56158,161 @@ "url": "https://paulmillr.com/funding/" } }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/wrap-ansi": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", + "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^6.2.1", + "string-width": "^7.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/yargs": { + "version": "18.0.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-18.0.0.tgz", + "integrity": "sha512-4UEqdc2RYGHZc7Doyqkrqiln3p9X2DZVxaGbwhn2pi7MrRagKaOcIKe8L3OxYcbhXLgLFUS3zAYuQjKBQgmuNg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^9.0.1", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "string-width": "^7.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^22.0.0" + }, + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/compiler-cli/node_modules/yargs-parser": { + "version": "22.0.0", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", + "integrity": "sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^20.19.0 || ^22.12.0 || >=23" + } + }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/core": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-19.1.3.tgz", - "integrity": "sha512-Hh1eHvi+y+gsTRODiEEEWnRj5zqv9WNoou1KmQ1mv1NTOf0Pv61Hg9P2rBWDr0mPIXFSzqUKjyzW30BgdQ+AEA==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-20.0.4.tgz", + "integrity": "sha512-JhSl3B6CrJ9kegLffgWVFGF4D4bWLV/9r8R0+h78vU+ppdPFPWDha7WnirF31cPIg3pBzy6wn103Kcy9Ri5M5w==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { + "@angular/compiler": "20.0.4", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0" + }, + "peerDependenciesMeta": { + "@angular/compiler": { + "optional": true + }, + "zone.js": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/forms": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-19.1.3.tgz", - "integrity": "sha512-M6eEJBysJm9zSUhm8ggljZCsgHLccZl70P34tyddb8erh9it2uoOXW0aVaZgDt1UAiF5a1EzjdVdN4TZTT/OGA==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-20.0.4.tgz", + "integrity": "sha512-bFTMgJSHiLr80ELymRykZW6o5QroDlk+g5AFFiY9yxM8I0DV5YpCNBefv8GiuWubE+Lw6LkQ/HMYeXYJMTue3A==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "19.1.3", - "@angular/core": "19.1.3", - "@angular/platform-browser": "19.1.3", + "@angular/common": "20.0.4", + "@angular/core": "20.0.4", + "@angular/platform-browser": "20.0.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/material": { - "version": "18.2.14", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.14.tgz", - "integrity": "sha512-28pxzJP49Mymt664WnCtPkKeg7kXUsQKTKGf/Kl95rNTEdTJLbnlcc8wV0rT0yQNR7kXgpfBnG7h0ETLv/iu5Q==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-20.0.3.tgz", + "integrity": "sha512-kd5Mi6gVxcjDs1nfm8GG2rId59SXWQjkiBMqrYuhy2Trpb+zG0vrLClrpoe3JdWqoX4GJagxGwl3VRDBIoP/cw==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { - "@angular/animations": "^18.0.0 || ^19.0.0", - "@angular/cdk": "18.2.14", - "@angular/common": "^18.0.0 || ^19.0.0", - "@angular/core": "^18.0.0 || ^19.0.0", - "@angular/forms": "^18.0.0 || ^19.0.0", - "@angular/platform-browser": "^18.0.0 || ^19.0.0", + "@angular/cdk": "20.0.3", + "@angular/common": "^20.0.0 || ^21.0.0", + "@angular/core": "^20.0.0 || ^21.0.0", + "@angular/forms": "^20.0.0 || ^21.0.0", + "@angular/platform-browser": "^20.0.0 || ^21.0.0", "rxjs": "^6.5.3 || ^7.4.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/platform-browser": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-19.1.3.tgz", - "integrity": "sha512-bLgnM2hRyzUdoWRoUhe+IMenlr74EvrgwyG7anJ27bjg5PcvhQPXrGqU0hri5yPDb9SHVJZminr7OjNCN8QJkQ==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-20.0.4.tgz", + "integrity": "sha512-hMJYvtZlNPh4Tt6JrnK+vmBmHWok04EkuJwyPcPhlle1u6/LihuCj4suELLqCanX9EzyNgvyKnws0i6JE/qh8Q==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "19.1.3", - "@angular/common": "19.1.3", - "@angular/core": "19.1.3" + "@angular/animations": "20.0.4", + "@angular/common": "20.0.4", + "@angular/core": "20.0.4" }, "peerDependenciesMeta": { "@angular/animations": { @@ -55159,55 +56321,58 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/platform-browser-dynamic": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-19.1.3.tgz", - "integrity": "sha512-rfsHu/+wB8YLPjsHKd/Go0SI8zP2gjMkebUHM9SbvVLXEAkxFubcF2htVKbKu8eTncfEJEXD6+3gRAjh5SLrKw==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-20.0.4.tgz", + "integrity": "sha512-MTjnSd/nuurpBT5FosgPSGsuH5xF9czmZOSvjBRPKDwAKBCBxISYx/Qb7ktqxI8Fp2ER2wbyxrypwcZHpDyysg==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "19.1.3", - "@angular/compiler": "19.1.3", - "@angular/core": "19.1.3", - "@angular/platform-browser": "19.1.3" + "@angular/common": "20.0.4", + "@angular/compiler": "20.0.4", + "@angular/core": "20.0.4", + "@angular/platform-browser": "20.0.4" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@angular/router": { - "version": "19.1.3", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.1.3.tgz", - "integrity": "sha512-DJ9BgvtxJV6xohaPQXPdBsFCZoQIEq2OPDyKcoW4L0ST4kIIFpHyI6wJ+AlPnLkhSwmOOoHciH0oxZ2xPVxmiQ==", + "version": "20.0.4", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-20.0.4.tgz", + "integrity": "sha512-t02ukwKh+YDZutR09ZYJVLaC+OPyDxu6ll7A2MFK0BNLPpD9oQc0lDwJZSrqfAhlXU0arWUjmwkNvFdh21/Z5Q==", + "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "19.1.3", - "@angular/core": "19.1.3", - "@angular/platform-browser": "19.1.3", + "@angular/common": "20.0.4", + "@angular/core": "20.0.4", + "@angular/platform-browser": "20.0.4", "rxjs": "^6.5.3 || ^7.4.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/core": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.26.0.tgz", - "integrity": "sha512-i1SLeK+DzNnQ3LL/CswPCa/E5u4lh1k6IAEphON8F+cXt0t9euTshDru0q7/IqMa1PMPz5RnHuHscF8/ZJsStg==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.27.1.tgz", + "integrity": "sha512-IaaGWsQqfsQWVLqMn9OB92MNN7zukfVA4s7KKAI0KfrrDsZ0yhi5uV4baBuLuN7n3vsZpwP8asPPcVwApxvjBQ==", "dev": true, + "license": "MIT", "dependencies": { "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.26.0", - "@babel/generator": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-module-transforms": "^7.26.0", - "@babel/helpers": "^7.26.0", - "@babel/parser": "^7.26.0", - "@babel/template": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@babel/types": "^7.26.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.27.1", + "@babel/helper-compilation-targets": "^7.27.1", + "@babel/helper-module-transforms": "^7.27.1", + "@babel/helpers": "^7.27.1", + "@babel/parser": "^7.27.1", + "@babel/template": "^7.27.1", + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -55226,25 +56391,28 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true + "dev": true, + "license": "MIT" }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/core/node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", "dev": true, + "license": "ISC", "bin": { "semver": "bin/semver.js" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/generator": { - "version": "7.26.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.26.3.tgz", - "integrity": "sha512-6FF/urZvD0sTeO7k6/B15pMLC4CHUv1426lzr3N01aHJTl046uCAh9LXW/fzeXXjPNCJ6iABW5XaWOsIZB93aQ==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.27.1.tgz", + "integrity": "sha512-UnJfnIpc/+JO0/+KRVQNGU+y5taA5vCbwN8+azkX6beii/ZF+enZJSOKo11ZSzGJjlNfJHfQtmQT8H+9TXPG2w==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/parser": "^7.26.3", - "@babel/types": "^7.26.3", + "@babel/parser": "^7.27.1", + "@babel/types": "^7.27.1", "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.25", "jsesc": "^3.0.2" @@ -55254,38 +56422,24 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/helper-annotate-as-pure": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.25.9.tgz", - "integrity": "sha512-gv7320KBUFJz1RnylIg5WWYPRXKZ884AGkYpgpWW02TH66Dl+HaC1t1CKd0z3R4b6hdYEcmrNZHUmfCP+1u3/g==", + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.1.tgz", + "integrity": "sha512-WnuuDILl9oOBbKnb4L+DyODx7iC47XfzmNCpTttFsSp6hTG7XZxu60+4IO+2/hPfcGOoKbFiwoI/+zwARbNQow==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/types": "^7.25.9" + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.3.tgz", - "integrity": "sha512-HK7Bi+Hj6H+VTHA3ZvBis7V/6hu9QuTrnMXNybfUf2iiuU/N97I8VjB+KbhFF8Rld/Lx5MzoCwPCpPjfK+n8Cg==", - "dev": true, - "dependencies": { - "@babel/helper-compilation-targets": "^7.22.6", - "@babel/helper-plugin-utils": "^7.22.5", - "debug": "^4.1.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.14.2" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/helper-split-export-declaration": { "version": "7.24.7", "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.7.tgz", "integrity": "sha512-oy5V7pD+UvfkEATUKvIjvIAH/xCzfsFVw7ygW2SI6NClZzquT+mwdTfgfdbUiceh6iQO0CHtCPsyze/MZ2YbAA==", "dev": true, + "license": "MIT", "dependencies": { "@babel/types": "^7.24.7" }, @@ -55293,199 +56447,30 @@ "node": ">=6.9.0" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "dev": true, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.25.9.tgz", - "integrity": "sha512-NT7Ejn7Z/LjUH0Gv5KsBCxh7BH3fbLTV0ptHvpeMvrt3cPThHfJfst9Wrb7S8EvJ7vRTFI7z+VAvFVEQn/m5zQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-remap-async-to-generator": "^7.25.9" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/plugin-transform-runtime": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.25.9.tgz", - "integrity": "sha512-nZp7GlEl+yULJrClz0SwHPqir3lc0zsPrDHQUcxGspSL7AKrexNSEfTbfqnDNJUO13bgKyfuOLMF8Xqtu8j3YQ==", - "dev": true, - "dependencies": { - "@babel/helper-module-imports": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/preset-env": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.26.0.tgz", - "integrity": "sha512-H84Fxq0CQJNdPFT2DrfnylZ3cf5K43rGfWK4LJGPpjKHiZlk0/RzwEus3PDDZZg+/Er7lCA03MVacueUuXdzfw==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.26.0", - "@babel/helper-compilation-targets": "^7.25.9", - "@babel/helper-plugin-utils": "^7.25.9", - "@babel/helper-validator-option": "^7.25.9", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.25.9", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.25.9", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.25.9", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.25.9", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.25.9", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.26.0", - "@babel/plugin-syntax-import-attributes": "^7.26.0", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.25.9", - "@babel/plugin-transform-async-generator-functions": "^7.25.9", - "@babel/plugin-transform-async-to-generator": "^7.25.9", - "@babel/plugin-transform-block-scoped-functions": "^7.25.9", - "@babel/plugin-transform-block-scoping": "^7.25.9", - "@babel/plugin-transform-class-properties": "^7.25.9", - "@babel/plugin-transform-class-static-block": "^7.26.0", - "@babel/plugin-transform-classes": "^7.25.9", - "@babel/plugin-transform-computed-properties": "^7.25.9", - "@babel/plugin-transform-destructuring": "^7.25.9", - "@babel/plugin-transform-dotall-regex": "^7.25.9", - "@babel/plugin-transform-duplicate-keys": "^7.25.9", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-dynamic-import": "^7.25.9", - "@babel/plugin-transform-exponentiation-operator": "^7.25.9", - "@babel/plugin-transform-export-namespace-from": "^7.25.9", - "@babel/plugin-transform-for-of": "^7.25.9", - "@babel/plugin-transform-function-name": "^7.25.9", - "@babel/plugin-transform-json-strings": "^7.25.9", - "@babel/plugin-transform-literals": "^7.25.9", - "@babel/plugin-transform-logical-assignment-operators": "^7.25.9", - "@babel/plugin-transform-member-expression-literals": "^7.25.9", - "@babel/plugin-transform-modules-amd": "^7.25.9", - "@babel/plugin-transform-modules-commonjs": "^7.25.9", - "@babel/plugin-transform-modules-systemjs": "^7.25.9", - "@babel/plugin-transform-modules-umd": "^7.25.9", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.25.9", - "@babel/plugin-transform-new-target": "^7.25.9", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.25.9", - "@babel/plugin-transform-numeric-separator": "^7.25.9", - "@babel/plugin-transform-object-rest-spread": "^7.25.9", - "@babel/plugin-transform-object-super": "^7.25.9", - "@babel/plugin-transform-optional-catch-binding": "^7.25.9", - "@babel/plugin-transform-optional-chaining": "^7.25.9", - "@babel/plugin-transform-parameters": "^7.25.9", - "@babel/plugin-transform-private-methods": "^7.25.9", - "@babel/plugin-transform-private-property-in-object": "^7.25.9", - "@babel/plugin-transform-property-literals": "^7.25.9", - "@babel/plugin-transform-regenerator": "^7.25.9", - "@babel/plugin-transform-regexp-modifiers": "^7.26.0", - "@babel/plugin-transform-reserved-words": "^7.25.9", - "@babel/plugin-transform-shorthand-properties": "^7.25.9", - "@babel/plugin-transform-spread": "^7.25.9", - "@babel/plugin-transform-sticky-regex": "^7.25.9", - "@babel/plugin-transform-template-literals": "^7.25.9", - "@babel/plugin-transform-typeof-symbol": "^7.25.9", - "@babel/plugin-transform-unicode-escapes": "^7.25.9", - "@babel/plugin-transform-unicode-property-regex": "^7.25.9", - "@babel/plugin-transform-unicode-regex": "^7.25.9", - "@babel/plugin-transform-unicode-sets-regex": "^7.25.9", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.10", - "babel-plugin-polyfill-corejs3": "^0.10.6", - "babel-plugin-polyfill-regenerator": "^0.6.1", - "core-js-compat": "^3.38.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/runtime": { - "version": "7.26.0", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.0.tgz", - "integrity": "sha512-FDSOghenHTiToteC/QRlv2q3DhPZ/oOXTBoirfWNx1Cx3TMVcGWQtMMmQcSvb/JjpNeGzx8Pq/b4fKEJuWm1sw==", - "dev": true, - "dependencies": { - "regenerator-runtime": "^0.14.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@babel/template": { - "version": "7.25.9", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.25.9.tgz", - "integrity": "sha512-9DGttpmPvIxBb/2uwpVo3dqJ+O6RooAFOS+lB+xDqoE2PVCE8nfoHMdZLpfCQRLwvohzXISPZcgxt80xLfsuwg==", + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", "dev": true, + "license": "MIT", "dependencies": { - "@babel/code-frame": "^7.25.9", - "@babel/parser": "^7.25.9", - "@babel/types": "^7.25.9" + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" }, "engines": { "node": ">=6.9.0" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@discoveryjs/json-ext": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.6.3.tgz", - "integrity": "sha512-4B4OijXeVNOPZlYA2oEwWOTkzyltLao+xbotHQeqN++Rv27Y6s818+n2Qkp8q+Fxhn0t/5lA5X1Mxktud8eayQ==", - "dev": true, - "engines": { - "node": ">=14.17.0" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.5.tgz", + "integrity": "sha512-AdJKSPeEHgi7/ZhuIPtcQKr5RQdo6OO2IL87JkianiMYMPbCtot9fxPbrMiBADOWWm3T2si9stAiVsGbTQFkbA==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -55495,13 +56480,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.5.tgz", + "integrity": "sha512-VGzGhj4lJO+TVGV1v8ntCZWJktV7SGCs3Pn1GRWI1SBFtRALoomm8k5E9Pmwg3HOAal2VDc2F9+PM/rEY6oIDg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -55511,13 +56497,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.5.tgz", + "integrity": "sha512-D2GyJT1kjvO//drbRT3Hib9XPwQeWd9vZoBJn+bu/lVsOZ13cqNdDeqIF/xQ5/VmWvMduP6AmXvylO/PIc2isw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "android" @@ -55527,13 +56514,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.5.tgz", + "integrity": "sha512-GtaBgammVvdF7aPIgH2jxMDdivezgFu6iKpmT+48+F8Hhg5J/sfnDieg0aeG/jfSvkYQU2/pceFPDKlqZzwnfQ==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -55543,13 +56531,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.5.tgz", + "integrity": "sha512-1iT4FVL0dJ76/q1wd7XDsXrSW+oLoquptvh4CLR4kITDtqi2e/xwXwdCVH8hVHU43wgJdsq7Gxuzcs6Iq/7bxQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "darwin" @@ -55559,13 +56548,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.5.tgz", + "integrity": "sha512-nk4tGP3JThz4La38Uy/gzyXtpkPW8zSAmoUhK9xKKXdBCzKODMc2adkB2+8om9BDYugz+uGV7sLmpTYzvmz6Sw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -55575,13 +56565,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.5.tgz", + "integrity": "sha512-PrikaNjiXdR2laW6OIjlbeuCPrPaAl0IwPIaRv+SMV8CiM8i2LqVUHFC1+8eORgWyY7yhQY+2U2fA55mBzReaw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "freebsd" @@ -55591,13 +56582,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.5.tgz", + "integrity": "sha512-cPzojwW2okgh7ZlRpcBEtsX7WBuqbLrNXqLU89GxWbNt6uIg78ET82qifUy3W6OVww6ZWobWub5oqZOVtwolfw==", "cpu": [ "arm" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -55607,13 +56599,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.5.tgz", + "integrity": "sha512-Z9kfb1v6ZlGbWj8EJk9T6czVEjjq2ntSYLY2cw6pAZl4oKtfgQuS4HOq41M/BcoLPzrUbNd+R4BXFyH//nHxVg==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -55623,13 +56616,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.5.tgz", + "integrity": "sha512-sQ7l00M8bSv36GLV95BVAdhJ2QsIbCuCjh/uYrWiMQSUuV+LpXwIqhgJDcvMTj+VsQmqAHL2yYaasENvJ7CDKA==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -55639,13 +56633,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.5.tgz", + "integrity": "sha512-0ur7ae16hDUC4OL5iEnDb0tZHDxYmuQyhKhsPBV8f99f6Z9KQM02g33f93rNH5A30agMS46u2HP6qTdEt6Q1kg==", "cpu": [ "loong64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -55655,13 +56650,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.5.tgz", + "integrity": "sha512-kB/66P1OsHO5zLz0i6X0RxlQ+3cu0mkxS3TKFvkb5lin6uwZ/ttOkP3Z8lfR9mJOBk14ZwZ9182SIIWFGNmqmg==", "cpu": [ "mips64el" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -55671,13 +56667,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.5.tgz", + "integrity": "sha512-UZCmJ7r9X2fe2D6jBmkLBMQetXPXIsZjQJCjgwpVDz+YMcS6oFR27alkgGv3Oqkv07bxdvw7fyB71/olceJhkQ==", "cpu": [ "ppc64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -55687,13 +56684,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.5.tgz", + "integrity": "sha512-kTxwu4mLyeOlsVIFPfQo+fQJAV9mh24xL+y+Bm6ej067sYANjyEw1dNHmvoqxJUCMnkBdKpvOn0Ahql6+4VyeA==", "cpu": [ "riscv64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -55703,13 +56701,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.5.tgz", + "integrity": "sha512-K2dSKTKfmdh78uJ3NcWFiqyRrimfdinS5ErLSn3vluHNeHVnBAFWC8a4X5N+7FgVE1EjXS1QDZbpqZBjfrqMTQ==", "cpu": [ "s390x" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -55719,13 +56718,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.5.tgz", + "integrity": "sha512-uhj8N2obKTE6pSZ+aMUbqq+1nXxNjZIIjCjGLfsWvVpy7gKCOL6rsY1MhRh9zLtUtAI7vpgLMK6DxjO8Qm9lJw==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "linux" @@ -55735,13 +56735,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.5.tgz", + "integrity": "sha512-WOb5fKrvVTRMfWFNCroYWWklbnXH0Q5rZppjq0vQIdlsQKuw6mdSihwSo4RV/YdQ5UCKKvBy7/0ZZYLBZKIbwQ==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "netbsd" @@ -55751,13 +56752,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.5.tgz", + "integrity": "sha512-G4hE405ErTWraiZ8UiSoesH8DaCsMm0Cay4fsFWOOUcz8b8rC6uCvnagr+gnioEjWn0wC+o1/TAHt+It+MpIMg==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "openbsd" @@ -55767,13 +56769,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.5.tgz", + "integrity": "sha512-l+azKShMy7FxzY0Rj4RCt5VD/q8mG/e+mDivgspo+yL8zW7qEwctQ6YqKX34DTEleFAvCIUviCFX1SDZRSyMQA==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "sunos" @@ -55783,13 +56786,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.5.tgz", + "integrity": "sha512-O2S7SNZzdcFG7eFKgvwUEZ2VG9D/sn/eIiz8XRZ1Q/DO5a3s76Xv0mdBzVM5j5R639lXQmPmSo0iRpHqUUrsxw==", "cpu": [ "arm64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -55799,13 +56803,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.5.tgz", + "integrity": "sha512-onOJ02pqs9h1iMJ1PQphR+VZv8qBMQ77Klcsqv9CNW2w6yLqoURLcgERAIurY6QE63bbLuqgP9ATqajFLK5AMQ==", "cpu": [ "ia32" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -55815,13 +56820,14 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.5.tgz", + "integrity": "sha512-TXv6YnJ8ZMVdX+SXWVBo/0p8LTcrUYngpWjvm91TMjjBQii7Oz11Lw5lbDV5Y0TzuhSJHwiH4hEtC1I42mMS0g==", "cpu": [ "x64" ], "dev": true, + "license": "MIT", "optional": true, "os": [ "win32" @@ -55831,14 +56837,15 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@inquirer/checkbox": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.0.6.tgz", - "integrity": "sha512-PgP35JfmGjHU0LSXOyRew0zHuA9N6OJwOlos1fZ20b7j8ISeAdib3L+n0jIxBtX958UeEpte6xhG/gxJ5iUqMw==", + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-4.1.8.tgz", + "integrity": "sha512-d/QAsnwuHX2OPolxvYcgSj7A9DO9H6gVOy2DvBTx+P2LH2iRTo/RSGV3iwCzW024nP9hw98KIuDmdyhZQj1UQg==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -55847,52 +56854,72 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@inquirer/confirm": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.1.tgz", - "integrity": "sha512-vVLSbGci+IKQvDOtzpPTCOiEJCNidHcAq9JYVoWTW0svb5FiwSLotkM+JXNXejfjnzVYV9n0DTBythl9+XgTxg==", + "version": "5.1.10", + "resolved": "https://registry.npmjs.org/@inquirer/confirm/-/confirm-5.1.10.tgz", + "integrity": "sha512-FxbQ9giWxUWKUk2O5XZ6PduVnH2CZ/fmMKMBkH71MHJvWr7WL5AHKevhzF1L5uYWB2P548o1RzVxrNd3dpmk6g==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.2", - "@inquirer/type": "^3.0.2" + "@inquirer/core": "^10.1.11", + "@inquirer/type": "^3.0.6" }, "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@inquirer/core": { - "version": "10.1.4", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.4.tgz", - "integrity": "sha512-5y4/PUJVnRb4bwWY67KLdebWOhOc7xj5IP2J80oWXa64mVag24rwQ1VAdnj7/eDY/odhguW0zQ1Mp1pj6fO/2w==", + "version": "10.1.13", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-10.1.13.tgz", + "integrity": "sha512-1viSxebkYN2nJULlzCxES6G9/stgHSepZ9LqqfdIGPHj5OHhiBUXVS0a6R0bEC2A+VL4D9w6QB66ebCr6HGllA==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "cli-width": "^4.1.0", "mute-stream": "^2.0.0", "signal-exit": "^4.1.0", - "strip-ansi": "^6.0.1", "wrap-ansi": "^6.2.0", "yoctocolors-cjs": "^2.1.2" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@inquirer/editor": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.3.tgz", - "integrity": "sha512-S9KnIOJuTZpb9upeRSBBhoDZv7aSV3pG9TECrBj0f+ZsFwccz886hzKBrChGrXMJwd4NKY+pOA9Vy72uqnd6Eg==", + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-4.2.13.tgz", + "integrity": "sha512-WbicD9SUQt/K8O5Vyk9iC2ojq5RHoCLK6itpp2fHsWe44VxxcA9z3GTWlvjSTGmMQpZr+lbVmrxdHcumJoLbMA==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", "external-editor": "^3.1.0" }, "engines": { @@ -55900,16 +56927,22 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@inquirer/expand": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.6.tgz", - "integrity": "sha512-TRTfi1mv1GeIZGyi9PQmvAaH65ZlG4/FACq6wSzs7Vvf1z5dnNWsAAXBjWMHt76l+1hUY8teIqJFrWBk5N6gsg==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-4.0.15.tgz", + "integrity": "sha512-4Y+pbr/U9Qcvf+N/goHzPEXiHH8680lM3Dr3Y9h9FFw4gHS+zVpbj8LfbKWIb/jayIB4aSO4pWiBTrBYWkvi5A==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -55917,32 +56950,44 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@inquirer/input": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.3.tgz", - "integrity": "sha512-zeo++6f7hxaEe7OjtMzdGZPHiawsfmCZxWB9X1NpmYgbeoyerIbWemvlBxxl+sQIlHC0WuSAG19ibMq3gbhaqQ==", + "version": "4.1.12", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-4.1.12.tgz", + "integrity": "sha512-xJ6PFZpDjC+tC1P8ImGprgcsrzQRsUh9aH3IZixm1lAZFK49UGHxM3ltFfuInN2kPYNfyoPRh+tU4ftsjPLKqQ==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2" + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7" }, "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@inquirer/password": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.6.tgz", - "integrity": "sha512-QLF0HmMpHZPPMp10WGXh6F+ZPvzWE7LX6rNoccdktv/Rov0B+0f+eyXkAcgqy5cH9V+WSpbLxu2lo3ysEVK91w==", + "version": "4.0.15", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-4.0.15.tgz", + "integrity": "sha512-75CT2p43DGEnfGTaqFpbDC2p2EEMrq0S+IRrf9iJvYreMy5mAWj087+mdKyLHapUEPLjN10mNvABpGbk8Wdraw==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2" }, "engines": { @@ -55950,40 +56995,52 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@inquirer/prompts": { - "version": "7.2.1", - "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.2.1.tgz", - "integrity": "sha512-v2JSGri6/HXSfoGIwuKEn8sNCQK6nsB2BNpy2lSX6QH9bsECrMv93QHnj5+f+1ZWpF/VNioIV2B/PDox8EvGuQ==", + "version": "7.5.1", + "resolved": "https://registry.npmjs.org/@inquirer/prompts/-/prompts-7.5.1.tgz", + "integrity": "sha512-5AOrZPf2/GxZ+SDRZ5WFplCA2TAQgK3OYrXCYmJL5NaTu4ECcoWFlfUZuw7Es++6Njv7iu/8vpYJhuzxUH76Vg==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/checkbox": "^4.0.4", - "@inquirer/confirm": "^5.1.1", - "@inquirer/editor": "^4.2.1", - "@inquirer/expand": "^4.0.4", - "@inquirer/input": "^4.1.1", - "@inquirer/number": "^3.0.4", - "@inquirer/password": "^4.0.4", - "@inquirer/rawlist": "^4.0.4", - "@inquirer/search": "^3.0.4", - "@inquirer/select": "^4.0.4" + "@inquirer/checkbox": "^4.1.6", + "@inquirer/confirm": "^5.1.10", + "@inquirer/editor": "^4.2.11", + "@inquirer/expand": "^4.0.13", + "@inquirer/input": "^4.1.10", + "@inquirer/number": "^3.0.13", + "@inquirer/password": "^4.0.13", + "@inquirer/rawlist": "^4.1.1", + "@inquirer/search": "^3.0.13", + "@inquirer/select": "^4.2.1" }, "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@inquirer/rawlist": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.0.6.tgz", - "integrity": "sha512-QoE4s1SsIPx27FO4L1b1mUjVcoHm1pWE/oCmm4z/Hl+V1Aw5IXl8FYYzGmfXaBT0l/sWr49XmNSiq7kg3Kd/Lg==", + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-4.1.3.tgz", + "integrity": "sha512-7XrV//6kwYumNDSsvJIPeAqa8+p7GJh7H5kRuxirct2cgOcSWwwNGoXDRgpNFbY/MG2vQ4ccIWCi8+IXXyFMZA==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.13", + "@inquirer/type": "^3.0.7", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -55991,17 +57048,23 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@inquirer/select": { - "version": "4.0.6", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.0.6.tgz", - "integrity": "sha512-yANzIiNZ8fhMm4NORm+a74+KFYHmf7BZphSOBovIzYPVLquseTGEkU5l2UTnBOf5k0VLmTgPighNDLE9QtbViQ==", + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-4.2.3.tgz", + "integrity": "sha512-OAGhXU0Cvh0PhLz9xTF/kx6g6x+sP+PcyTiLvCrewI99P3BBeexD+VbuwkNDvqGkk3y2h5ZiWLeRP7BFlhkUDg==", "dev": true, + "license": "MIT", "dependencies": { - "@inquirer/core": "^10.1.4", - "@inquirer/figures": "^1.0.9", - "@inquirer/type": "^3.0.2", + "@inquirer/core": "^10.1.13", + "@inquirer/figures": "^1.0.12", + "@inquirer/type": "^3.0.7", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -56010,18 +57073,29 @@ }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@inquirer/type": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.2.tgz", - "integrity": "sha512-ZhQ4TvhwHZF+lGhQ2O/rsjo80XoZR5/5qhOY3t6FJuX5XBg5Be8YzYTvaUGJnc12AUGI2nr4QSUE4PhKSigx7g==", + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-3.0.7.tgz", + "integrity": "sha512-PfunHQcjwnju84L+ycmcMKB/pTPIngjUJvfnRhKY6FKPuYXlM4aQCb/nIdTFR6BEhMjFvngzvng/vBAJMZpLSA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, "peerDependencies": { "@types/node": ">=18" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@jridgewell/gen-mapping": { @@ -56029,6 +57103,7 @@ "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.8.tgz", "integrity": "sha512-imAbBGkb+ebQyxKgzv5Hu2nmROxoDOXHh80evxdoXNOrvAnVx7zimzc1Oo5h9RlfV4vPXaE2iM5pOFbvOCClWA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/set-array": "^1.2.1", "@jridgewell/sourcemap-codec": "^1.4.10", @@ -56038,27 +57113,12 @@ "node": ">=6.0.0" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@ngtools/webpack": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-19.1.4.tgz", - "integrity": "sha512-ZmUlbVqu/pz8abxVxNCKgKeY5g2MX1NsKxhM8rRV5tVV/MaAtSYNHgmFSYcKWA178v7k6BUuhnoNNxl5qqc1kw==", - "dev": true, - "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", - "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", - "yarn": ">= 1.13.0" - }, - "peerDependencies": { - "@angular/compiler-cli": "^19.0.0", - "typescript": ">=5.5 <5.8", - "webpack": "^5.54.0" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@npmcli/fs": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/@npmcli/fs/-/fs-4.0.0.tgz", "integrity": "sha512-/xGlezI6xfGO9NwuJlnwz/K14qD1kCSAGtacBHnGzeAIuJGazcp45KP5NuyARXoKb7cwulAGWVsbeSxdG/cb0Q==", "dev": true, + "license": "ISC", "dependencies": { "semver": "^7.3.5" }, @@ -56067,17 +57127,17 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@npmcli/git": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.1.tgz", - "integrity": "sha512-BBWMMxeQzalmKadyimwb2/VVQyJB01PH0HhVSNLHNBDZN/M/h/02P6f8fxedIiFhpMj11SO9Ep5tKTBE7zL2nw==", + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/@npmcli/git/-/git-6.0.3.tgz", + "integrity": "sha512-GUYESQlxZRAdhs3UhbB6pVRNUELQOHXwK9ruDkwmCv2aZ5y0SApQzUJCg02p3A7Ue2J5hxvlk1YI53c00NmRyQ==", "dev": true, + "license": "ISC", "dependencies": { "@npmcli/promise-spawn": "^8.0.0", "ini": "^5.0.0", "lru-cache": "^10.0.1", "npm-pick-manifest": "^10.0.0", "proc-log": "^5.0.0", - "promise-inflight": "^1.0.1", "promise-retry": "^2.0.1", "semver": "^7.3.5", "which": "^5.0.0" @@ -56091,6 +57151,7 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } @@ -56100,6 +57161,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -56115,6 +57177,7 @@ "resolved": "https://registry.npmjs.org/@npmcli/installed-package-contents/-/installed-package-contents-3.0.0.tgz", "integrity": "sha512-fkxoPuFGvxyrH+OQzyTkX2LUEamrF4jZSmxjAtPPHHGO0dqsQ8tTKjnIS8SAnPHdk2I03BDtSMR5K/4loKg79Q==", "dev": true, + "license": "ISC", "dependencies": { "npm-bundled": "^4.0.0", "npm-normalize-package-bin": "^4.0.0" @@ -56131,6 +57194,7 @@ "resolved": "https://registry.npmjs.org/@npmcli/node-gyp/-/node-gyp-4.0.0.tgz", "integrity": "sha512-+t5DZ6mO/QFh78PByMq1fGSAub/agLJZDRfJRMeOSNCt8s9YVlTjmGpIPwPhvXTGUIJk+WszlT0rQa1W33yzNA==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -56140,6 +57204,7 @@ "resolved": "https://registry.npmjs.org/@npmcli/promise-spawn/-/promise-spawn-8.0.2.tgz", "integrity": "sha512-/bNJhjc+o6qL+Dwz/bqfTQClkEO5nTQ1ZEcdCkAQjhkZMHIh22LPG7fNh1enJP1NKWDqYiiABnjFCY7E0zHYtQ==", "dev": true, + "license": "ISC", "dependencies": { "which": "^5.0.0" }, @@ -56152,6 +57217,7 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } @@ -56161,6 +57227,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -56172,10 +57239,11 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@npmcli/run-script": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.0.2.tgz", - "integrity": "sha512-cJXiUlycdizQwvqE1iaAb4VRUM3RX09/8q46zjvy+ct9GhfZRWd7jXYVc1tn/CfRlGPVkX/u4sstRlepsm7hfw==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@npmcli/run-script/-/run-script-9.1.0.tgz", + "integrity": "sha512-aoNSbxtkePXUlbZB+anS1LqsJdctG5n3UVhfU47+CDdwMi6uNTBMF9gPcQRnqghQd2FGzcwwIFBruFMxjhBewg==", "dev": true, + "license": "ISC", "dependencies": { "@npmcli/node-gyp": "^4.0.0", "@npmcli/package-json": "^6.0.0", @@ -56193,6 +57261,7 @@ "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } @@ -56202,6 +57271,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -56212,284 +57282,39 @@ "node": "^18.17.0 || >=20.5.0" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.30.1.tgz", - "integrity": "sha512-pSWY+EVt3rJ9fQ3IqlrEUtXh3cGqGtPDH1FQlNZehO2yYxCHEX1SPsz1M//NXwYfbTlcKr9WObLnJX9FsS9K1Q==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-android-arm64": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.30.1.tgz", - "integrity": "sha512-/NA2qXxE3D/BRjOJM8wQblmArQq1YoBVJjrjoTSBS09jgUisq7bqxNHJ8kjCHeV21W/9WDGwJEWSN0KQ2mtD/w==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "android" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.30.1.tgz", - "integrity": "sha512-r7FQIXD7gB0WJ5mokTUgUWPl0eYIH0wnxqeSAhuIwvnnpjdVB8cRRClyKLQr7lgzjctkbp5KmswWszlwYln03Q==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-darwin-x64": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.30.1.tgz", - "integrity": "sha512-x78BavIwSH6sqfP2xeI1hd1GpHL8J4W2BXcVM/5KYKoAD3nNsfitQhvWSw+TFtQTLZ9OmlF+FEInEHyubut2OA==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "darwin" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.30.1.tgz", - "integrity": "sha512-HYTlUAjbO1z8ywxsDFWADfTRfTIIy/oUlfIDmlHYmjUP2QRDTzBuWXc9O4CXM+bo9qfiCclmHk1x4ogBjOUpUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.30.1.tgz", - "integrity": "sha512-1MEdGqogQLccphhX5myCJqeGNYTNcmTyaic9S7CG3JhwuIByJ7J05vGbZxsizQthP1xpVx7kd3o31eOogfEirw==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "freebsd" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.30.1.tgz", - "integrity": "sha512-PaMRNBSqCx7K3Wc9QZkFx5+CX27WFpAMxJNiYGAXfmMIKC7jstlr32UhTgK6T07OtqR+wYlWm9IxzennjnvdJg==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.30.1.tgz", - "integrity": "sha512-B8Rcyj9AV7ZlEFqvB5BubG5iO6ANDsRKlhIxySXcF1axXYUyqwBok+XZPgIYGBgs7LDXfWfifxhw0Ik57T0Yug==", - "cpu": [ - "arm" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.30.1.tgz", - "integrity": "sha512-hqVyueGxAj3cBKrAI4aFHLV+h0Lv5VgWZs9CUGqr1z0fZtlADVV1YPOij6AhcK5An33EXaxnDLmJdQikcn5NEw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.30.1.tgz", - "integrity": "sha512-i4Ab2vnvS1AE1PyOIGp2kXni69gU2DAUVt6FSXeIqUCPIR3ZlheMW3oP2JkukDfu3PsexYRbOiJrY+yVNSk9oA==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-linux-loongarch64-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loongarch64-gnu/-/rollup-linux-loongarch64-gnu-4.30.1.tgz", - "integrity": "sha512-fARcF5g296snX0oLGkVxPmysetwUk2zmHcca+e9ObOovBR++9ZPOhqFUM61UUZ2EYpXVPN1redgqVoBB34nTpQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-linux-powerpc64le-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-powerpc64le-gnu/-/rollup-linux-powerpc64le-gnu-4.30.1.tgz", - "integrity": "sha512-GLrZraoO3wVT4uFXh67ElpwQY0DIygxdv0BNW9Hkm3X34wu+BkqrDrkcsIapAY+N2ATEbvak0XQ9gxZtCIA5Rw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.30.1.tgz", - "integrity": "sha512-0WKLaAUUHKBtll0wvOmh6yh3S0wSU9+yas923JIChfxOaaBarmb/lBKPF0w/+jTVozFnOXJeRGZ8NvOxvk/jcw==", - "cpu": [ - "riscv64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.30.1.tgz", - "integrity": "sha512-GWFs97Ruxo5Bt+cvVTQkOJ6TIx0xJDD/bMAOXWJg8TCSTEK8RnFeOeiFTxKniTc4vMIaWvCplMAFBt9miGxgkA==", - "cpu": [ - "s390x" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.30.1.tgz", - "integrity": "sha512-UtgGb7QGgXDIO+tqqJ5oZRGHsDLO8SlpE4MhqpY9Llpzi5rJMvrK6ZGhsRCST2abZdBqIBeXW6WPD5fGK5SDwg==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.30.1.tgz", - "integrity": "sha512-V9U8Ey2UqmQsBT+xTOeMzPzwDzyXmnAoO4edZhL7INkwQcaW1Ckv3WJX3qrrp/VHaDkEWIBWhRwP47r8cdrOow==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "linux" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.30.1.tgz", - "integrity": "sha512-WabtHWiPaFF47W3PkHnjbmWawnX/aE57K47ZDT1BXTS5GgrBUEpvOzq0FI0V/UYzQJgdb8XlhVNH8/fwV8xDjw==", - "cpu": [ - "arm64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.30.1.tgz", - "integrity": "sha512-pxHAU+Zv39hLUTdQQHUVHf4P+0C47y/ZloorHpzs2SXMRqeAWmGghzAhfOlzFHHwjvgokdFAhC4V+6kC1lRRfw==", - "cpu": [ - "ia32" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.30.1.tgz", - "integrity": "sha512-D6qjsXGcvhTjv0kI4fU8tUuBDF/Ueee4SVX79VfNDXZa64TfCW1Slkb6Z7O1p7vflqZjcmOVdZlqf8gvJxc6og==", - "cpu": [ - "x64" - ], - "dev": true, - "optional": true, - "os": [ - "win32" - ] - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@schematics/angular": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-19.1.4.tgz", - "integrity": "sha512-HFf83SoXbj1K4jkYSSfCg/oXkmSGBx0zG1Lh+dE5GZFdTQmykrBY519aSdrqLVyZzKYjTGfDfSewUeO4a0GE2A==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-20.0.3.tgz", + "integrity": "sha512-oWj5UU1gR12KDxQwOUpxweaaF8PPF7t5ymTa/px/nl4YYWd9s5e1skoDNcGHHl0MPHklJzNLxP7O89BORie5vQ==", "dev": true, + "license": "MIT", "dependencies": { - "@angular-devkit/core": "19.1.4", - "@angular-devkit/schematics": "19.1.4", + "@angular-devkit/core": "20.0.3", + "@angular-devkit/schematics": "20.0.3", "jsonc-parser": "3.3.1" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@schematics/angular/node_modules/@angular-devkit/core": { - "version": "19.1.4", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-19.1.4.tgz", - "integrity": "sha512-IDvSSiQgaixH2RtZtIpq1+XaHeuzMiTWfDyNF9DuYcU+S8CdG1SWrc8d59tmOrM/q+IRGyFgbBhTU1un52hNHw==", + "version": "20.0.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-20.0.3.tgz", + "integrity": "sha512-XgEIbIky0pMtJSomHRaf16BT1jzJNQCm2geNZ642n3cj8fYLm4jHJX/r738kIfbHWoWXT/hlTmVgIH9TdQPicA==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "8.17.1", "ajv-formats": "3.0.1", "jsonc-parser": "3.3.1", "picomatch": "4.0.2", - "rxjs": "7.8.1", + "rxjs": "7.8.2", "source-map": "0.7.4" }, "engines": { - "node": "^18.19.1 || ^20.11.1 || >=22.0.0", + "node": "^20.19.0 || ^22.12.0 || >=24.0.0", "npm": "^6.11.0 || ^7.5.6 || >=8.0.0", "yarn": ">= 1.13.0" }, @@ -56507,6 +57332,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "dependencies": { @@ -56520,10 +57346,11 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@schematics/angular/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "engines": { @@ -56535,36 +57362,39 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@sigstore/bundle": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.0.0.tgz", - "integrity": "sha512-XDUYX56iMPAn/cdgh/DTJxz5RWmqKV4pwvUAEKEWJl+HzKdCd/24wUa9JYNMlDSCb7SUHAdtksxYX779Nne/Zg==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/bundle/-/bundle-3.1.0.tgz", + "integrity": "sha512-Mm1E3/CmDDCz3nDhFKTuYdB47EdRFRQMOE/EAbiG1MJW77/w1b3P7Qx7JSrVJs8PfwOLOVcKQCHErIwCTyPbag==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2" + "@sigstore/protobuf-specs": "^0.4.0" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@sigstore/protobuf-specs": { - "version": "0.3.3", - "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.3.3.tgz", - "integrity": "sha512-RpacQhBlwpBWd7KEJsRKcBQalbV28fvkxwTOJIqhIuDysMMaJW47V4OqW30iJB9uRpqOSxxEAQFdr8tTattReQ==", + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@sigstore/protobuf-specs/-/protobuf-specs-0.4.3.tgz", + "integrity": "sha512-fk2zjD9117RL9BjqEwF7fwv7Q/P9yGsMV4MUJZ/DocaQJ6+3pKr+syBq1owU5Q5qGw5CUbXzm+4yJ2JVRDQeSA==", "dev": true, + "license": "Apache-2.0", "engines": { "node": "^18.17.0 || >=20.5.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@sigstore/sign": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.0.0.tgz", - "integrity": "sha512-UjhDMQOkyDoktpXoc5YPJpJK6IooF2gayAr5LvXI4EL7O0vd58okgfRcxuaH+YTdhvb5aa1Q9f+WJ0c2sVuYIw==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@sigstore/sign/-/sign-3.1.0.tgz", + "integrity": "sha512-knzjmaOHOov1Ur7N/z4B1oPqZ0QX5geUfhrVaqVlu+hl0EAoL4o+l0MSULINcD5GCWe3Z0+YJO8ues6vFlW0Yw==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.0.0", + "@sigstore/bundle": "^3.1.0", "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "make-fetch-happen": "^14.0.1", + "@sigstore/protobuf-specs": "^0.4.0", + "make-fetch-happen": "^14.0.2", "proc-log": "^5.0.0", "promise-retry": "^2.0.1" }, @@ -56573,12 +57403,13 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/@sigstore/tuf": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.0.0.tgz", - "integrity": "sha512-9Xxy/8U5OFJu7s+OsHzI96IX/OzjF/zj0BSSaWhgJgTqtlBhQIV2xdrQI5qxLD7+CWWDepadnXAxzaZ3u9cvRw==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@sigstore/tuf/-/tuf-3.1.1.tgz", + "integrity": "sha512-eFFvlcBIoGwVkkwmTi/vEQFSva3xs5Ot3WmBcjgjVdiaoelBLQaQ/ZBfhlG0MnG0cmTYScPpk7eDdGDWUcFUmg==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/protobuf-specs": "^0.3.2", + "@sigstore/protobuf-specs": "^0.4.1", "tuf-js": "^3.0.1" }, "engines": { @@ -56590,6 +57421,7 @@ "resolved": "https://registry.npmjs.org/@tufjs/canonical-json/-/canonical-json-2.0.0.tgz", "integrity": "sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==", "dev": true, + "license": "MIT", "engines": { "node": "^16.14.0 || >=18.0.0" } @@ -56599,6 +57431,7 @@ "resolved": "https://registry.npmjs.org/@tufjs/models/-/models-3.0.1.tgz", "integrity": "sha512-UUYHISyhCU3ZgN8yaear3cGATHb3SMuKHsQ/nVbHXcmnBf+LzQ/cQfhNG+rfaSHgqGKNEm2cOCLVLELStUQ1JA==", "dev": true, + "license": "MIT", "dependencies": { "@tufjs/canonical-json": "2.0.0", "minimatch": "^9.0.5" @@ -56612,6 +57445,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -56628,29 +57462,12 @@ "integrity": "sha512-SaCZ3kM5NjOiJqMRYwHpLbTfUC2Dyk1KS3QanNFsUYPGTk70CWVK/J9ueun6zNhw/UkgV7xl8V4ZLQZNRbfnNw==", "dev": true }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@types/retry": { - "version": "0.12.2", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.2.tgz", - "integrity": "sha512-XISRgDJ2Tc5q4TRqvgJtzsRkFYNJzZrhTdtMoGVBttwzzQJkPnS3WWTFc7kuDRoPtPakl+T+OfdEUjYJj7Jbow==", - "dev": true - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/@vitejs/plugin-basic-ssl": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@vitejs/plugin-basic-ssl/-/plugin-basic-ssl-1.2.0.tgz", - "integrity": "sha512-mkQnxTkcldAzIsomk1UuLfAu9n+kpQ3JbHcpCp7d2Oo6ITtji8pHS3QToOWjhPFvNQSnhlkAjmGbhv2QvwO/7Q==", - "dev": true, - "engines": { - "node": ">=14.21.3" - }, - "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/abbrev": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.0.tgz", - "integrity": "sha512-+/kfrslGQ7TNV2ecmQwMJj/B65g5KVq1/L3SGVZ3tCYGqlzFuFCGBZJtMP99wH3NpEUyAjn0zPdPUg0D+DwrOA==", + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-3.0.1.tgz", + "integrity": "sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -56676,6 +57493,7 @@ "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", "dev": true, + "license": "MIT", "dependencies": { "ajv": "^8.0.0" }, @@ -56693,6 +57511,7 @@ "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.1.0.tgz", "integrity": "sha512-7HSX4QQb4CspciLpVFwyRe79O3xsIZDDLER21kERQ71oaPodF8jL725AgJMFAYbooIqolJoRLuM81SpeUkpkvA==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -56700,84 +57519,20 @@ "url": "https://github.com/chalk/ansi-regex?sponsor=1" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/babel-loader": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", - "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", - "dev": true, - "dependencies": { - "find-cache-dir": "^4.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.12", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.12.tgz", - "integrity": "sha512-CPWT6BwvhrTO2d8QVorhTCQw9Y43zOu7G9HigcfxvepOU6b8o3tcWad6oVgZIsZCTt42FFv97aA7ZJsbM4+8og==", - "dev": true, - "dependencies": { - "@babel/compat-data": "^7.22.6", - "@babel/helper-define-polyfill-provider": "^0.6.3", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "bin": { - "semver": "bin/semver.js" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.10.6", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.6.tgz", - "integrity": "sha512-b37+KR2i/khY5sKmWNVQAnitvquQbNdWy6lJdsr0kmquCKEEUgMKK4SboVM3HtfnZilfjr4MMQ7vY58FVWDtIA==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.2", - "core-js-compat": "^3.38.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.3.tgz", - "integrity": "sha512-LiWSbl4CRSIa5x/JAU6jZiG9eit9w6mz+yVMFwDE83LAWvt0AfGBoZ7HS/mkhrKuh2ZlzfVZYKoLjXdqw6Yt7Q==", - "dev": true, - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.3" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/brace-expansion": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", - "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", "dev": true, + "license": "MIT", "dependencies": { "balanced-match": "^1.0.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/browserslist": { - "version": "4.24.4", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.24.4.tgz", - "integrity": "sha512-KDi1Ny1gSePi1vm0q4oxSF8b4DR44GF4BbmS2YdhPLOEqd8pDviZOGH/GsmRwoWJ2+5Lr085X7naowMwKHDG1A==", + "version": "4.25.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.25.0.tgz", + "integrity": "sha512-PJ8gYKeS5e/whHBh8xrwYK+dAvEj7JXtz6uTucnMRB8OiGTsKccFekoRrjajPBHV8oOY+2tI4uxeceSimKwMFA==", "dev": true, "funding": [ { @@ -56793,11 +57548,12 @@ "url": "https://github.com/sponsors/ai" } ], + "license": "MIT", "dependencies": { - "caniuse-lite": "^1.0.30001688", - "electron-to-chromium": "^1.5.73", + "caniuse-lite": "^1.0.30001718", + "electron-to-chromium": "^1.5.160", "node-releases": "^2.0.19", - "update-browserslist-db": "^1.1.1" + "update-browserslist-db": "^1.1.3" }, "bin": { "browserslist": "cli.js" @@ -56811,6 +57567,7 @@ "resolved": "https://registry.npmjs.org/cacache/-/cacache-19.0.1.tgz", "integrity": "sha512-hdsUxulXCi5STId78vRVYEtDAjq99ICAUktLTeTYsLoTE6Z8dS0c8pWNCxwdrk9YfJeobDZc2Y186hD/5ZQgFQ==", "dev": true, + "license": "ISC", "dependencies": { "@npmcli/fs": "^4.0.0", "fs-minipass": "^3.0.0", @@ -56834,6 +57591,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", "dev": true, + "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", @@ -56854,6 +57612,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -56869,6 +57628,7 @@ "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true, + "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" }, @@ -56884,6 +57644,7 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "dev": true, + "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -56896,6 +57657,19 @@ "node": ">=18" } }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/chalk": { + "version": "5.4.1", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.4.1.tgz", + "integrity": "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -56937,6 +57711,7 @@ "resolved": "https://registry.npmjs.org/chownr/-/chownr-3.0.0.tgz", "integrity": "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } @@ -56946,6 +57721,7 @@ "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-5.0.0.tgz", "integrity": "sha512-aCj4O5wKyszjMmDT4tZj93kxyydN/K5zPWSCe6/0AV/AA1pqe5ZBIw0a2ZfPQV7lL5/yb5HsUreJ6UFAF1tEQw==", "dev": true, + "license": "MIT", "dependencies": { "restore-cursor": "^5.0.0" }, @@ -56961,6 +57737,7 @@ "resolved": "https://registry.npmjs.org/cli-truncate/-/cli-truncate-4.0.0.tgz", "integrity": "sha512-nPdaFdQ0h/GEigbPClz11D0v/ZJEwxmeVZGeMo3Z5StPtUTkA9o1lD6QwoirYiSDzbcwn2XcjwmCp68W1IS4TA==", "dev": true, + "license": "MIT", "dependencies": { "slice-ansi": "^5.0.0", "string-width": "^7.0.0" @@ -56977,6 +57754,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -56994,6 +57772,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -57009,6 +57788,7 @@ "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-4.1.0.tgz", "integrity": "sha512-ouuZd4/dm2Sw5Gmqy6bGyNNNe1qt9RpmxveLSO7KcgsTnU7RXfsw+/bukWGo1abgBiMAic068rclZsO4IWmmxQ==", "dev": true, + "license": "ISC", "engines": { "node": ">= 12" } @@ -57042,106 +57822,23 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/commander": { - "version": "13.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-13.1.0.tgz", - "integrity": "sha512-/rFeCpNJQbhSZjGVwO9RFV3xPqbnERS8MmIQzCtD/zl6gpJuV/bMLuN92oG3F7d8oDEHHRrujSXNUr8fpjntKw==", + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-14.0.0.tgz", + "integrity": "sha512-2uM9rYjPvyq39NwLRqaiLtWHyDC1FvryJDa2ATTVims5YAS4PupsEQsDvP14FqhFr0P49CYDugi59xaxJlTXRA==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "engines": { - "node": ">=18" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/copy-webpack-plugin": { - "version": "12.0.2", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-12.0.2.tgz", - "integrity": "sha512-SNwdBeHyII+rWvee/bTnAYyO8vfVdcSTud4EIb6jcZ8inLeWucJE0DnxXQBjlQ5zlteuuvooGQy3LIyGxhvlOA==", - "dev": true, - "dependencies": { - "fast-glob": "^3.3.2", - "glob-parent": "^6.0.1", - "globby": "^14.0.0", - "normalize-path": "^3.0.0", - "schema-utils": "^4.2.0", - "serialize-javascript": "^6.0.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/cosmiconfig": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.0.tgz", - "integrity": "sha512-itvL5h8RETACmOTFc4UfIyB2RfEHi71Ax6E/PivVxq9NseKbOWpeyHEOIbmAw1rs8Ak0VursQNww7lf7YtUwzg==", - "dev": true, - "dependencies": { - "env-paths": "^2.2.1", - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/css-loader": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.2.tgz", - "integrity": "sha512-6WvYYn7l/XEGN8Xu2vWFt9nVzrCn39vKyTEFf/ExEyoksJjjSZV/0/35XPlMbpnr6VGhZIUg5yJrL8tGfes/FA==", - "dev": true, - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.27.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "node": ">=20" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/debug": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", - "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.1.tgz", + "integrity": "sha512-KcKCqiftBJcZr++7ykoDIEwSa3XWowTfNPo92BYxjXiyYEVrUQh2aLyhxBCwww+heortUFxEJYcRzosstTEBYQ==", "dev": true, + "license": "MIT", "dependencies": { "ms": "^2.1.3" }, @@ -57154,23 +57851,12 @@ } } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/define-lazy-prop": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-3.0.0.tgz", - "integrity": "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg==", - "dev": true, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/dependency-graph": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/dependency-graph/-/dependency-graph-1.0.0.tgz", "integrity": "sha512-cW3gggJ28HZ/LExwxP2B++aiKxhJXMSIt9K48FOXQkm+vuG5gyatXnLsONRJdzO/7VfjDIiaOOa/bs4l464Lwg==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "engines": { @@ -57181,14 +57867,13 @@ "version": "10.4.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==", - "dev": true + "dev": true, + "license": "MIT" }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/entities": { "version": "4.5.0", "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "optional": true, - "peer": true, "engines": { "node": ">=0.12" }, @@ -57197,11 +57882,12 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", + "version": "0.25.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.5.tgz", + "integrity": "sha512-P8OtKZRv/5J5hhz0cUAdu/cLuPIKXpQl1R9pZtvmHWQvrAUVd0UNIPT4IB4W3rNOqVO0rlqHmCIbSwxh/c9yUQ==", "dev": true, "hasInstallScript": true, + "license": "MIT", "bin": { "esbuild": "bin/esbuild" }, @@ -57209,82 +57895,39 @@ "node": ">=18" }, "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/esbuild-wasm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild-wasm/-/esbuild-wasm-0.24.2.tgz", - "integrity": "sha512-03/7Z1gD+ohDnScFztvI4XddTAbKVmMEzCvvkBpQdWKEXJ+73dTyeNrmdxP1Q0zpDMFjzUJwtK4rLjqwiHbzkw==", - "dev": true, - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" + "@esbuild/aix-ppc64": "0.25.5", + "@esbuild/android-arm": "0.25.5", + "@esbuild/android-arm64": "0.25.5", + "@esbuild/android-x64": "0.25.5", + "@esbuild/darwin-arm64": "0.25.5", + "@esbuild/darwin-x64": "0.25.5", + "@esbuild/freebsd-arm64": "0.25.5", + "@esbuild/freebsd-x64": "0.25.5", + "@esbuild/linux-arm": "0.25.5", + "@esbuild/linux-arm64": "0.25.5", + "@esbuild/linux-ia32": "0.25.5", + "@esbuild/linux-loong64": "0.25.5", + "@esbuild/linux-mips64el": "0.25.5", + "@esbuild/linux-ppc64": "0.25.5", + "@esbuild/linux-riscv64": "0.25.5", + "@esbuild/linux-s390x": "0.25.5", + "@esbuild/linux-x64": "0.25.5", + "@esbuild/netbsd-arm64": "0.25.5", + "@esbuild/netbsd-x64": "0.25.5", + "@esbuild/openbsd-arm64": "0.25.5", + "@esbuild/openbsd-x64": "0.25.5", + "@esbuild/sunos-x64": "0.25.5", + "@esbuild/win32-arm64": "0.25.5", + "@esbuild/win32-ia32": "0.25.5", + "@esbuild/win32-x64": "0.25.5" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/eventemitter3": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-5.0.1.tgz", "integrity": "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA==", - "dev": true - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/find-cache-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", - "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", - "dev": true, - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", "dev": true, - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } + "license": "MIT" }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/glob": { "version": "7.2.3", @@ -57307,31 +57950,12 @@ "url": "https://github.com/sponsors/isaacs" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/globby": { - "version": "14.0.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-14.0.2.tgz", - "integrity": "sha512-s3Fq41ZVh7vbbe2PN3nrW7yC7U7MFVc5c98/iTl9c2GawNMKx/J648KQRW6WKkuU8GIbbh2IXfIRQjOZnXcTnw==", - "dev": true, - "dependencies": { - "@sindresorhus/merge-streams": "^2.1.0", - "fast-glob": "^3.3.2", - "ignore": "^5.2.4", - "path-type": "^5.0.0", - "slash": "^5.1.0", - "unicorn-magic": "^0.1.0" - }, - "engines": { - "node": ">=18" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/hosted-git-info": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.0.2.tgz", - "integrity": "sha512-sYKnA7eGln5ov8T8gnYlkSOxFJvywzEx9BueN6xo/GKO8PGiI6uK6xx+DIGe45T3bdVjLAQDQW1aicT8z8JwQg==", + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-8.1.0.tgz", + "integrity": "sha512-Rw/B2DNQaPBICNXEm8balFz9a6WpZrkCGpcWFpy7nCj+NyhSdqXipmfvtmWt9xGfp0wZnBxB+iVpLmQMYt47Tw==", "dev": true, + "license": "ISC", "dependencies": { "lru-cache": "^10.0.1" }, @@ -57344,6 +57968,7 @@ "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", "dev": true, + "license": "MIT", "dependencies": { "agent-base": "^7.1.2", "debug": "4" @@ -57352,23 +57977,12 @@ "node": ">= 14" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/ignore-walk": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/ignore-walk/-/ignore-walk-7.0.0.tgz", "integrity": "sha512-T4gbf83A4NH95zvhVYZc+qWocBBGlpzUXLPGurJggw/WIOwicfXJChLDP/iBZnN5WqROSu5Bm3hhle4z8a8YGQ==", "dev": true, + "license": "ISC", "dependencies": { "minimatch": "^9.0.0" }, @@ -57381,6 +57995,7 @@ "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", "dev": true, + "license": "ISC", "dependencies": { "brace-expansion": "^2.0.1" }, @@ -57396,24 +58011,30 @@ "resolved": "https://registry.npmjs.org/ini/-/ini-5.0.0.tgz", "integrity": "sha512-+N0ngpO3e7cRUWOJAS7qw0IZIVc6XPrW4MlFBdD066F2L4k1L6ker3hLqSq7iXxU5tgS4WGkIUElWn5vogAEnw==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/is-fullwidth-code-point": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-4.0.0.tgz", "integrity": "sha512-O4L094N2/dZ7xqVdrXhh9r1KODPJpFms8B5sGdJLPy664AgvXsreZUyCQQNItZRDlYug4xStLjNp/sz3HvBowQ==", "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/is-interactive": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/is-interactive/-/is-interactive-2.0.0.tgz", + "integrity": "sha512-qP1vozQRI+BMOPcjFzrjXuQvdak2pHNUMZoeG2eRbiSqyvbEf/wQtEOTOX1guk6E3t36RkaqiSt8A/6YElNxLQ==", + "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -57421,16 +58042,14 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/is-wsl": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.0.tgz", - "integrity": "sha512-UcVfVfaK4Sc4m7X3dUSoHoozQGBEFeDC+zVo06t98xe8CzHSZZBekNXH+tu0NalHolcJ/QAGqS46Hef7QXBIMw==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/is-unicode-supported": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-2.1.0.tgz", + "integrity": "sha512-mE00Gnza5EEB3Ds0HfMyllZzbBrmLOX3vfWoj9A9PEnTfratQ/BcaJOuMhnkhjXvb2+FkY3VuHqtAGpTPmglFQ==", "dev": true, - "dependencies": { - "is-inside-container": "^1.0.0" - }, + "license": "MIT", "engines": { - "node": ">=16" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -57441,6 +58060,7 @@ "resolved": "https://registry.npmjs.org/istanbul-lib-instrument/-/istanbul-lib-instrument-6.0.3.tgz", "integrity": "sha512-Vtgk7L/R2JHyyGW07spoFlB8/lpjiOLTjMdms6AFMraYt3BaJauod/NGrfnVG/y4Ix1JEuMRPDPEj2ua+zz1/Q==", "dev": true, + "license": "BSD-3-Clause", "dependencies": { "@babel/core": "^7.23.9", "@babel/parser": "^7.23.9", @@ -57463,6 +58083,7 @@ "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, + "license": "MIT", "bin": { "jsesc": "bin/jsesc" }, @@ -57578,10 +58199,13 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/less": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/less/-/less-4.2.1.tgz", - "integrity": "sha512-CasaJidTIhWmjcqv0Uj5vccMI7pJgfD9lMkKtlnTHAdJdYK/7l8pM9tumLyJ0zhbD4KJLo/YvTj+xznQd5NBhg==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/less/-/less-4.3.0.tgz", + "integrity": "sha512-X9RyH9fvemArzfdP8Pi3irr7lor2Ok4rOttDXBhlwDg+wKQsXOXgHWduAJE1EsF7JJx0w0bcO6BC6tCKKYnXKA==", "dev": true, + "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "copy-anything": "^2.0.1", "parse-node-version": "^1.0.1", @@ -57591,7 +58215,7 @@ "lessc": "bin/lessc" }, "engines": { - "node": ">=6" + "node": ">=14" }, "optionalDependencies": { "errno": "^0.1.1", @@ -57603,38 +58227,14 @@ "source-map": "~0.6.0" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/less-loader": { - "version": "12.2.0", - "resolved": "https://registry.npmjs.org/less-loader/-/less-loader-12.2.0.tgz", - "integrity": "sha512-MYUxjSQSBUQmowc0l5nPieOYwMzGPUaTzB6inNW/bdPEG9zOL3eAAD1Qw5ZxSPk7we5dMojHwNODYMV1hq4EVg==", - "dev": true, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "less": "^3.5.0 || ^4.0.0", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/less/node_modules/mime": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", "dev": true, + "license": "MIT", "optional": true, + "peer": true, "bin": { "mime": "cli.js" }, @@ -57647,16 +58247,19 @@ "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "dev": true, + "license": "BSD-3-Clause", "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/listr2": { - "version": "8.2.5", - "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.2.5.tgz", - "integrity": "sha512-iyAZCeyD+c1gPyE9qpFu8af0Y+MRtmKOncdGoA2S5EY8iFq99dmmvkNnHiWo+pj0s7yH7l3KPIgee77tKpXPWQ==", + "version": "8.3.3", + "resolved": "https://registry.npmjs.org/listr2/-/listr2-8.3.3.tgz", + "integrity": "sha512-LWzX2KsqcB1wqQ4AHgYb4RsDXauQiqhjLk+6hjbaeHG4zpjjVAB6wC/gz6X0l+Du1cN3pUB5ZlrvTbhGSNnUQQ==", "dev": true, + "license": "MIT", "dependencies": { "cli-truncate": "^4.0.0", "colorette": "^2.0.20", @@ -57674,6 +58277,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -57686,6 +58290,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -57703,6 +58308,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -57718,6 +58324,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -57730,25 +58337,31 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/loader-utils": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-3.3.1.tgz", - "integrity": "sha512-FMJTLMXfCLMLfJxcX9PFqX5qD88Z5MRGaZCVzfuqeZSPsyiBzs+pahDQjbIWz2QIzPZz0NX9Zy4FX3lmK6YHIg==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/log-symbols": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-6.0.0.tgz", + "integrity": "sha512-i24m8rpwhmPIS4zscNzK6MSEhk0DUWa/8iYQWxhffV8jkI4Phvs3F+quL5xvS0gdQR0FyTCMMH33Y78dDTzzIw==", "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^5.3.0", + "is-unicode-supported": "^1.3.0" + }, "engines": { - "node": ">= 12.13.0" + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/log-symbols/node_modules/is-unicode-supported": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-1.3.0.tgz", + "integrity": "sha512-43r2mRvz+8JRIKnWJ+3j8JtjRKZ6GmjzfaE/qiBJnikNnYv/6bagRJ1kUhNk8R5EX/GkobD+r+sfxCPJsiKBLQ==", "dev": true, - "dependencies": { - "p-locate": "^6.0.0" - }, + "license": "MIT", "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=12" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -57759,6 +58372,7 @@ "resolved": "https://registry.npmjs.org/log-update/-/log-update-6.1.0.tgz", "integrity": "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w==", "dev": true, + "license": "MIT", "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", @@ -57778,6 +58392,7 @@ "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.0.0.tgz", "integrity": "sha512-GdYO7a61mR0fOlAsvC9/rIHf7L96sBc6dEWzeOu+KAea5bZyQRPIpojrVoI4AXGJS/ycu/fBTdLrUkA4ODrvjw==", "dev": true, + "license": "MIT", "dependencies": { "environment": "^1.0.0" }, @@ -57793,6 +58408,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -57805,6 +58421,7 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-5.0.0.tgz", "integrity": "sha512-OVa3u9kkBbw7b8Xw5F9P+D/T9X+Z4+JruYVNapTjPYZYUznQ5YfWeFkOj606XYYW8yugTfC8Pj0hYqvi4ryAhA==", "dev": true, + "license": "MIT", "dependencies": { "get-east-asian-width": "^1.0.0" }, @@ -57820,6 +58437,7 @@ "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-7.1.0.tgz", "integrity": "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" @@ -57836,6 +58454,7 @@ "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", @@ -57853,6 +58472,7 @@ "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-regex": "^6.0.1" }, @@ -57868,6 +58488,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-9.0.0.tgz", "integrity": "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", @@ -57884,13 +58505,15 @@ "version": "10.4.3", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", - "dev": true + "dev": true, + "license": "ISC" }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/magic-string": { "version": "0.30.17", "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.17.tgz", "integrity": "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA==", "dev": true, + "license": "MIT", "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } @@ -57900,6 +58523,7 @@ "resolved": "https://registry.npmjs.org/make-fetch-happen/-/make-fetch-happen-14.0.3.tgz", "integrity": "sha512-QMjGbFTP0blj97EeidG5hk/QhKQ3T4ICckQGLgz38QF7Vgbk6e6FTARN8KhKxyBbWn8R0HU+bnw8aSoFPD4qtQ==", "dev": true, + "license": "ISC", "dependencies": { "@npmcli/agent": "^3.0.0", "cacache": "^19.0.1", @@ -57917,50 +58541,12 @@ "node": "^18.17.0 || >=20.5.0" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/memfs": { - "version": "4.17.0", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-4.17.0.tgz", - "integrity": "sha512-4eirfZ7thblFmqFjywlTmuWVSvccHAJbn1r8qQLzmTO11qcqpohOjmY2mFce6x7x7WtskzRqApPD0hv+Oa74jg==", - "dev": true, - "dependencies": { - "@jsonjoy.com/json-pack": "^1.0.3", - "@jsonjoy.com/util": "^1.3.0", - "tree-dump": "^1.0.1", - "tslib": "^2.0.0" - }, - "engines": { - "node": ">= 4.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/streamich" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/mini-css-extract-plugin": { - "version": "2.9.2", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.2.tgz", - "integrity": "sha512-GJuACcS//jtq4kCtd5ii/M0SZf7OZRH+BxdqXZHaJfb8TJiVl+NgQRPwiYt2EuqeSkNydn/7vP+bcE27C5mb9w==", - "dev": true, - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/minipass": { "version": "7.1.2", "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", "dev": true, + "license": "ISC", "engines": { "node": ">=16 || 14 >=14.17" } @@ -57970,6 +58556,7 @@ "resolved": "https://registry.npmjs.org/minipass-collect/-/minipass-collect-2.0.1.tgz", "integrity": "sha512-D7V8PO9oaz7PWGLbCACuI1qEOsq7UKfLotx/C0Aet43fCUB/wfQ7DYeq2oR/svFJGYDHPr38SHATeaj/ZoKHKw==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -57978,10 +58565,11 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/minipass-fetch": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.0.tgz", - "integrity": "sha512-2v6aXUXwLP1Epd/gc32HAMIWoczx+fZwEPRHm/VwtrJzRGwR1qGZXEYV3Zp8ZjjbwaZhMrM6uHV4KVkk+XCc2w==", + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/minipass-fetch/-/minipass-fetch-4.0.1.tgz", + "integrity": "sha512-j7U11C5HXigVuutxebFadoYBbd7VSdZWggSe64NVdvWNBqGAiXPL2QVCehjmw7lY1oF9gOllYbORh+hiNgfPgQ==", "dev": true, + "license": "MIT", "dependencies": { "minipass": "^7.0.3", "minipass-sized": "^1.0.3", @@ -57995,68 +58583,18 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/minizlib": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.1.tgz", - "integrity": "sha512-umcy022ILvb5/3Djuu8LWeqUa8D68JaBzlttKeMWen48SjabqS3iY5w/vzeMzMUNhLDifyhbOwKDSznB1vvrwg==", + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-3.0.2.tgz", + "integrity": "sha512-oG62iEk+CYt5Xj2YqI5Xi9xWUeZhDI8jjQmC5oThVH5JGCTgIjr7ciJDzC7MBzYd//WvR1OTmP5Q38Q8ShQtVA==", "dev": true, + "license": "MIT", "dependencies": { - "minipass": "^7.0.4", - "rimraf": "^5.0.5" + "minipass": "^7.1.2" }, "engines": { "node": ">= 18" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/minizlib/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/minizlib/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/minizlib/node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", - "dev": true, - "dependencies": { - "glob": "^10.3.7" - }, - "bin": { - "rimraf": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/mkdirp": { "version": "0.5.6", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", @@ -58074,6 +58612,7 @@ "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-2.0.0.tgz", "integrity": "sha512-WWdIxpyjEn+FhQJQQv9aQAYlHoNVdzIzUySNV1gHUPDSdZJ3yZn7pAAbQcV7B56Mvu881q9FZV+0Vx2xC44VWA==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -58083,53 +58622,56 @@ "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-1.0.0.tgz", "integrity": "sha512-8Ofs/AUQh8MaEcrlq5xOX0CQ9ypTF5dl78mjlMNfOK08fzpgTHQRQPBxcPlEtIw0yRpws+Zo/3r+5WRby7u3Gg==", "dev": true, + "license": "MIT", "engines": { "node": ">= 0.6" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-19.1.1.tgz", - "integrity": "sha512-KToU6/nkzD/XDhWJL0Na5ita0d/W6HiohMbDIWv36xQTV26CfICZ3xAPGE4vTbnQS77wCSR+8+jbiBtkw6J1pA==", + "version": "20.0.1", + "resolved": "https://registry.npmjs.org/ng-packagr/-/ng-packagr-20.0.1.tgz", + "integrity": "sha512-MDqUwAg5tXpbOmt7DJH+qvycgNgxEPchwWUy7//1p6lOl2VvbF/XxrC4kAt948YQIkn1UhPxLXHIIcpZt5rt9g==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "dependencies": { + "@ampproject/remapping": "^2.3.0", "@rollup/plugin-json": "^6.1.0", "@rollup/wasm-node": "^4.24.0", "ajv": "^8.17.1", "ansi-colors": "^4.1.3", "browserslist": "^4.22.1", "chokidar": "^4.0.1", - "commander": "^13.0.0", - "convert-source-map": "^2.0.0", + "commander": "^14.0.0", "dependency-graph": "^1.0.0", - "esbuild": "^0.24.0", - "fast-glob": "^3.3.2", - "find-cache-dir": "^3.3.2", + "esbuild": "^0.25.0", + "find-cache-directory": "^6.0.0", "injection-js": "^2.4.0", "jsonc-parser": "^3.3.1", "less": "^4.2.0", - "ora": "^5.1.0", - "piscina": "^4.7.0", + "ora": "^8.2.0", + "piscina": "^5.0.0", "postcss": "^8.4.47", + "rollup-plugin-dts": "^6.2.0", "rxjs": "^7.8.1", - "sass": "^1.81.0" + "sass": "^1.81.0", + "tinyglobby": "^0.2.12" }, "bin": { - "ng-packagr": "cli/main.js" + "ng-packagr": "src/cli/main.js" }, "engines": { - "node": "^18.19.1 || >=20.11.1" + "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "optionalDependencies": { "rollup": "^4.24.0" }, "peerDependencies": { - "@angular/compiler-cli": "^19.0.0 || ^19.1.0-next.0 || ^19.2.0-next.0", - "tailwindcss": "^2.0.0 || ^3.0.0", + "@angular/compiler-cli": "^20.0.0 || ^20.1.0-next.0", + "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0", "tslib": "^2.3.0", - "typescript": ">=5.5 <5.8" + "typescript": ">=5.8 <5.9" }, "peerDependenciesMeta": { "tailwindcss": { @@ -58142,6 +58684,7 @@ "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", "dev": true, + "license": "MIT", "optional": true, "peer": true, "dependencies": { @@ -58154,176 +58697,63 @@ "url": "https://paulmillr.com/funding/" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "dev": true, - "optional": true, - "peer": true - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/find-cache-dir": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-3.3.2.tgz", - "integrity": "sha512-wXZV5emFEjrridIgED11OoUKLxiYjAcqot/NJdAkOhlJ+vGzwhOAfcG5OX1jP+S0PcjEn8bdMJv+g2jwQ3Onig==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "commondir": "^1.0.1", - "make-dir": "^3.0.2", - "pkg-dir": "^4.1.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/avajs/find-cache-dir?sponsor=1" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/find-up": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", - "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "locate-path": "^5.0.0", - "path-exists": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/locate-path": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", - "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "p-locate": "^4.1.0" - }, - "engines": { - "node": ">=8" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/make-dir": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", - "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", "dev": true, + "license": "MIT", "optional": true, "peer": true, - "dependencies": { - "semver": "^6.0.0" - }, "engines": { - "node": ">=8" + "node": ">= 14.18.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "type": "individual", + "url": "https://paulmillr.com/funding/" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/p-limit": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", - "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/rollup-plugin-dts": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/rollup-plugin-dts/-/rollup-plugin-dts-6.2.1.tgz", + "integrity": "sha512-sR3CxYUl7i2CHa0O7bA45mCrgADyAQ0tVtGSqi3yvH28M+eg1+g5d7kQ9hLvEz5dorK3XVsH5L2jwHLQf72DzA==", "dev": true, + "license": "LGPL-3.0-only", "optional": true, "peer": true, "dependencies": { - "p-try": "^2.0.0" + "magic-string": "^0.30.17" }, "engines": { - "node": ">=6" + "node": ">=16" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/p-locate": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", - "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "p-limit": "^2.2.0" - }, - "engines": { - "node": ">=8" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/path-exists": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", - "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">=8" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/pkg-dir": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", - "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", - "dev": true, - "optional": true, - "peer": true, - "dependencies": { - "find-up": "^4.0.0" + "url": "https://github.com/sponsors/Swatinem" }, - "engines": { - "node": ">=8" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", - "dev": true, - "optional": true, - "peer": true, - "engines": { - "node": ">= 14.18.0" + "optionalDependencies": { + "@babel/code-frame": "^7.26.2" }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ng-packagr/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "dev": true, - "optional": true, - "peer": true, - "bin": { - "semver": "bin/semver.js" + "peerDependencies": { + "rollup": "^3.29.4 || ^4", + "typescript": "^4.5 || ^5.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/node-gyp": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.0.0.tgz", - "integrity": "sha512-zQS+9MTTeCMgY0F3cWPyJyRFAkVltQ1uXm+xXu/ES6KFgC6Czo1Seb9vQW2wNxSX2OrDTiqL0ojtkFxBQ0ypIw==", + "version": "11.2.0", + "resolved": "https://registry.npmjs.org/node-gyp/-/node-gyp-11.2.0.tgz", + "integrity": "sha512-T0S1zqskVUSxcsSTkAsLc7xCycrRYmtDHadDinzocrThjyQCn5kMlEBSj6H4qDbgsIOSLmmlRIeb0lZXj+UArA==", "dev": true, + "license": "MIT", "dependencies": { "env-paths": "^2.2.0", "exponential-backoff": "^3.1.1", - "glob": "^10.3.10", "graceful-fs": "^4.2.6", "make-fetch-happen": "^14.0.3", "nopt": "^8.0.0", "proc-log": "^5.0.0", "semver": "^7.3.5", "tar": "^7.4.3", + "tinyglobby": "^0.2.12", "which": "^5.0.0" }, "bin": { @@ -58333,55 +58763,22 @@ "node": "^18.17.0 || >=20.5.0" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/node-gyp/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", - "dev": true, - "dependencies": { - "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", - "minipass": "^7.1.2", - "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" - }, - "bin": { - "glob": "dist/esm/bin.mjs" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/node-gyp/node_modules/isexe": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/isexe/-/isexe-3.1.1.tgz", "integrity": "sha512-LpB/54B+/2J5hqQ7imZHfdU31OlgQqx7ZicVlkm9kzg9/w8GKLEcFfJl/t7DCEDueOyBAD6zCCwTO6Fzs0NoEQ==", "dev": true, + "license": "ISC", "engines": { "node": ">=16" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/node-gyp/node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", - "dev": true, - "dependencies": { - "brace-expansion": "^2.0.1" - }, - "engines": { - "node": ">=16 || 14 >=14.17" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/node-gyp/node_modules/mkdirp": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-3.0.1.tgz", "integrity": "sha512-+NsyUUAZDmo6YVHzL/stxSu3t9YS1iljliy3BSDrXJ/dkn1KYdmtZODGGjLcc9XLgVVpH4KshHB8XmZgMhaBXg==", "dev": true, + "license": "MIT", "bin": { "mkdirp": "dist/cjs/src/bin.js" }, @@ -58397,6 +58794,7 @@ "resolved": "https://registry.npmjs.org/tar/-/tar-7.4.3.tgz", "integrity": "sha512-5S7Va8hKfV7W5U6g3aYxXmlPoZVAwUMy9AOKyF2fVuZa2UD3qZjg578OrLRt8PcNN1PleVaL/5/yYATNL0ICUw==", "dev": true, + "license": "ISC", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -58414,6 +58812,7 @@ "resolved": "https://registry.npmjs.org/which/-/which-5.0.0.tgz", "integrity": "sha512-JEdGzHwwkrbWoGOlIHqQ5gtprKGOenpDHpxE9zVR1bWbOtYRyPPHMe9FaP6x61CmNaTThSkb0DAJte5jD+DmzQ==", "dev": true, + "license": "ISC", "dependencies": { "isexe": "^3.1.1" }, @@ -58429,6 +58828,7 @@ "resolved": "https://registry.npmjs.org/nopt/-/nopt-8.1.0.tgz", "integrity": "sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==", "dev": true, + "license": "ISC", "dependencies": { "abbrev": "^3.0.0" }, @@ -58444,6 +58844,7 @@ "resolved": "https://registry.npmjs.org/npm-bundled/-/npm-bundled-4.0.0.tgz", "integrity": "sha512-IxaQZDMsqfQ2Lz37VvyyEtKLe8FsRZuysmedy/N06TU1RyVppYKXrO4xIhR0F+7ubIBox6Q7nir6fQI3ej39iA==", "dev": true, + "license": "ISC", "dependencies": { "npm-normalize-package-bin": "^4.0.0" }, @@ -58456,6 +58857,7 @@ "resolved": "https://registry.npmjs.org/npm-install-checks/-/npm-install-checks-7.1.1.tgz", "integrity": "sha512-u6DCwbow5ynAX5BdiHQ9qvexme4U3qHW3MWe5NqH+NeBm0LbiH6zvGjNNew1fY+AZZUtVHbOPF3j7mJxbUzpXg==", "dev": true, + "license": "BSD-2-Clause", "dependencies": { "semver": "^7.1.1" }, @@ -58468,15 +58870,17 @@ "resolved": "https://registry.npmjs.org/npm-normalize-package-bin/-/npm-normalize-package-bin-4.0.0.tgz", "integrity": "sha512-TZKxPvItzai9kN9H/TkmCtx/ZN/hvr3vUycjlfmH0ootY9yFBzNOpiXAdIn1Iteqsvk4lQn6B5PTrt+n6h8k/w==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/npm-package-arg": { - "version": "12.0.1", - "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.1.tgz", - "integrity": "sha512-aDxjFfPV3Liw0WOBWlyZLMBqtbgbg03rmGvHDJa2Ttv7tIz+1oB5qWec4psCDFZcZi9b5XdGkPdQiJxOPzvQRQ==", + "version": "12.0.2", + "resolved": "https://registry.npmjs.org/npm-package-arg/-/npm-package-arg-12.0.2.tgz", + "integrity": "sha512-f1NpFjNI9O4VbKMOlA5QoBq/vSQPORHcTZ2feJpFkTHJ9eQkdlmZEKSjcAhxTGInC7RlEyScT9ui67NaOsjFWA==", "dev": true, + "license": "ISC", "dependencies": { "hosted-git-info": "^8.0.0", "proc-log": "^5.0.0", @@ -58488,15 +58892,16 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/npm-packlist": { - "version": "9.0.0", - "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-9.0.0.tgz", - "integrity": "sha512-8qSayfmHJQTx3nJWYbbUmflpyarbLMBc6LCAjYsiGtXxDB68HaZpb8re6zeaLGxZzDuMdhsg70jryJe+RrItVQ==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/npm-packlist/-/npm-packlist-10.0.0.tgz", + "integrity": "sha512-rht9U6nS8WOBDc53eipZNPo5qkAV4X2rhKE2Oj1DYUQ3DieXfj0mKkVmjnf3iuNdtMd8WfLdi2L6ASkD/8a+Kg==", "dev": true, + "license": "ISC", "dependencies": { "ignore-walk": "^7.0.0" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/npm-pick-manifest": { @@ -58504,6 +58909,7 @@ "resolved": "https://registry.npmjs.org/npm-pick-manifest/-/npm-pick-manifest-10.0.0.tgz", "integrity": "sha512-r4fFa4FqYY8xaM7fHecQ9Z2nE9hgNfJR+EmoKv0+chvzWkBcORX3r0FpTByP+CbOVJDladMXnPQGVN8PBLGuTQ==", "dev": true, + "license": "ISC", "dependencies": { "npm-install-checks": "^7.1.0", "npm-normalize-package-bin": "^4.0.0", @@ -58519,6 +58925,7 @@ "resolved": "https://registry.npmjs.org/npm-registry-fetch/-/npm-registry-fetch-18.0.2.tgz", "integrity": "sha512-LeVMZBBVy+oQb5R6FDV9OlJCcWDU+al10oKpe+nsvcHnG24Z3uM3SvJYKfGJlfGjVU8v9liejCrUR/M5HO5NEQ==", "dev": true, + "license": "ISC", "dependencies": { "@npmcli/redact": "^3.0.0", "jsonparse": "^1.3.1", @@ -58538,6 +58945,7 @@ "resolved": "https://registry.npmjs.org/onetime/-/onetime-7.0.0.tgz", "integrity": "sha512-VXJjc87FScF88uafS3JllDgvAm+c/Slfz06lorj2uAY34rlUu0Nt+v8wreiImcrgAjjIHp1rXpTDlLOGw29WwQ==", "dev": true, + "license": "MIT", "dependencies": { "mimic-function": "^5.0.0" }, @@ -58548,16 +58956,22 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/open": { - "version": "10.1.0", - "resolved": "https://registry.npmjs.org/open/-/open-10.1.0.tgz", - "integrity": "sha512-mnkeQ1qP5Ue2wd+aivTD3NHd/lZ96Lu0jgf0pwktLPtx6cTZiH7tyeGRRHs0zX0rbrahXPnXlUnbeXyaBBuIaw==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/ora": { + "version": "8.2.0", + "resolved": "https://registry.npmjs.org/ora/-/ora-8.2.0.tgz", + "integrity": "sha512-weP+BZ8MVNnlCm8c0Qdc1WSWq4Qn7I+9CJGm7Qali6g44e/PUzbjNqJX5NJ9ljlNMosfJvg1fKEGILklK9cwnw==", "dev": true, + "license": "MIT", "dependencies": { - "default-browser": "^5.2.1", - "define-lazy-prop": "^3.0.0", - "is-inside-container": "^1.0.0", - "is-wsl": "^3.1.0" + "chalk": "^5.3.0", + "cli-cursor": "^5.0.0", + "cli-spinners": "^2.9.2", + "is-interactive": "^2.0.0", + "is-unicode-supported": "^2.0.0", + "log-symbols": "^6.0.0", + "stdin-discarder": "^0.2.2", + "string-width": "^7.2.0", + "strip-ansi": "^7.1.0" }, "engines": { "node": ">=18" @@ -58566,34 +58980,38 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/ora/node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", "dev": true, + "license": "MIT", "dependencies": { - "yocto-queue": "^1.0.0" + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=18" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/ora/node_modules/strip-ansi": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.0.tgz", + "integrity": "sha512-iq6eVVI64nQQTRYq2KtEg2d2uU7LElhTJwsH4YzIHZshxlgZms/wIc4VoDQTlG/IvVIrBKG06CrZnp0qv7hkcQ==", "dev": true, + "license": "MIT", "dependencies": { - "p-limit": "^4.0.0" + "ansi-regex": "^6.0.1" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" + "url": "https://github.com/chalk/strip-ansi?sponsor=1" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/p-map": { @@ -58601,6 +59019,7 @@ "resolved": "https://registry.npmjs.org/p-map/-/p-map-7.0.3.tgz", "integrity": "sha512-VkndIv2fIB99swvQoA65bm+fsmt6UNdGeIB0oxBs+WhAhdh08QA04JXpI7rbB9r08/nkbysKoya9rtDERYOYMA==", "dev": true, + "license": "MIT", "engines": { "node": ">=18" }, @@ -58608,28 +59027,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/p-retry": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-6.2.1.tgz", - "integrity": "sha512-hEt02O4hUct5wtwg4H4KcWgDdm+l1bOaEy/hWzd8xtXB9BqxTWBBhb+2ImAtH4Cv4rPjV76xN3Zumqk3k3AhhQ==", - "dev": true, - "dependencies": { - "@types/retry": "0.12.2", - "is-network-error": "^1.0.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=16.17" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/pacote": { - "version": "20.0.0", - "resolved": "https://registry.npmjs.org/pacote/-/pacote-20.0.0.tgz", - "integrity": "sha512-pRjC5UFwZCgx9kUFDVM9YEahv4guZ1nSLqwmWiLUnDbGsjs+U5w7z6Uc8HNR1a6x8qnu5y9xtGE6D1uAuYz+0A==", + "version": "21.0.0", + "resolved": "https://registry.npmjs.org/pacote/-/pacote-21.0.0.tgz", + "integrity": "sha512-lcqexq73AMv6QNLo7SOpz0JJoaGdS3rBFgF122NZVl1bApo2mfu+XzUBU/X/XsiJu+iUmKpekRayqQYAs+PhkA==", "dev": true, + "license": "ISC", "dependencies": { "@npmcli/git": "^6.0.0", "@npmcli/installed-package-contents": "^3.0.0", @@ -58640,7 +59043,7 @@ "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^12.0.0", - "npm-packlist": "^9.0.0", + "npm-packlist": "^10.0.0", "npm-pick-manifest": "^10.0.0", "npm-registry-fetch": "^18.0.0", "proc-log": "^5.0.0", @@ -58653,15 +59056,13 @@ "pacote": "bin/index.js" }, "engines": { - "node": "^18.17.0 || >=20.5.0" + "node": "^20.17.0 || >=22.9.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/parse5": { "version": "7.2.1", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.2.1.tgz", "integrity": "sha512-BuBYQYlv1ckiPdQi/ohiivi9Sagc9JG+Ozs0r7b/0iK3sKmrb0b9FdWdBbOdx6hBCM/F9Ir82ofnBhtZOjCRPQ==", - "optional": true, - "peer": true, "dependencies": { "entities": "^4.5.0" }, @@ -58669,108 +59070,45 @@ "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "dev": true, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/path-type": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-5.0.0.tgz", - "integrity": "sha512-5HviZNaZcfqP95rwpv+1HDgUamezbqdSYTyzjTvwtJSnIH+3vnbmWsItli8OFEndS984VT55M3jduxZbX351gg==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/parse5-html-rewriting-stream": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-html-rewriting-stream/-/parse5-html-rewriting-stream-7.1.0.tgz", + "integrity": "sha512-2ifK6Jb+ONoqOy5f+cYHsqvx1obHQdvIk13Jmt/5ezxP0U9p+fqd+R6O73KblGswyuzBYfetmsfK9ThMgnuPPg==", "dev": true, - "engines": { - "node": ">=12" + "license": "MIT", + "dependencies": { + "entities": "^6.0.0", + "parse5": "^7.0.0", + "parse5-sax-parser": "^7.0.0" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/piscina": { - "version": "4.8.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-4.8.0.tgz", - "integrity": "sha512-EZJb+ZxDrQf3dihsUL7p42pjNyrNIFJCrRHPMgxu/svsj+P3xS3fuEWp7k2+rfsavfl1N0G29b1HGs7J0m8rZA==", - "dev": true, - "optionalDependencies": { - "@napi-rs/nice": "^1.0.1" + "url": "https://github.com/inikulin/parse5?sponsor=1" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/parse5-html-rewriting-stream/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", "dev": true, - "dependencies": { - "find-up": "^6.3.0" - }, + "license": "BSD-2-Clause", "engines": { - "node": ">=14.16" + "node": ">=0.12" }, "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/postcss": { - "version": "8.4.49", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.49.tgz", - "integrity": "sha512-OCVPnIObs4N29kxTjzLfUryOkvZEq+pf8jTF0lg8E7uETuWHA+v7j3c/xJmiqpX450191LlmZfUKkXxkTry7nA==", - "dev": true, - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "dependencies": { - "nanoid": "^3.3.7", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" + "url": "https://github.com/fb55/entities?sponsor=1" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/postcss-loader": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-8.1.1.tgz", - "integrity": "sha512-0IeqyAsG6tYiDRCYKQJLAmgQr47DX6N7sFSWvQxt6AcupX8DIdmykuk/o/tx0Lze3ErGHJEp5OSRxrelC6+NdQ==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/piscina": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.0.0.tgz", + "integrity": "sha512-R+arufwL7sZvGjAhSMK3TfH55YdGOqhpKXkcwQJr432AAnJX/xxX19PA4QisrmJ+BTTfZVggaz6HexbkQq1l1Q==", "dev": true, - "dependencies": { - "cosmiconfig": "^9.0.0", - "jiti": "^1.20.0", - "semver": "^7.5.4" - }, + "license": "MIT", "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" + "node": ">=18.x" }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } + "optionalDependencies": { + "@napi-rs/nice": "^1.0.1" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/proc-log": { @@ -58778,6 +59116,7 @@ "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-5.0.0.tgz", "integrity": "sha512-Azwzvl90HaF0aCz1JrDdXQykFakSSNPaPoiZ9fm5qJIMHioDZEi7OAdRwSm6rSoPtY3Qutnm3L7ogmg3dc+wbQ==", "dev": true, + "license": "ISC", "engines": { "node": "^18.17.0 || >=20.5.0" } @@ -58786,13 +59125,8 @@ "version": "0.2.2", "resolved": "https://registry.npmjs.org/reflect-metadata/-/reflect-metadata-0.2.2.tgz", "integrity": "sha512-urBwgfrvVP/eAyXx4hluJivBKzuEbSQs9rKWCrCkbSxNv8mxPcUZKeuoF3Uy4mJl3Lwprp6yy5/39VWigZ4K6Q==", - "dev": true - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/regenerator-runtime": { - "version": "0.14.1", - "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", - "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", - "dev": true + "dev": true, + "license": "Apache-2.0" }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/resolve": { "version": "1.22.10", @@ -58819,6 +59153,7 @@ "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-5.1.0.tgz", "integrity": "sha512-oMA2dcrw6u0YfxJQXm342bFKX/E4sG9rbTzO9ptUcR/e8A33cHuvStiYOwH7fszkZlZ1z/ta9AAoPk2F4qIOHA==", "dev": true, + "license": "MIT", "dependencies": { "onetime": "^7.0.0", "signal-exit": "^4.1.0" @@ -58830,139 +59165,26 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "dev": true, - "engines": { - "node": ">= 4" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/rollup": { - "version": "4.30.1", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.30.1.tgz", - "integrity": "sha512-mlJ4glW020fPuLi7DkM/lN97mYEZGWeqBnrljzN0gs7GLctqX3lNWxKQ7Gl712UAX+6fog/L3jh4gb7R6aVi3w==", - "dev": true, + "samples/msal-angular-samples/angular-standalone-sample/node_modules/rxjs": { + "version": "7.8.2", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-7.8.2.tgz", + "integrity": "sha512-dhKf903U/PQZY6boNNtAGdWbG85WAbjT/1xYoZIC7FAY0yWapOBQVsVrDl58W86//e1VpMNBtRV4MaXfdMySFA==", + "license": "Apache-2.0", "dependencies": { - "@types/estree": "1.0.6" - }, - "bin": { - "rollup": "dist/bin/rollup" - }, - "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" - }, - "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.30.1", - "@rollup/rollup-android-arm64": "4.30.1", - "@rollup/rollup-darwin-arm64": "4.30.1", - "@rollup/rollup-darwin-x64": "4.30.1", - "@rollup/rollup-freebsd-arm64": "4.30.1", - "@rollup/rollup-freebsd-x64": "4.30.1", - "@rollup/rollup-linux-arm-gnueabihf": "4.30.1", - "@rollup/rollup-linux-arm-musleabihf": "4.30.1", - "@rollup/rollup-linux-arm64-gnu": "4.30.1", - "@rollup/rollup-linux-arm64-musl": "4.30.1", - "@rollup/rollup-linux-loongarch64-gnu": "4.30.1", - "@rollup/rollup-linux-powerpc64le-gnu": "4.30.1", - "@rollup/rollup-linux-riscv64-gnu": "4.30.1", - "@rollup/rollup-linux-s390x-gnu": "4.30.1", - "@rollup/rollup-linux-x64-gnu": "4.30.1", - "@rollup/rollup-linux-x64-musl": "4.30.1", - "@rollup/rollup-win32-arm64-msvc": "4.30.1", - "@rollup/rollup-win32-ia32-msvc": "4.30.1", - "@rollup/rollup-win32-x64-msvc": "4.30.1", - "fsevents": "~2.3.2" + "tslib": "^2.1.0" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/sass": { - "version": "1.83.1", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.83.1.tgz", - "integrity": "sha512-EVJbDaEs4Rr3F0glJzFSOvtg2/oy2V/YrGFPqPY24UqcLDWcI9ZY5sN+qyO3c/QCZwzgfirvhXvINiJCE/OLcA==", + "samples/msal-angular-samples/angular-standalone-sample/node_modules/semver": { + "version": "7.7.2", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", + "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", "dev": true, - "dependencies": { - "chokidar": "^4.0.0", - "immutable": "^5.0.2", - "source-map-js": ">=0.6.2 <2.0.0" - }, + "license": "ISC", "bin": { - "sass": "sass.js" - }, - "engines": { - "node": ">=14.0.0" - }, - "optionalDependencies": { - "@parcel/watcher": "^2.4.1" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/sass-loader": { - "version": "16.0.4", - "resolved": "https://registry.npmjs.org/sass-loader/-/sass-loader-16.0.4.tgz", - "integrity": "sha512-LavLbgbBGUt3wCiYzhuLLu65+fWXaXLmq7YxivLhEqmiupCFZ5sKUAipK3do6V80YSU0jvSxNhEdT13IXNr3rg==", - "dev": true, - "dependencies": { - "neo-async": "^2.6.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "node-sass": "^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0 || ^9.0.0", - "sass": "^1.3.0", - "sass-embedded": "*", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "node-sass": { - "optional": true - }, - "sass": { - "optional": true - }, - "sass-embedded": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/sass/node_modules/chokidar": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", - "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", - "dev": true, - "dependencies": { - "readdirp": "^4.0.1" + "semver": "bin/semver.js" }, "engines": { - "node": ">= 14.16.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/sass/node_modules/readdirp": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.1.tgz", - "integrity": "sha512-h80JrZu/MHUZCyHu5ciuoI0+WxsCxzxJTILn6Fs8rxSnFPh+UVHYfeIxK1nVGugMqkfC4vJcBOYbkfkwYK0+gw==", - "dev": true, - "engines": { - "node": ">= 14.18.0" - }, - "funding": { - "type": "individual", - "url": "https://paulmillr.com/funding/" + "node": ">=10" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/signal-exit": { @@ -58970,6 +59192,7 @@ "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-4.1.0.tgz", "integrity": "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==", "dev": true, + "license": "ISC", "engines": { "node": ">=14" }, @@ -58978,39 +59201,29 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/sigstore": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.0.0.tgz", - "integrity": "sha512-PHMifhh3EN4loMcHCz6l3v/luzgT3za+9f8subGgeMNjbJjzH4Ij/YoX3Gvu+kaouJRIlVdTHHCREADYf+ZteA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/sigstore/-/sigstore-3.1.0.tgz", + "integrity": "sha512-ZpzWAFHIFqyFE56dXqgX/DkDRZdz+rRcjoIk/RQU4IX0wiCv1l8S7ZrXDHcCc+uaf+6o7w3h2l3g6GYG5TKN9Q==", "dev": true, + "license": "Apache-2.0", "dependencies": { - "@sigstore/bundle": "^3.0.0", + "@sigstore/bundle": "^3.1.0", "@sigstore/core": "^2.0.0", - "@sigstore/protobuf-specs": "^0.3.2", - "@sigstore/sign": "^3.0.0", - "@sigstore/tuf": "^3.0.0", - "@sigstore/verify": "^2.0.0" + "@sigstore/protobuf-specs": "^0.4.0", + "@sigstore/sign": "^3.1.0", + "@sigstore/tuf": "^3.1.0", + "@sigstore/verify": "^2.1.0" }, "engines": { "node": "^18.17.0 || >=20.5.0" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/slash": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-5.1.0.tgz", - "integrity": "sha512-ZA6oR3T/pEyuqwMgAKT0/hAv8oAXckzbkmR0UkUosQ+Mc4RxGoJkRmwHgHufaenlyAgE1Mxgpdcrf75y6XcnDg==", - "dev": true, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/slice-ansi": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-5.0.0.tgz", "integrity": "sha512-FC+lgizVPfie0kkhqUScwRu1O/lF6NOgJmlCgK+/LYxDCTk8sGelYaHDhFcDN+Sn3Cv+3VSa4Byeo+IMCzpMgQ==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^6.0.0", "is-fullwidth-code-point": "^4.0.0" @@ -59027,6 +59240,7 @@ "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz", "integrity": "sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==", "dev": true, + "license": "MIT", "engines": { "node": ">=12" }, @@ -59034,31 +59248,12 @@ "url": "https://github.com/chalk/ansi-styles?sponsor=1" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/source-map-loader": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/source-map-loader/-/source-map-loader-5.0.0.tgz", - "integrity": "sha512-k2Dur7CbSLcAH73sBcIkV5xjPV4SzqO1NJ7+XaQl8if3VODDUj3FNchNGpqgJSKbvUfJuhVdv8K2Eu8/TNl2eA==", - "dev": true, - "dependencies": { - "iconv-lite": "^0.6.3", - "source-map-js": "^1.0.2" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.72.1" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/ssri": { "version": "12.0.0", "resolved": "https://registry.npmjs.org/ssri/-/ssri-12.0.0.tgz", "integrity": "sha512-S7iGNosepx9RadX82oimUkvr0Ct7IjJbEbs4mJcTxst8um95J3sDYU1RBEOvdu6oL1Wek2ODI5i4MAw+dZ6cAQ==", "dev": true, + "license": "ISC", "dependencies": { "minipass": "^7.0.3" }, @@ -59067,10 +59262,13 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/terser": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.37.0.tgz", - "integrity": "sha512-B8wRRkmre4ERucLM/uXx4MOV5cbnOlVAqUst+1+iLKPI0dOgFO28f84ptoQt9HEI537PMzfYa/d+GEPKTRXmYA==", + "version": "5.39.1", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.39.1.tgz", + "integrity": "sha512-Mm6+uad0ZuDtcV8/4uOZQDQ8RuiC5Pu+iZRedJtF7yA/27sPL7d++In/AJKpWZlU3SYMPPkVfwetn6sgZ66pUA==", "dev": true, + "license": "BSD-2-Clause", + "optional": true, + "peer": true, "dependencies": { "@jridgewell/source-map": "^0.3.3", "acorn": "^8.8.2", @@ -59088,7 +59286,10 @@ "version": "2.20.3", "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "dev": true + "dev": true, + "license": "MIT", + "optional": true, + "peer": true }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/tmp": { "version": "0.2.3", @@ -59104,6 +59305,7 @@ "resolved": "https://registry.npmjs.org/tuf-js/-/tuf-js-3.0.1.tgz", "integrity": "sha512-+68OP1ZzSF84rTckf3FA95vJ1Zlx/uaXyiiKyPd1pA4rZNkpEvDAKmsu1xUSmbF/chCRYgZ6UZkDwC7PmzmAyA==", "dev": true, + "license": "MIT", "dependencies": { "@tufjs/models": "3.0.1", "debug": "^4.3.6", @@ -59114,10 +59316,11 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/typescript": { - "version": "5.7.3", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", - "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", + "version": "5.8.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.8.3.tgz", + "integrity": "sha512-p1diW6TqL9L07nNxvRMM7hMMw4c5XOo/1ibL4aAIGmSAt9slTE1Xgw5KWuof2uTOvCg9BY7ZRi+GaF+7sfgPeQ==", "dev": true, + "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" @@ -59131,6 +59334,7 @@ "resolved": "https://registry.npmjs.org/unique-filename/-/unique-filename-4.0.0.tgz", "integrity": "sha512-XSnEewXmQ+veP7xX2dS5Q4yZAvO40cBN2MWkJ7D/6sW4Dg6wYBNwM1Vrnz1FhH5AdeLIlUXRI9e28z1YZi71NQ==", "dev": true, + "license": "ISC", "dependencies": { "unique-slug": "^5.0.0" }, @@ -59143,6 +59347,7 @@ "resolved": "https://registry.npmjs.org/unique-slug/-/unique-slug-5.0.0.tgz", "integrity": "sha512-9OdaqO5kwqR+1kVgHAhsp5vPNU0hnxRa26rBFNfNgM7M6pNtgzeBn3s/xbyCQL3dcjzOatcef6UUHpB/6MaETg==", "dev": true, + "license": "ISC", "dependencies": { "imurmurhash": "^0.1.4" }, @@ -59151,111 +59356,13 @@ } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/validate-npm-package-name": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.0.tgz", - "integrity": "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg==", - "dev": true, - "engines": { - "node": "^18.17.0 || >=20.5.0" - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/webpack-dev-middleware": { - "version": "7.4.2", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-7.4.2.tgz", - "integrity": "sha512-xOO8n6eggxnwYpy1NlzUKpvrjfJTvae5/D6WOK0S2LSo7vjmo5gCM1DbLUmFqrMTJP+W/0YZNctm7jasWvLuBA==", - "dev": true, - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^4.6.0", - "mime-types": "^2.1.31", - "on-finished": "^2.4.1", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - } - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/webpack-dev-server": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.0.tgz", - "integrity": "sha512-90SqqYXA2SK36KcT6o1bvwvZfJFcmoamqeJY7+boioffX9g9C0wjjJRGUrQIuh43pb0ttX7+ssavmj/WN2RHtA==", - "dev": true, - "dependencies": { - "@types/bonjour": "^3.5.13", - "@types/connect-history-api-fallback": "^1.5.4", - "@types/express": "^4.17.21", - "@types/serve-index": "^1.9.4", - "@types/serve-static": "^1.15.5", - "@types/sockjs": "^0.3.36", - "@types/ws": "^8.5.10", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.2.1", - "chokidar": "^3.6.0", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "express": "^4.21.2", - "graceful-fs": "^4.2.6", - "http-proxy-middleware": "^2.0.7", - "ipaddr.js": "^2.1.0", - "launch-editor": "^2.6.1", - "open": "^10.0.3", - "p-retry": "^6.2.0", - "schema-utils": "^4.2.0", - "selfsigned": "^2.4.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^7.4.2", - "ws": "^8.18.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 18.12.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/webpack-merge": { "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", + "resolved": "https://registry.npmjs.org/validate-npm-package-name/-/validate-npm-package-name-6.0.1.tgz", + "integrity": "sha512-OaI//3H0J7ZkR1OqlhGA8cA+Cbk/2xFOQpJOt5+s27/ta9eZwpeervh4Mxh4w0im/kdgktowaqVNR7QOrUd7Yg==", "dev": true, - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" - }, + "license": "ISC", "engines": { - "node": ">=18.0.0" + "node": "^18.17.0 || >=20.5.0" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/which": { @@ -59275,6 +59382,7 @@ "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", "dev": true, + "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", @@ -59284,48 +59392,29 @@ "node": ">=8" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/yallist": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-5.0.0.tgz", "integrity": "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==", "dev": true, + "license": "BlueOak-1.0.0", "engines": { "node": ">=18" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/yaml": { - "version": "2.7.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.7.0.tgz", - "integrity": "sha512-+hSoy/QHluxmC9kCIJyL/uyFmLmc+e5CFR5Wa+bpIhIj85LVb9ZH2nVnqrHoSvKogwODv0ClqZkmiSSaIH5LTA==", + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.0.tgz", + "integrity": "sha512-4lLa/EcQCB0cJkyts+FpIRx5G/llPxfP6VQU5KByHEhLxY3IJCH0f0Hy1MHI8sClTvsIb8qwRJ6R/ZdlDJ/leQ==", "dev": true, + "license": "ISC", "optional": true, "peer": true, "bin": { "yaml": "bin.mjs" }, "engines": { - "node": ">= 14" + "node": ">= 14.6" } }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/yargs-parser": { @@ -59337,18 +59426,6 @@ "node": ">=10" } }, - "samples/msal-angular-samples/angular-standalone-sample/node_modules/yocto-queue": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.1.1.tgz", - "integrity": "sha512-b4JR1PFR10y1mKjhHY9LaGo6tmrgjit7hxVIeAmyMw3jegXR4dhYqLaQF5zMXZxY7tLpMyJeLjr1C4rLmkVe8g==", - "dev": true, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "samples/msal-angular-samples/angular-standalone-sample/node_modules/zone.js": { "version": "0.15.0", "resolved": "https://registry.npmjs.org/zone.js/-/zone.js-0.15.0.tgz", @@ -59368,7 +59445,8 @@ "express-session": "^1.17.2", "hbs": "^4.0.4", "http-errors": "~1.6.3", - "morgan": "~1.9.1" + "morgan": "~1.9.1", + "yargs": "^17.7.2" }, "devDependencies": { "nodemon": "^2.0.12" @@ -59418,7 +59496,7 @@ "webpack": "^5.94.0", "webpack-bundle-analyzer": "^4.8.0", "webpack-cli": "^5.0.1", - "webpack-dev-server": "4.13.1" + "webpack-dev-server": "5.2.1" } }, "samples/msal-browser-samples/OfficeAddin/node_modules/babel-loader": { @@ -59542,15 +59620,6 @@ "node": ">=0.10.0" } }, - "samples/msal-browser-samples/OfficeAddin/node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "dev": true, - "engines": { - "node": ">= 10" - } - }, "samples/msal-browser-samples/OfficeAddin/node_modules/loader-utils": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", @@ -59661,128 +59730,6 @@ "webpack": "^5.0.0" } }, - "samples/msal-browser-samples/OfficeAddin/node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", - "dev": true, - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "samples/msal-browser-samples/OfficeAddin/node_modules/webpack-dev-server": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.13.1.tgz", - "integrity": "sha512-5tWg00bnWbYgkN+pd5yISQKDejRBYGEw15RaEEslH+zdbNDxxaZvEAO2WulaSaFKb5n3YG8JXsGaDsut1D0xdA==", - "dev": true, - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.1", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.1", - "ws": "^8.13.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "samples/msal-browser-samples/OfficeAddin/node_modules/ws": { - "version": "8.18.0", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.0.tgz", - "integrity": "sha512-8VbfWfHLbbwu3+N6OKsOMpBdT4kXPDDB9cJk2bJ6mh9ucxdlnNvH1e+roYkKmN9Nxw2yjz7VzeO9oOz2zJ04Pw==", - "dev": true, - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "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/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", @@ -59850,7 +59797,7 @@ "devDependencies": { "@vitejs/plugin-vue": "^3.1.2", "typescript": "^4.8.4", - "vite": "^4.5.6", + "vite": "^4.5.14", "vue-tsc": "^1.0.8" } }, @@ -60006,12 +59953,12 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-session": "^1.17.2" }, "devDependencies": { "@types/jest": "^29.5.0", - "dotenv": "^16.3.1", "e2e-test-utils": "^0.0.1", "jest": "^29.5.0", "jest-junit": "^16.0.0", @@ -60024,7 +59971,7 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", "open": "^8.4.0" }, "devDependencies": { @@ -60051,8 +59998,9 @@ "license": "ISC", "dependencies": { "@azure/msal-node": "^1.15.0", - "axios": "^1.7.4", + "axios": "^1.9.0", "connect-redis": "^7.0.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-session": "^1.17.3", "hbs": "^4.2.0", @@ -60062,7 +60010,6 @@ "@types/express": "^4.17.17", "@types/express-session": "^1.17.6", "@types/node": "^18.14.1", - "dotenv": "^16.0.3", "nodemon": "^2.0.20", "rimraf": "^4.1.2", "ts-node": "^10.9.1", @@ -60073,6 +60020,7 @@ "version": "13.3.1", "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.1.tgz", "integrity": "sha512-Lrk1ozoAtaP/cp53May3v6HtcFSVxdFrg2Pa/1xu5oIvsIwhxW6zSPibKefCOVgd5osgykMi5jjcZHv8XkzZEQ==", + "license": "MIT", "engines": { "node": ">=0.8.0" } @@ -60082,6 +60030,7 @@ "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.18.4.tgz", "integrity": "sha512-Kc/dRvhZ9Q4+1FSfsTFDME/v6+R2Y1fuMty/TfwqE5p9GTPw08BPbKgeWinE8JRHRp+LemjQbUZsn4Q4l6Lszg==", "deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.", + "license": "MIT", "dependencies": { "@azure/msal-common": "13.3.1", "jsonwebtoken": "^9.0.0", @@ -60110,10 +60059,10 @@ } }, "samples/msal-node-samples/auth-code-distributed-cache/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -60230,6 +60179,7 @@ "@azure/keyvault-certificates": "^4.1.0", "@azure/keyvault-secrets": "^4.1.0", "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0", "express": "^4.20.0" } }, @@ -60300,6 +60250,18 @@ "node": ">=0.8.0" } }, + "samples/msal-node-samples/auth-code-key-vault/node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "samples/msal-node-samples/auth-code-key-vault/node_modules/jwa": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz", @@ -60325,6 +60287,7 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-session": "^1.17.2" }, @@ -60332,7 +60295,6 @@ "@types/express": "^4.17.13", "@types/express-session": "^1.17.4", "@types/node": "^16.10.1", - "dotenv": "^16.3.1", "typescript": "^4.4.3" } }, @@ -60343,10 +60305,10 @@ "dev": true }, "samples/msal-node-samples/auth-code-pkce/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -60360,15 +60322,28 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "local": "^0.3.3" } }, + "samples/msal-node-samples/auth-code-with-certs/node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "samples/msal-node-samples/auth-code/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -60382,14 +60357,14 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-handlebars": "^5.3.5", "express-session": "^1.17.2" }, "devDependencies": { "@types/jest": "^29.5.0", - "dotenv": "^16.3.1", "e2e-test-utils": "^0.0.1", "jest": "^29.5.0", "jest-junit": "^16.0.0", @@ -60397,10 +60372,10 @@ } }, "samples/msal-node-samples/b2c-user-flows/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -60413,7 +60388,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@azure/msal-node": "^3.0.0" + "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0" }, "devDependencies": { "@types/jest": "^29.5.0", @@ -60429,14 +60405,14 @@ "license": "ISC", "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", + "dotenv": "^16.5.0", "open": "^8.4.2", "redis": "^4.6.5", "yargs": "^17.7.1" }, "devDependencies": { "@types/node": "^18.14.1", - "dotenv": "^16.0.3", "nodemon": "^2.0.20", "rimraf": "^4.1.2", "ts-node": "^10.9.1", @@ -60462,10 +60438,10 @@ } }, "samples/msal-node-samples/client-credentials-distributed-cache/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -60578,7 +60554,8 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@azure/msal-node": "^3.0.0" + "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0" }, "devDependencies": { "@types/jest": "^29.5.0", @@ -60588,6 +60565,30 @@ "ts-jest": "^29.1.0" } }, + "samples/msal-node-samples/client-credentials-with-cert-from-key-vault/node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, + "samples/msal-node-samples/client-credentials/node_modules/dotenv": { + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "samples/msal-node-samples/custom-INetworkModule-and-network-tracing": { "name": "custom-inetworkmodule-and-network-tracing", "version": "1.0.0", @@ -60640,12 +60641,12 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", "bootstrap": "^5.0.0", "electron-squirrel-startup": "^1.0.0", - "react": "^18.2.0", + "react": "^19.1.0", "react-bootstrap": "^2.7.2", - "react-dom": "^18.2.0", + "react-dom": "^19.1.0", "react-router-dom": "^6.9.0" }, "devDependencies": { @@ -60658,8 +60659,8 @@ "@electron/asar": "^3.2.3", "@playwright/test": "^1.31.1", "@types/jest": "^29.5.0", - "@types/react": "^18.0.28", - "@types/react-dom": "^18.0.11", + "@types/react": "^19.1.3", + "@types/react-dom": "^19.1.3", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "@vercel/webpack-asset-relocator-loader": "1.7.3", @@ -60827,7 +60828,7 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4" + "axios": "^1.9.0" }, "devDependencies": { "babel": "^6.23.0", @@ -60841,7 +60842,7 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", - "dotenv": "^16.0.3", + "dotenv": "^16.5.0", "express": "^4.20.0", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.0.1", @@ -60860,7 +60861,8 @@ "license": "ISC", "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.0.1", @@ -60870,7 +60872,6 @@ "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.1", "@types/node": "^18.14.1", - "dotenv": "^16.0.3", "nodemon": "^2.0.20", "rimraf": "^4.1.2", "ts-node": "^10.9.1", @@ -60896,10 +60897,10 @@ } }, "samples/msal-node-samples/on-behalf-of-distributed-cache/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -61008,9 +61009,10 @@ "dev": true }, "samples/msal-node-samples/on-behalf-of/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -61025,19 +61027,18 @@ "@azure/msal-node": "^3.0.0", "adal-node": "^0.2.3", "cookie-parser": "^1.4.6", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-session": "^1.17.2", "underscore": "^1.13.3" }, - "devDependencies": { - "dotenv": "^16.3.1" - } + "devDependencies": {} }, "samples/msal-node-samples/refresh-token/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -61051,14 +61052,14 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-handlebars": "^5.3.5", "express-promise-router": "^4.0.1" }, "devDependencies": { "@types/jest": "^29.5.0", - "dotenv": "^16.3.1", "e2e-test-utils": "^0.0.1", "jest": "^29.5.0", "jest-junit": "^16.0.0", @@ -61066,10 +61067,10 @@ } }, "samples/msal-node-samples/silent-flow/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -61090,17 +61091,16 @@ "version": "1.0.0", "license": "MIT", "dependencies": { - "@azure/msal-node": "^3.0.0" + "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0" }, - "devDependencies": { - "dotenv": "^16.3.1" - } + "devDependencies": {} }, "samples/msal-node-samples/username-password-cca/node_modules/dotenv": { - "version": "16.4.7", - "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.4.7.tgz", - "integrity": "sha512-47qPchRCykZC03FhkYAhrvwU4xDBFIj1QPqaarj6mdM/hgUzfPHcpkHJOn3mJAufFeeAxAzeGsr5X0M4k6fLZQ==", - "dev": true, + "version": "16.5.0", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.5.0.tgz", + "integrity": "sha512-m/C+AwOAr9/W1UOIZUo232ejMNnJAJtYQjUbHoNTBNTJSvqzzDh7vnrei3o3r3m9blf6ZoDkvcw0VmozNRFJxg==", + "license": "BSD-2-Clause", "engines": { "node": ">=12" }, @@ -61117,8 +61117,8 @@ "@emotion/styled": "^11.9.3", "@mui/icons-material": "^5.8.4", "@mui/material": "^5.9.0", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-router-dom": "^6.7.0" }, "devDependencies": { @@ -61143,9 +61143,9 @@ "@emotion/styled": "^11.10.5", "@mui/icons-material": "^5.10.14", "@mui/material": "^5.10.14", - "next": "^14.2.10", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "next": "^15.3.3", + "react": "^19.1.0", + "react-dom": "^19.1.0" }, "devDependencies": { "@types/jest": "^29.5.2", @@ -61164,8 +61164,8 @@ "@emotion/styled": "^11.10.5", "@mui/icons-material": "^5.10.16", "@mui/material": "^5.10.17", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-router-dom": "^6.7.0", "react-scripts": "^5.0.1" }, @@ -61189,11 +61189,11 @@ "@mui/icons-material": "^5.10.16", "@mui/material": "^5.10.17", "@types/node": "^16.18.10", - "@types/react": "^18.0.26", - "@types/react-dom": "^18.0.9", + "@types/react": "^19.1.3", + "@types/react-dom": "^19.1.3", "@types/react-router-dom": "^5.1.7", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-router-dom": "^6.7.0", "react-scripts": "5.0.1", "typescript": "^4.9.4" diff --git a/samples/e2eTestUtils/package.json b/samples/e2eTestUtils/package.json index 7775b46cb0..7ee0ea35d0 100644 --- a/samples/e2eTestUtils/package.json +++ b/samples/e2eTestUtils/package.json @@ -6,7 +6,7 @@ "main": "src/index.ts", "dependencies": { "@azure/identity": "^4.5.0", - "axios": "^1.7.4", + "axios": "^1.9.0", "dotenv": "^8.2.0", "find-process": "^1.4.4" }, diff --git a/samples/e2eTestUtils/src/Constants.ts b/samples/e2eTestUtils/src/Constants.ts index e2544e9d9f..0d56dc15b5 100644 --- a/samples/e2eTestUtils/src/Constants.ts +++ b/samples/e2eTestUtils/src/Constants.ts @@ -87,28 +87,36 @@ export const GuestHomedIn = { ONPREM: "onprem", }; +export const UsernameSelectors = { + I0116: "#i0116, input[name='i0116']", + USERNAME_ENTRY: "#usernameEntry, input[name='usernameEntry']", + LOGON_IDENTIFIER: "#logonIdentifier, input[type='email']", +} + +export const SubmitButtonSelectors = { + IDSIBUTTON9: "#idSIButton9, input[name='idSIButton9']", + NEXT: "#next, input[name='next']", + ACCEPTBUTTON: "#acceptButton, input[name='acceptButton']", + REMOTE_CONNECT_SUBMIT: "#remoteConnectSubmit, input[name='remoteConnectSubmit']", + SUBMITBUTTON: "#submitButton, input[name='submitButton']", + SUBMIT: "button[type='submit']" +} + +export const PasswordInputSelectors = { + PASSWORD: "#password, input[name='password']", + PASSWORD_INPUT: "#passwordInput, input[name='passwordInput']", + I0118: "#i0118, input[name='i0118']", + PASSWORDENTRY: "#passwordEntry, input[type='password']", +} + export const HtmlSelectors = { - BUTTON9SELECTOR: "#idSIButton9, input[name='idSIButton9']", - USERNAME_INPUT: "#i0116, input[name='i0116']", AAD_TITLE: "#aadTile, input[name='aadTile']", - B2C_LOCAL_ACCOUNT_USERNAME: - "#logonIdentifier, input[name='logonIdentifier']", - B2C_LOCAL_ACCOUNT_PASSWORD: "#password, input[name='password']", - NEXT_BUTTON: "#next, input[name='next']", B2C_AAD_MSIDLAB4_SIGNIN_PAGE: "#MSIDLAB4_AzureAD, input[name='MSIDLAB4_AzureAD']", B2C_MSA_SIGNIN_PAGE: "#MicrosoftAccountExchange, input[name='MicrosoftAccountExchange']", - FORGOT_PASSWORD_LINK: - "#idA_PWD_ForgotPassword, input[name='idA_PWD_ForgotPassword']", - PASSWORD_INPUT_TEXTBOX: "#i0118, input[name='i0118']", KMSI_PAGE: "#kmsiTitle, input[name='kmsiTitle']", - STAY_SIGNEDIN_BUTTON: "#acceptButton, input[name='acceptButton']", REMOTE_LOCATION_DESCRPITION: "#remoteConnectDescription, input[name='remoteConnectDescription']", - REMOTE_LOCATION_SUBMIT_BUTTON: - "#remoteConnectSubmit, input[name='remoteConnectSubmit']", - PASSWORD_INPUT_SELECTOR: "#passwordInput, input[name='passwordInput']", - CREDENTIALS_SUBMIT_BUTTON: "#submitButton, input[name='submitButton']", DEVICE_OTC_INPUT_SELECTOR: "#otc, input[name='otc']", }; diff --git a/samples/e2eTestUtils/src/ElectronPlaywrightTestUtils.ts b/samples/e2eTestUtils/src/ElectronPlaywrightTestUtils.ts index 9c2318cd26..930129a4cb 100644 --- a/samples/e2eTestUtils/src/ElectronPlaywrightTestUtils.ts +++ b/samples/e2eTestUtils/src/ElectronPlaywrightTestUtils.ts @@ -1,86 +1,77 @@ -import { ElectronApplication, Page } from "playwright-core"; +import { ElectronApplication, ElementHandle, Page } from "playwright-core"; import * as fs from "fs"; -import { HtmlSelectors } from "./Constants"; - -export async function enterCredentials( - page: Page, - screenshot: Screenshot, - username: string, - accountPwd: string -): Promise { - await Promise.all([ - page.waitForSelector(HtmlSelectors.USERNAME_INPUT), - page.waitForSelector(HtmlSelectors.BUTTON9SELECTOR), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; +import { HtmlSelectors, PasswordInputSelectors, SubmitButtonSelectors, UsernameSelectors } from "./Constants"; + +export async function getUsernameInput(page: Page): Promise { + const usernameInput = await Promise.any([ + page.waitForSelector( + UsernameSelectors.I0116), + page.waitForSelector( + UsernameSelectors.USERNAME_ENTRY), + page.waitForSelector( + UsernameSelectors.LOGON_IDENTIFIER) + ]).catch(() => { + throw new Error("Username input not found"); }); - await screenshot.takeScreenshot(page, "loginPage"); - await page.type(HtmlSelectors.USERNAME_INPUT, username); - await screenshot.takeScreenshot(page, "loginPageUsernameFilled"); + return usernameInput; +} - await Promise.all([ - page.waitForNavigation({ waitUntil: "load" }), - page.waitForNavigation({ waitUntil: "domcontentloaded" }), - page.waitForNavigation({ waitUntil: "networkidle" }).catch(() => {}), // Wait for navigation but don't throw due to timeout - page.click(HtmlSelectors.BUTTON9SELECTOR), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; +export async function getSubmitButton(page: Page): Promise { + const submitButton = await Promise.any([ + page.waitForSelector(SubmitButtonSelectors.IDSIBUTTON9), + page.waitForSelector(SubmitButtonSelectors.NEXT), + page.waitForSelector(SubmitButtonSelectors.ACCEPTBUTTON), + page.waitForSelector(SubmitButtonSelectors.REMOTE_CONNECT_SUBMIT), + page.waitForSelector(SubmitButtonSelectors.SUBMITBUTTON), + page.waitForSelector(SubmitButtonSelectors.SUBMIT), + ]).catch(() => { + throw new Error("Submit button not found"); }); - await page.waitForSelector(HtmlSelectors.FORGOT_PASSWORD_LINK); - await page.waitForSelector(HtmlSelectors.PASSWORD_INPUT_TEXTBOX); - await page.waitForSelector(HtmlSelectors.BUTTON9SELECTOR); - await screenshot.takeScreenshot(page, "pwdInputPage"); - await page.type(HtmlSelectors.PASSWORD_INPUT_TEXTBOX, accountPwd); - await screenshot.takeScreenshot(page, "loginPagePasswordFilled"); - await page.click(HtmlSelectors.BUTTON9SELECTOR, { noWaitAfter: true }); + return submitButton; } -export async function enterCredentialsADFS( +export async function getPasswordInput(page: Page): Promise { + const passwordInput = await Promise.any([ + page.waitForSelector(PasswordInputSelectors.PASSWORD), + page.waitForSelector(PasswordInputSelectors.PASSWORD_INPUT), + page.waitForSelector(PasswordInputSelectors.I0118), + page.waitForSelector(PasswordInputSelectors.PASSWORDENTRY), + ]).catch(() => { + throw new Error("Password input not found"); + }); + return passwordInput; +} + +export async function enterCredentials( page: Page, screenshot: Screenshot, username: string, accountPwd: string ): Promise { - await Promise.all([ - page.waitForSelector(HtmlSelectors.USERNAME_INPUT), - page.waitForSelector(HtmlSelectors.BUTTON9SELECTOR), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); - - await screenshot.takeScreenshot(page, "loginPageADFS"); - await page.type(HtmlSelectors.USERNAME_INPUT, username); + const usernameInput = await getUsernameInput(page); + await screenshot.takeScreenshot(page, "loginPage"); + await usernameInput.fill(username); await screenshot.takeScreenshot(page, "loginPageUsernameFilled"); + let submitButton = await getSubmitButton(page); await Promise.all([ page.waitForNavigation({ waitUntil: "load" }), page.waitForNavigation({ waitUntil: "domcontentloaded" }), page.waitForNavigation({ waitUntil: "networkidle" }).catch(() => {}), // Wait for navigation but don't throw due to timeout - page.click(HtmlSelectors.BUTTON9SELECTOR), + submitButton.click(), ]).catch(async (e) => { await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); throw e; }); - await page.waitForSelector(HtmlSelectors.PASSWORD_INPUT_SELECTOR); - await page.waitForSelector(HtmlSelectors.CREDENTIALS_SUBMIT_BUTTON); - await page.type(HtmlSelectors.PASSWORD_INPUT_SELECTOR, accountPwd); - await screenshot.takeScreenshot(page, "passwordEntered"); - - await Promise.all([ - page.waitForNavigation({ waitUntil: "load" }), - page.waitForNavigation({ waitUntil: "domcontentloaded" }), - page.waitForNavigation({ waitUntil: "networkidle" }).catch(() => {}), // Wait for navigation but don't throw due to timeout - page.click(HtmlSelectors.CREDENTIALS_SUBMIT_BUTTON), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); + const passwordInput = await getPasswordInput(page); + await screenshot.takeScreenshot(page, "pwdInputPage"); + await passwordInput.fill(accountPwd); + await screenshot.takeScreenshot(page, "loginPagePasswordFilled"); + submitButton = await getSubmitButton(page); + await submitButton.click({ noWaitAfter: true }); } export async function clickSignIn( diff --git a/samples/e2eTestUtils/src/TestUtils.ts b/samples/e2eTestUtils/src/TestUtils.ts index 8cee888852..505ae62c9a 100644 --- a/samples/e2eTestUtils/src/TestUtils.ts +++ b/samples/e2eTestUtils/src/TestUtils.ts @@ -1,14 +1,15 @@ import * as fs from "fs"; -import { Page, HTTPResponse, Browser, WaitForOptions } from "puppeteer"; +import { Page, Browser, WaitForOptions } from "puppeteer"; import { LabConfig } from "./LabConfig"; import { LabClient } from "./LabClient"; -import { HtmlSelectors } from "./Constants"; +import { HtmlSelectors, PasswordInputSelectors, SubmitButtonSelectors, UsernameSelectors } from "./Constants"; export const ONE_SECOND_IN_MS = 1000; export const RETRY_TIMES = 5; const WAIT_FOR_NAVIGATION_CONFIG: WaitForOptions = { waitUntil: ["load", "domcontentloaded", "networkidle0"], + timeout: 2000 }; export class Screenshot { @@ -174,11 +175,9 @@ export async function b2cLocalAccountEnterCredentials( username: string, accountPwd: string ) { - await page.waitForSelector(HtmlSelectors.B2C_LOCAL_ACCOUNT_USERNAME); - await screenshot.takeScreenshot(page, "b2cSignInPage"); - await page.type(HtmlSelectors.B2C_LOCAL_ACCOUNT_USERNAME, username); - await page.type(HtmlSelectors.B2C_LOCAL_ACCOUNT_PASSWORD, accountPwd); - await page.click(HtmlSelectors.NEXT_BUTTON); + await fillUsername(page, screenshot, username); + await fillPassword(page, screenshot, accountPwd); + await clickSubmitButton(page, screenshot); } export async function b2cAadPpeAccountEnterCredentials( @@ -216,126 +215,100 @@ export const SUCCESSFUL_GRAPH_CALL_ID = "graph-called-successfully"; export const SUCCESSFUL_SILENT_TOKEN_ACQUISITION_ID = "token-acquired-silently"; export const SUCCESSFUL_GET_ALL_ACCOUNTS_ID = "accounts-retrieved-successfully"; -export async function enterCredentials( - page: Page, - screenshot: Screenshot, - username: string, - accountPwd: string -): Promise { - await Promise.all([ - page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG).catch(() => {}), // Wait for navigation but don't throw due to timeout - page.waitForSelector(HtmlSelectors.USERNAME_INPUT), - page.waitForSelector(HtmlSelectors.BUTTON9SELECTOR), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); - await screenshot.takeScreenshot(page, "loginPage"); - await page.type(HtmlSelectors.USERNAME_INPUT, username); - await screenshot.takeScreenshot(page, "loginPageUsernameFilled"); - await Promise.all([ - page.waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }), - page.click(HtmlSelectors.BUTTON9SELECTOR), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); - - // agce: which type of account do you want to use +export async function fillPassword(page: Page, screenshot: Screenshot, password: string): Promise { try { - await page.waitForSelector(HtmlSelectors.AAD_TITLE, { timeout: 1000 }); - await screenshot.takeScreenshot(page, "accountType"); - await Promise.all([ - page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), - page.click(HtmlSelectors.AAD_TITLE), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); + await screenshot.takeScreenshot(page, "passwordPage"); + await page.locator(`${Object.values(PasswordInputSelectors).join(", ")}`).setTimeout(2000).fill(password); + await screenshot.takeScreenshot(page, "loginPagePasswordFilled"); } catch (e) { - // - } - - await page.waitForSelector(HtmlSelectors.FORGOT_PASSWORD_LINK); - await page.waitForSelector(HtmlSelectors.PASSWORD_INPUT_TEXTBOX); - await page.waitForSelector(HtmlSelectors.BUTTON9SELECTOR); - await screenshot.takeScreenshot(page, "pwdInputPage"); - await page.type(HtmlSelectors.PASSWORD_INPUT_TEXTBOX, accountPwd); - await screenshot.takeScreenshot(page, "loginPagePasswordFilled"); - await Promise.all([ - page.click(HtmlSelectors.BUTTON9SELECTOR), - - // Wait either for another navigation to Keep me signed in page or back to redirectUri - Promise.race([ - page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), - page.waitForResponse( - (response: HTTPResponse) => - response.url().startsWith(SAMPLE_HOME_URL), - { timeout: 0 } - ), - ]), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); + await screenshot.takeScreenshot(page, "failedToFillPassword").catch(() => {}); throw e; - }); - - if (page.url().startsWith(SAMPLE_HOME_URL)) { - return; } - await screenshot.takeScreenshot(page, "passwordSubmitted"); +} - // agce: check if the "help us protect your account" dialog appears +export async function fillUsername(page: Page, screenshot: Screenshot, username: string): Promise { try { - const selector = - "#lightbox > div:nth-child(3) > div > div.pagination-view.has-identity-banner.animate.slide-in-next > div > div:nth-child(3) > a"; - await page.waitForSelector(selector, { timeout: 1000 }); - await page.click(selector); + await screenshot.takeScreenshot(page, "loginPage"); + await page.locator(`${Object.values(UsernameSelectors).join(", ")}`).setTimeout(2000).fill(username); + await screenshot.takeScreenshot(page, "loginPageUsernameFilled"); } catch (e) { - // continue + await screenshot.takeScreenshot(page, "failedToFillUsername").catch(() => {}); + throw e; } +} - // keep me signed in page +export async function clickSubmitButton(page: Page, screenshot: Screenshot): Promise { try { - const aadKmsi = page - .waitForSelector(HtmlSelectors.BUTTON9SELECTOR, { timeout: 1000 }) - .then(() => { - return HtmlSelectors.BUTTON9SELECTOR; - }); - const msaKmsi = page - .waitForSelector(HtmlSelectors.KMSI_PAGE, { timeout: 1000 }) - .then(() => { - return HtmlSelectors.STAY_SIGNEDIN_BUTTON; - }); - const buttonTag = await Promise.race([aadKmsi, msaKmsi]); - await screenshot.takeScreenshot(page, "keepMeSignedInPage"); - await Promise.all([ - page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), - page.click(buttonTag), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); + await page.locator(`${Object.values(SubmitButtonSelectors).join(", ")}`).setTimeout(2000).click(); + await page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG).catch(() => {}); } catch (e) { - return; + await screenshot.takeScreenshot(page, "errorClickingSubmit").catch(() => {}); + throw e; } +} - // agce: private tenant sign in page +export async function enterCredentials( + page: Page, + screenshot: Screenshot, + username: string, + accountPwd: string +): Promise { try { - await page.waitForSelector(HtmlSelectors.BUTTON9SELECTOR, { - timeout: 1000, - }); - await screenshot.takeScreenshot(page, "privateTenantSignInPage"); - await Promise.all([ - page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), - page.click(HtmlSelectors.BUTTON9SELECTOR), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); + await page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG).catch(() => {}); + await fillUsername(page, screenshot, username); + await clickSubmitButton(page, screenshot); + + // agce: which type of account do you want to use + try { + await page.waitForSelector(HtmlSelectors.AAD_TITLE, { timeout: 1000 }); + await screenshot.takeScreenshot(page, "accountType"); + await Promise.all([ + page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), + page.click(HtmlSelectors.AAD_TITLE), + ]).catch(async (e) => { + await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); + throw e; + }); + } catch (e) { + // + } + + await fillPassword(page, screenshot, accountPwd); + await clickSubmitButton(page, screenshot); + + if (page.isClosed() || page.url().startsWith(SAMPLE_HOME_URL)) { + return; + } + await screenshot.takeScreenshot(page, "passwordSubmitted"); + + // agce: check if the "help us protect your account" dialog appears + try { + const selector = + "#lightbox > div:nth-child(3) > div > div.pagination-view.has-identity-banner.animate.slide-in-next > div > div:nth-child(3) > a"; + await page.waitForSelector(selector, { timeout: 1000 }); + await page.click(selector); + } catch (e) { + // continue + } + + // keep me signed in page + try { + await screenshot.takeScreenshot(page, "keepMeSignedInPage"); + await clickSubmitButton(page, screenshot); + } catch (e) { + return; + } + + // agce: private tenant sign in page + try { + await screenshot.takeScreenshot(page, "privateTenantSignInPage"); + await clickSubmitButton(page, screenshot); + } catch (e) { + return; + } } catch (e) { - return; + await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); + throw e; } } @@ -345,15 +318,7 @@ export async function approveRemoteConnect( ): Promise { try { await page.waitForSelector(HtmlSelectors.REMOTE_LOCATION_DESCRPITION); - await page.waitForSelector(HtmlSelectors.REMOTE_LOCATION_SUBMIT_BUTTON); - await screenshot.takeScreenshot(page, "remoteConnectPage"); - await Promise.all([ - page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), - page.click(HtmlSelectors.REMOTE_LOCATION_SUBMIT_BUTTON), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); + await clickSubmitButton(page, screenshot); } catch (e) { return; } @@ -373,16 +338,7 @@ export async function approveConsent( page: Page, screenshot: Screenshot ): Promise { - await page.waitForSelector(HtmlSelectors.BUTTON9SELECTOR); - await Promise.all([ - page.waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }), - page.click(HtmlSelectors.BUTTON9SELECTOR), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); + await clickSubmitButton(page, screenshot); await screenshot.takeScreenshot(page, "consentApproved"); } @@ -393,9 +349,7 @@ export async function clickSignIn( await page.waitForSelector("#SignIn"); await screenshot.takeScreenshot(page, "samplePageInit"); await Promise.all([ - page.waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }), + page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG), page.click("#SignIn"), ]).catch(async (e) => { await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); @@ -410,40 +364,12 @@ export async function enterCredentialsADFS( username: string, accountPwd: string ): Promise { - await Promise.all([ - page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG).catch(() => {}), // Wait for navigation but don't throw due to timeout - page.waitForSelector(HtmlSelectors.USERNAME_INPUT), - page.waitForSelector(HtmlSelectors.BUTTON9SELECTOR), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); - await screenshot.takeScreenshot(page, "loginPageADFS"); - await page.type(HtmlSelectors.USERNAME_INPUT, username); - await screenshot.takeScreenshot(page, "usernameEntered"); - await Promise.all([ - page.waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }), - page.click(HtmlSelectors.BUTTON9SELECTOR), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); - await page.waitForSelector(HtmlSelectors.PASSWORD_INPUT_SELECTOR); - await page.waitForSelector(HtmlSelectors.CREDENTIALS_SUBMIT_BUTTON); - await page.type(HtmlSelectors.PASSWORD_INPUT_SELECTOR, accountPwd); - await screenshot.takeScreenshot(page, "passwordEntered"); - await Promise.all([ - page.waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }), - page.click(HtmlSelectors.CREDENTIALS_SUBMIT_BUTTON), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); - await screenshot.takeScreenshot(page, "pwdSubmitted"); + await page.waitForNavigation(WAIT_FOR_NAVIGATION_CONFIG).catch(() => {}); + await fillUsername(page, screenshot, username); + await clickSubmitButton(page, screenshot); + await page.waitForSelector(PasswordInputSelectors.PASSWORD_INPUT); + await fillPassword(page, screenshot, accountPwd); + await clickSubmitButton(page, screenshot); } export async function enterDeviceCode( @@ -456,18 +382,9 @@ export async function enterDeviceCode( waitUntil: ["load", "domcontentloaded", "networkidle0"], }); await page.waitForSelector(HtmlSelectors.DEVICE_OTC_INPUT_SELECTOR); - await page.waitForSelector(HtmlSelectors.BUTTON9SELECTOR); await screenshot.takeScreenshot(page, "deviceCodePage"); await page.type(HtmlSelectors.DEVICE_OTC_INPUT_SELECTOR, code); - await Promise.all([ - page.waitForNavigation({ - waitUntil: ["load", "domcontentloaded", "networkidle0"], - }), - page.click(HtmlSelectors.BUTTON9SELECTOR), - ]).catch(async (e) => { - await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); - throw e; - }); + await clickSubmitButton(page, screenshot); } export async function validateCacheLocation( diff --git a/samples/msal-angular-samples/angular-b2c-sample/package.json b/samples/msal-angular-samples/angular-b2c-sample/package.json index 7c4ca3fc60..edda114339 100644 --- a/samples/msal-angular-samples/angular-b2c-sample/package.json +++ b/samples/msal-angular-samples/angular-b2c-sample/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@angular-devkit/build-angular": "^16.0.5", - "@angular/cli": "~16.0.5", + "@angular/cli": "^16.0.5", "@angular/compiler-cli": "^16.0.4", "@types/jasmine": "~4.0.0", "@types/jasminewd2": "^2.0.10", diff --git a/samples/msal-angular-samples/angular-b2c-sample/tsconfig.json b/samples/msal-angular-samples/angular-b2c-sample/tsconfig.json index ff2ef2a9c6..b07061edd7 100644 --- a/samples/msal-angular-samples/angular-b2c-sample/tsconfig.json +++ b/samples/msal-angular-samples/angular-b2c-sample/tsconfig.json @@ -20,7 +20,7 @@ "module": "es2020", "esModuleInterop": true, "lib": [ - "es2020", + "es2021", "dom" ], "paths": { diff --git a/samples/msal-angular-samples/angular-modules-sample/package.json b/samples/msal-angular-samples/angular-modules-sample/package.json index 22ba5cadc8..5e7e24b1e1 100644 --- a/samples/msal-angular-samples/angular-modules-sample/package.json +++ b/samples/msal-angular-samples/angular-modules-sample/package.json @@ -30,7 +30,7 @@ }, "devDependencies": { "@angular-devkit/build-angular": "^16.0.4", - "@angular/cli": "~16.0.4", + "@angular/cli": "^16.0.4", "@angular/compiler-cli": "^16.0.4", "@types/jasmine": "~4.3.0", "@types/jest": "^29.5.0", diff --git a/samples/msal-angular-samples/angular-standalone-sample/angular.json b/samples/msal-angular-samples/angular-standalone-sample/angular.json index 6e3a118533..38119eb5f1 100644 --- a/samples/msal-angular-samples/angular-standalone-sample/angular.json +++ b/samples/msal-angular-samples/angular-standalone-sample/angular.json @@ -11,7 +11,7 @@ "prefix": "app", "architect": { "build": { - "builder": "@angular-devkit/build-angular:application", + "builder": "@angular/build:application", "options": { "outputPath": "dist/angular-standalone-sample", "index": "src/index.html", @@ -83,7 +83,7 @@ "defaultConfiguration": "production" }, "serve": { - "builder": "@angular-devkit/build-angular:dev-server", + "builder": "@angular/build:dev-server", "configurations": { "production": { "buildTarget": "angular-standalone-sample:build:production" @@ -98,13 +98,13 @@ "defaultConfiguration": "dev" }, "extract-i18n": { - "builder": "@angular-devkit/build-angular:extract-i18n", + "builder": "@angular/build:extract-i18n", "options": { "buildTarget": "angular-standalone-sample:build" } }, "test": { - "builder": "@angular-devkit/build-angular:karma", + "builder": "@angular/build:karma", "options": { "polyfills": [ "zone.js", @@ -129,5 +129,31 @@ }, "cli": { "analytics": false + }, + "schematics": { + "@schematics/angular:component": { + "type": "component" + }, + "@schematics/angular:directive": { + "type": "directive" + }, + "@schematics/angular:service": { + "type": "service" + }, + "@schematics/angular:guard": { + "typeSeparator": "." + }, + "@schematics/angular:interceptor": { + "typeSeparator": "." + }, + "@schematics/angular:module": { + "typeSeparator": "." + }, + "@schematics/angular:pipe": { + "typeSeparator": "." + }, + "@schematics/angular:resolver": { + "typeSeparator": "." + } } } \ No newline at end of file diff --git a/samples/msal-angular-samples/angular-standalone-sample/package.json b/samples/msal-angular-samples/angular-standalone-sample/package.json index b57f8bd0bc..89623ce083 100644 --- a/samples/msal-angular-samples/angular-standalone-sample/package.json +++ b/samples/msal-angular-samples/angular-standalone-sample/package.json @@ -13,15 +13,15 @@ }, "private": true, "dependencies": { - "@angular/animations": "^19.1.0", - "@angular/common": "^19.1.0", - "@angular/compiler": "^19.1.0", - "@angular/core": "^19.1.0", - "@angular/forms": "^19.1.0", - "@angular/material": "^18.0.0", - "@angular/platform-browser": "^19.1.0", - "@angular/platform-browser-dynamic": "^19.1.0", - "@angular/router": "^19.1.0", + "@angular/animations": "^20.0.4", + "@angular/common": "^20.0.4", + "@angular/compiler": "^20.0.4", + "@angular/core": "^20.0.4", + "@angular/forms": "^20.0.4", + "@angular/material": "^20.0.3", + "@angular/platform-browser": "^20.0.4", + "@angular/platform-browser-dynamic": "^20.0.4", + "@angular/router": "^20.0.4", "@azure/msal-angular": "^4.0.0", "@azure/msal-browser": "^4.0.0", "rxjs": "~7.8.0", @@ -29,9 +29,9 @@ "zone.js": "~0.15.0" }, "devDependencies": { - "@angular-devkit/build-angular": "^19.1.0", - "@angular/cli": "^19.1.0", - "@angular/compiler-cli": "^19.1.0", + "@angular/build": "^20.0.3", + "@angular/cli": "^20.0.3", + "@angular/compiler-cli": "^20.0.4", "@types/jasmine": "~5.1.0", "@types/jest": "^29.5.12", "e2e-test-utils": "file:../../e2eTestUtils", @@ -44,6 +44,6 @@ "karma-jasmine": "~5.1.0", "karma-jasmine-html-reporter": "~2.1.0", "ts-jest": "^29.1.2", - "typescript": "~5.7.3" + "typescript": "~5.8.3" } -} \ No newline at end of file +} diff --git a/samples/msal-angular-samples/angular-standalone-sample/tsconfig.json b/samples/msal-angular-samples/angular-standalone-sample/tsconfig.json index f00f29aab4..17810df24b 100644 --- a/samples/msal-angular-samples/angular-standalone-sample/tsconfig.json +++ b/samples/msal-angular-samples/angular-standalone-sample/tsconfig.json @@ -16,7 +16,7 @@ "sourceMap": true, "declaration": false, "experimentalDecorators": true, - "moduleResolution": "Node", + "moduleResolution": "bundler", "importHelpers": true, "target": "ES2022", "module": "ES2022", diff --git a/samples/msal-browser-samples/HybridSample/package.json b/samples/msal-browser-samples/HybridSample/package.json index 94945d2bdf..8f82f6b5ab 100644 --- a/samples/msal-browser-samples/HybridSample/package.json +++ b/samples/msal-browser-samples/HybridSample/package.json @@ -18,7 +18,8 @@ "express-session": "^1.17.2", "hbs": "^4.0.4", "http-errors": "~1.6.3", - "morgan": "~1.9.1" + "morgan": "~1.9.1", + "yargs": "^17.7.2" }, "devDependencies": { "nodemon": "^2.0.12" diff --git a/samples/msal-browser-samples/HybridSample/routes/auth.js b/samples/msal-browser-samples/HybridSample/routes/auth.js index 0ce4af4fe2..efe5657592 100644 --- a/samples/msal-browser-samples/HybridSample/routes/auth.js +++ b/samples/msal-browser-samples/HybridSample/routes/auth.js @@ -13,11 +13,8 @@ router.use('/lib', express.static(path.join(__dirname, '../node_modules/@azure/m router.get('/login', (req, res) => { const authCodeUrlParameters = { scopes: ["user.read"], - redirectUri: "https://localhost:3000/auth/server-redirect", - responseMode: "form_post", - extraQueryParameters: { - "nativebroker": "1", - } + redirectUri: "http://localhost:3000/auth/server-redirect", + responseMode: "form_post" }; // Set request state to use hybrid spa or implicit flow @@ -42,7 +39,7 @@ router.post('/server-redirect', (req, res) => { const tokenRequest = { code: req.body.code, scopes: ["user.read"], - redirectUri: "https://localhost:3000/auth/server-redirect" + redirectUri: "http://localhost:3000/auth/server-redirect" }; // Check if request is done via hybrid spa or implicit flow diff --git a/samples/msal-browser-samples/HybridSample/views/client-redirect.hbs b/samples/msal-browser-samples/HybridSample/views/client-redirect.hbs index b05b88da0c..f35fd71aa4 100644 --- a/samples/msal-browser-samples/HybridSample/views/client-redirect.hbs +++ b/samples/msal-browser-samples/HybridSample/views/client-redirect.hbs @@ -6,7 +6,7 @@ const msalInstance = new msal.PublicClientApplication({ auth: { clientId: "{{clientId}}", - redirectUri: "https://localhost:3000/auth/client-redirect", + redirectUri: "http://localhost:3000/auth/client-redirect", authority: "{{authority}}" }, system: { @@ -17,7 +17,7 @@ piiLoggingEnabled: false, logLevel: msal.LogLevel.Verbose, }, - allowPlatformBroker: true + allowNativeBroker: false } }); diff --git a/samples/msal-browser-samples/HybridSample/views/implicit-redirect.hbs b/samples/msal-browser-samples/HybridSample/views/implicit-redirect.hbs index a5deeb3491..cf8e0f3e1e 100644 --- a/samples/msal-browser-samples/HybridSample/views/implicit-redirect.hbs +++ b/samples/msal-browser-samples/HybridSample/views/implicit-redirect.hbs @@ -5,7 +5,7 @@ const msalInstance = new Msal.UserAgentApplication({ auth: { clientId: "{{clientId}}", - redirectUri: "https://localhost:3000/auth/implicit-redirect", + redirectUri: "http://localhost:3000/auth/implicit-redirect", authority: "{{authority}}" } }) diff --git a/samples/msal-browser-samples/OfficeAddin/package.json b/samples/msal-browser-samples/OfficeAddin/package.json index 21b0ed110b..4ce5b4f3cc 100644 --- a/samples/msal-browser-samples/OfficeAddin/package.json +++ b/samples/msal-browser-samples/OfficeAddin/package.json @@ -57,7 +57,7 @@ "webpack": "^5.94.0", "webpack-bundle-analyzer": "^4.8.0", "webpack-cli": "^5.0.1", - "webpack-dev-server": "4.13.1" + "webpack-dev-server": "5.2.1" }, "prettier": "office-addin-prettier-config", "browserslist": [ diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAAD.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAAD.spec.ts index 3d5c14c795..7295b14e1d 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAAD.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAAD.spec.ts @@ -295,7 +295,6 @@ describe("AAD-Prod Tests", () => { .startsWith("https://login.microsoftonline.com/common/") ).toBeTruthy(); expect(popupWindow.url()).toContain("logout"); - await popupWindow.waitForNavigation(); const tokenStore = await BrowserCache.getTokens(); expect(tokenStore.idTokens.length).toEqual(0); @@ -304,6 +303,91 @@ describe("AAD-Prod Tests", () => { }); }); + describe("login-logout-login tests", () => { + let testName: string; + let screenshot: Screenshot; + + async function loginPopup() { + const [popupPage, popupWindowClosed] = await clickLoginPopup( + screenshot, + page + ); + await enterCredentials(popupPage, screenshot, username, accountPwd); + await waitForReturnToApp( + screenshot, + page, + popupPage, + popupWindowClosed + ); + + // Verify browser cache contains Account, idToken, AccessToken and RefreshToken + await BrowserCache.verifyTokenStore({ + scopes: aadTokenRequest.scopes, + }); + } + + async function loginRedirect() { + await clickLoginRedirect(screenshot, page); + await enterCredentials(page, screenshot, username, accountPwd); + await waitForReturnToApp(screenshot, page); + } + + beforeEach(async () => { + context = await browser.createBrowserContext(); + page = await context.newPage(); + page.setDefaultTimeout(ONE_SECOND_IN_MS * 5); + BrowserCache = new BrowserCacheUtils( + page, + aadMsalConfig.cache.cacheLocation + ); + await page.goto(sampleHomeUrl); + await pcaInitializedPoller(page, 5000); + }); + + afterEach(async () => { + await page.evaluate(() => + Object.assign({}, window.sessionStorage.clear()) + ); + await page.evaluate(() => + Object.assign({}, window.localStorage.clear()) + ); + await page.close(); + }); + + it("login-logout-login redirect", async () => { + testName = "loginLogoutLoginRedirect"; + screenshot = new Screenshot( + `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` + ); + await loginRedirect(); + + await clickLogoutRedirect(screenshot, page); + expect( + page + .url() + .startsWith("https://login.microsoftonline.com/common/") + ).toBeTruthy(); + expect(page.url()).toContain("logout"); + await page.waitForFunction( + `window.location.href.startsWith("${sampleHomeUrl}")` + ); + + const tokenStore = await BrowserCache.getTokens(); + expect(tokenStore.idTokens.length).toEqual(0); + expect(tokenStore.accessTokens.length).toEqual(0); + expect(tokenStore.refreshTokens.length).toEqual(0); + + await clickLoginRedirect(screenshot, page); + await page.waitForNavigation({ waitUntil: ["load", "domcontentloaded", "networkidle0"] }).catch(() => {}); + try { + await page.waitForSelector("#loginHeader, div[name='loginHeader']"); + } catch (e) { + await screenshot.takeScreenshot(page, "errorPage").catch(() => {}); + throw e; + } + }); + }); + describe("acquireToken Tests", () => { let testName: string; let screenshot: Screenshot; diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts index 4f725d28c6..527ddf9189 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADMultiTenant.spec.ts @@ -150,7 +150,6 @@ describe("AAD-Prod Tests", () => { ) ).toBeTruthy(); expect(popupWindow.url()).toContain("logout"); - await popupWindow.waitForNavigation(); const tokenStore = await BrowserCache.getTokens(); expect(tokenStore.idTokens.length).toEqual(0); diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADTenanted.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADTenanted.spec.ts index 4b0d9821ea..ba82066b1b 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADTenanted.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/browserAADTenanted.spec.ts @@ -299,7 +299,6 @@ describe("AAD-Prod Tests", () => { ) ).toBeTruthy(); expect(popupWindow.url()).toContain("logout"); - await popupWindow.waitForNavigation(); const tokenStore = await BrowserCache.getTokens(); expect(tokenStore.idTokens.length).toEqual(0); diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/localStorage.spec.ts b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/localStorage.spec.ts index b0509e00f0..8061c1ba68 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/localStorage.spec.ts +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/customizable-e2e-test/test/localStorage.spec.ts @@ -112,7 +112,7 @@ describe("LocalStorage Tests", function () { `${SCREENSHOT_BASE_FOLDER_NAME}/${testName}` ); await clickLoginRedirect(screenshot, page); - await page.waitForNavigation({ waitUntil: "networkidle0" }); + await page.waitForNavigation({ waitUntil: "networkidle0" }).catch(() => {}); // Navigate back to home page await page.goto(sampleHomeUrl); // Wait for processing @@ -125,9 +125,9 @@ describe("LocalStorage Tests", function () { const sessionStorage = await sessionBrowserStorage.getWindowStorage(); const localStorage = await BrowserCache.getWindowStorage(); - expect(Object.keys(localStorage).length).toBeLessThanOrEqual(2); + expect(Object.keys(localStorage).length).toBeLessThanOrEqual(3); Object.keys(localStorage).forEach((key) => { - expect(key.startsWith("msal.token.keys") || key === "msal.account.keys").toBe(true); + expect(key.startsWith("msal.token.keys") || key === "msal.account.keys" || key == "msal.version").toBe(true); }); expect(Object.keys(sessionStorage).length).toEqual(0); }, ONE_SECOND_IN_MS); @@ -166,7 +166,7 @@ describe("LocalStorage Tests", function () { screenshot, page ); - await popupPage.waitForNavigation({ waitUntil: "networkidle0" }); + await popupPage.waitForNavigation({ waitUntil: "networkidle0" }).catch(() => {}); await popupPage.close(); // Wait until popup window closes await popupWindowClosed; @@ -180,7 +180,7 @@ describe("LocalStorage Tests", function () { const sessionStorage = await sessionBrowserStorage.getWindowStorage(); const localStorage = await BrowserCache.getWindowStorage(); - expect(Object.keys(localStorage).length).toEqual(1); // Telemetry + expect(Object.keys(localStorage).length).toEqual(2); // Telemetry expect(Object.keys(sessionStorage).length).toEqual(0); }, ONE_SECOND_IN_MS); }); diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/authConfig.js b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/authConfig.js index 3714c6dd9b..602c16b197 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/authConfig.js +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/app/wamBroker/authConfig.js @@ -1,8 +1,16 @@ +// demo usage of isPlatformBrokerAvailable API +const isPlatformBrokerAvailable = msal.isPlatformBrokerAvailable().then((isAvailable) => { + console.log(`isNativeAvailable: ${isAvailable}`); + return isAvailable; +}).catch((error) => { + console.error("Error checking if platform broker is available:", error); +}); + // Config object to be passed to Msal on creation const msalConfig = { auth: { - clientId: "b5c2e510-4a17-4feb-b219-e55aa5b74144", - authority: "https://login.microsoftonline.com/common" + clientId: "591ddbcc-105b-42c5-89e6-c7638c4124d4", + authority: "https://login.microsoftonline.com/f645ad92-e38d-4d1a-b510-d1b09a74a8ca" }, cache: { cacheLocation: "sessionStorage", // This configures where your cache will be stored diff --git a/samples/msal-browser-samples/VanillaJSTestApp2.0/tsconfig.json b/samples/msal-browser-samples/VanillaJSTestApp2.0/tsconfig.json index 5e6695a0fa..faa7ec0463 100644 --- a/samples/msal-browser-samples/VanillaJSTestApp2.0/tsconfig.json +++ b/samples/msal-browser-samples/VanillaJSTestApp2.0/tsconfig.json @@ -5,9 +5,9 @@ "module": "commonjs", "target": "es5", "lib": [ - "es2015", + "es2021", "dom", - "es2015.promise" + "es2021.promise" ], "allowUnusedLabels": false, "noImplicitReturns": true, diff --git a/samples/msal-browser-samples/vue3-sample-app/package.json b/samples/msal-browser-samples/vue3-sample-app/package.json index c7e5889d06..bd174c8404 100644 --- a/samples/msal-browser-samples/vue3-sample-app/package.json +++ b/samples/msal-browser-samples/vue3-sample-app/package.json @@ -16,7 +16,7 @@ "devDependencies": { "@vitejs/plugin-vue": "^3.1.2", "typescript": "^4.8.4", - "vite": "^4.5.6", + "vite": "^4.5.14", "vue-tsc": "^1.0.8" } } diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/package.json b/samples/msal-node-samples/ElectronSystemBrowserTestApp/package.json index e72377bb67..8e5746b6da 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/package.json +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/package.json @@ -31,8 +31,8 @@ "@electron/asar": "^3.2.3", "@playwright/test": "^1.31.1", "@types/jest": "^29.5.0", - "@types/react": "^18.0.28", - "@types/react-dom": "^18.0.11", + "@types/react": "^19.1.3", + "@types/react-dom": "^19.1.3", "@typescript-eslint/eslint-plugin": "^5.0.0", "@typescript-eslint/parser": "^5.0.0", "@vercel/webpack-asset-relocator-loader": "1.7.3", @@ -57,12 +57,12 @@ }, "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", "bootstrap": "^5.0.0", "electron-squirrel-startup": "^1.0.0", - "react": "^18.2.0", + "react": "^19.1.0", "react-bootstrap": "^2.7.2", - "react-dom": "^18.2.0", + "react-dom": "^19.1.0", "react-router-dom": "^6.9.0" }, "config": { diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/components/PageLayout.tsx b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/components/PageLayout.tsx index 194d07c982..c44743a012 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/components/PageLayout.tsx +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/components/PageLayout.tsx @@ -4,7 +4,7 @@ import { NavigationBar } from "./NavigationBar"; type PageLayoutProps = { account: AccountInfo; - children: JSX.Element; + children: React.JSX.Element; }; export const PageLayout = (props: PageLayoutProps) => { diff --git a/samples/msal-node-samples/ElectronTestApp/package.json b/samples/msal-node-samples/ElectronTestApp/package.json index 22e4e26e48..a9e6ef2f86 100644 --- a/samples/msal-node-samples/ElectronTestApp/package.json +++ b/samples/msal-node-samples/ElectronTestApp/package.json @@ -24,6 +24,6 @@ }, "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4" + "axios": "^1.9.0" } } diff --git a/samples/msal-node-samples/Managed-Identity/FIC/.npmrc b/samples/msal-node-samples/Managed-Identity/FIC/.npmrc new file mode 100644 index 0000000000..43c97e719a --- /dev/null +++ b/samples/msal-node-samples/Managed-Identity/FIC/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/samples/msal-node-samples/Managed-Identity/FIC/package.json b/samples/msal-node-samples/Managed-Identity/FIC/package.json index f8666bee56..2d1fc68501 100644 --- a/samples/msal-node-samples/Managed-Identity/FIC/package.json +++ b/samples/msal-node-samples/Managed-Identity/FIC/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "@azure/msal-node": "^3.0.0", - "dotenv": "^16.4.5" + "dotenv": "^16.5.0" }, "devDependencies": { "typescript": "^5.3.3" diff --git a/samples/msal-node-samples/Managed-Identity/Imds/.npmrc b/samples/msal-node-samples/Managed-Identity/Imds/.npmrc new file mode 100644 index 0000000000..43c97e719a --- /dev/null +++ b/samples/msal-node-samples/Managed-Identity/Imds/.npmrc @@ -0,0 +1 @@ +package-lock=false diff --git a/samples/msal-node-samples/Managed-Identity/Imds/package.json b/samples/msal-node-samples/Managed-Identity/Imds/package.json index 7b9281b523..66fd3c1c04 100644 --- a/samples/msal-node-samples/Managed-Identity/Imds/package.json +++ b/samples/msal-node-samples/Managed-Identity/Imds/package.json @@ -8,7 +8,7 @@ }, "dependencies": { "@azure/msal-node": "^3.0.0", - "dotenv": "^16.4.5" + "dotenv": "^16.5.0" }, "devDependencies": { "typescript": "^5.3.3" diff --git a/samples/msal-node-samples/auth-code-cli-app/package.json b/samples/msal-node-samples/auth-code-cli-app/package.json index 57810107e7..6a7c21dc33 100644 --- a/samples/msal-node-samples/auth-code-cli-app/package.json +++ b/samples/msal-node-samples/auth-code-cli-app/package.json @@ -14,7 +14,7 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", "open": "^8.4.0" }, "devDependencies": { diff --git a/samples/msal-node-samples/auth-code-distributed-cache/package.json b/samples/msal-node-samples/auth-code-distributed-cache/package.json index 53316d89a9..2e133df571 100644 --- a/samples/msal-node-samples/auth-code-distributed-cache/package.json +++ b/samples/msal-node-samples/auth-code-distributed-cache/package.json @@ -16,7 +16,6 @@ "@types/express": "^4.17.17", "@types/express-session": "^1.17.6", "@types/node": "^18.14.1", - "dotenv": "^16.0.3", "nodemon": "^2.0.20", "rimraf": "^4.1.2", "ts-node": "^10.9.1", @@ -24,8 +23,9 @@ }, "dependencies": { "@azure/msal-node": "^1.15.0", - "axios": "^1.7.4", + "axios": "^1.9.0", "connect-redis": "^7.0.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-session": "^1.17.3", "hbs": "^4.2.0", diff --git a/samples/msal-node-samples/auth-code-key-vault/package.json b/samples/msal-node-samples/auth-code-key-vault/package.json index 0dc66ac577..0e481dd404 100644 --- a/samples/msal-node-samples/auth-code-key-vault/package.json +++ b/samples/msal-node-samples/auth-code-key-vault/package.json @@ -16,6 +16,7 @@ "@azure/identity": "^3.4.2", "@azure/keyvault-certificates": "^4.1.0", "@azure/keyvault-secrets": "^4.1.0", + "dotenv": "^16.5.0", "express": "^4.20.0" } -} \ No newline at end of file +} diff --git a/samples/msal-node-samples/auth-code-pkce/package.json b/samples/msal-node-samples/auth-code-pkce/package.json index adb061a968..1b8a6cb03d 100644 --- a/samples/msal-node-samples/auth-code-pkce/package.json +++ b/samples/msal-node-samples/auth-code-pkce/package.json @@ -15,6 +15,7 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-session": "^1.17.2" }, @@ -22,7 +23,6 @@ "@types/express": "^4.17.13", "@types/express-session": "^1.17.4", "@types/node": "^16.10.1", - "dotenv": "^16.3.1", "typescript": "^4.4.3" } } diff --git a/samples/msal-node-samples/auth-code-with-certs/package.json b/samples/msal-node-samples/auth-code-with-certs/package.json index bced00afb5..0018ff4eb5 100644 --- a/samples/msal-node-samples/auth-code-with-certs/package.json +++ b/samples/msal-node-samples/auth-code-with-certs/package.json @@ -13,7 +13,8 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "local": "^0.3.3" } -} \ No newline at end of file +} diff --git a/samples/msal-node-samples/auth-code/package.json b/samples/msal-node-samples/auth-code/package.json index 237b917d47..6b525c8815 100644 --- a/samples/msal-node-samples/auth-code/package.json +++ b/samples/msal-node-samples/auth-code/package.json @@ -15,12 +15,12 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-session": "^1.17.2" }, "devDependencies": { "@types/jest": "^29.5.0", - "dotenv": "^16.3.1", "e2e-test-utils": "^0.0.1", "jest": "^29.5.0", "jest-junit": "^16.0.0", diff --git a/samples/msal-node-samples/b2c-user-flows/package.json b/samples/msal-node-samples/b2c-user-flows/package.json index 39afdf2c8a..dc02dfd7a3 100644 --- a/samples/msal-node-samples/b2c-user-flows/package.json +++ b/samples/msal-node-samples/b2c-user-flows/package.json @@ -14,14 +14,14 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-handlebars": "^5.3.5", "express-session": "^1.17.2" }, "devDependencies": { "@types/jest": "^29.5.0", - "dotenv": "^16.3.1", "e2e-test-utils": "^0.0.1", "jest": "^29.5.0", "jest-junit": "^16.0.0", diff --git a/samples/msal-node-samples/client-credentials-distributed-cache/package.json b/samples/msal-node-samples/client-credentials-distributed-cache/package.json index e1967c38eb..27bc97311e 100644 --- a/samples/msal-node-samples/client-credentials-distributed-cache/package.json +++ b/samples/msal-node-samples/client-credentials-distributed-cache/package.json @@ -14,7 +14,6 @@ "license": "ISC", "devDependencies": { "@types/node": "^18.14.1", - "dotenv": "^16.0.3", "nodemon": "^2.0.20", "rimraf": "^4.1.2", "ts-node": "^10.9.1", @@ -22,9 +21,10 @@ }, "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", + "dotenv": "^16.5.0", "open": "^8.4.2", "redis": "^4.6.5", "yargs": "^17.7.1" } -} \ No newline at end of file +} diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/README.md b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/README.md index 4faa882fbc..c93c7c8b3e 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/README.md +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/README.md @@ -37,28 +37,24 @@ Locate the folder where `package.json` resides in your terminal. Then type: - Select the **Add permissions** button at the bottom. - Finally, grant **admin consent** for this scope. -Before running the sample, you will need to replace the values in retrieve-cert-from-key-vault code as well as the configuration object: +Before running the sample, you will need to replace the values in retrieve-cert-from-key-vault code as well as create a .env file: ```typescript const keyVaultSecretClient = await getKeyVaultSecretClient( - "ENTER_KEY_VAULT_URL" // optional, the "KEY_VAULT_URL" environment variable can be set instead + "KEY_VAULT_URL" // optional, the "KEY_VAULT_URL" environment variable can be set instead ); [thumbprint, privateKey, x5c] = await getCertificateInfo( keyVaultSecretClient, - "ENTER_CERT_NAME" + "CERT_NAME" // optional, the "CERT_NAME" environment variable can be set instead ); +``` -config = { - auth: { - clientId: "ENTER_CLIENT_ID", - authority: "https://login.microsoftonline.com/ENTER_TENANT_INFO", - clientCertificate: { - thumbprintSha256: thumbprint, - privateKey: privateKey, - x5c: x5c, - }, - }, -}; +``` +CLIENT_ID=YOUR_CLIENT_ID_HERE +TENANT_ID=YOUR_TENANT_ID_HERE +CLIENT_CERTIFICATE_THUMBPRINT_SHA_256=YOUR_CLIENT_CERTIFICATE_THUMBPRINT_SHA_256_HERE +CLIENT_CERTIFICATE_PRIVATE_KEY=YOUR_CLIENT_CERTIFICATE_PRIVATE_KEY_HERE +CLIENT_CERTIFICATE_X5C=YOUR_CLIENT_CERTIFICATE_X5C_HERE ``` ## Run the app @@ -74,7 +70,7 @@ This will compile the TypeScript into JavaScript, and put the compiled files in The sample can now be run by typing: ```console - node dist/client-credentials-with-cert-from-key-vault/app.js + node dist/app.js ``` An npm script, which will run the above npx and node command, has been configured in package.json. To compile and start the sample, type: diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/app.ts b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/app.ts index 1136e10242..07e47fc940 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/app.ts +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/app.ts @@ -9,7 +9,8 @@ import { Configuration, LogLevel, } from "@azure/msal-node"; -import argv from "../cliArgs"; // command line arguments - see samples/msal-node-samples/cliArgs.ts +const argv = require("./cliArgs").default; +require("dotenv").config(); const getClientCredentialsToken = async ( cca: ConfidentialClientApplication, @@ -29,7 +30,7 @@ const getClientCredentialsToken = async ( await cca.acquireTokenByClientCredential(clientCredentialRequest); return response; } catch (error) { - throw error.errorMessage; + throw error; } }; @@ -38,18 +39,28 @@ const getClientCredentialsToken = async ( * If the script was executed manually, it will initialize a ConfidentialClientApplication object * and execute the sample client credentials application. */ -if (argv.$0 === "dist/client-credentials-with-cert-from-key-vault/app.js") { +if (argv.$0 === "dist/app.js") { (async () => { + if ( + !process.env.CLIENT_ID || + !process.env.TENANT_ID || + !process.env.CLIENT_CERTIFICATE_THUMBPRINT_SHA_256 || + !process.env.CLIENT_CERTIFICATE_PRIVATE_KEY || + !process.env.CLIENT_CERTIFICATE_X5C + ) { + throw new Error( + "Please set the environment variables CLIENT_ID, TENANT_ID, CLIENT_CERTIFICATE_THUMBPRINT_SHA_256, CLIENT_CERTIFICATE_PRIVATE_KEY, and CLIENT_CERTIFICATE_X5C." + ); + } const clientConfig: Configuration = { auth: { - clientId: "", - authority: - "https://login.microsoftonline.com/", + clientId: process.env.CLIENT_ID, + authority: `https://login.microsoftonline.com/${process.env.TENANT_ID}`, clientCertificate: { thumbprintSha256: - "", - privateKey: "ENTER_CLIENT_CERTIFICATE_PRIVATE_KEY", - x5c: "ENTER_CLIENT_CERTIFICATE_X5C", + process.env.CLIENT_CERTIFICATE_THUMBPRINT_SHA_256, + privateKey: process.env.CLIENT_CERTIFICATE_PRIVATE_KEY, + x5c: process.env.CLIENT_CERTIFICATE_X5C, }, }, system: { diff --git a/samples/msal-node-samples/cliArgs.ts b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/cliArgs.ts similarity index 58% rename from samples/msal-node-samples/cliArgs.ts rename to samples/msal-node-samples/client-credentials-with-cert-from-key-vault/cliArgs.ts index 94c0800778..972f23a72f 100644 --- a/samples/msal-node-samples/cliArgs.ts +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/cliArgs.ts @@ -1,19 +1,19 @@ -import yargs from "yargs"; +import yargs, { ArgumentsCamelCase } from "yargs"; -interface Arguments { - c: string; - p: number; - r: string | undefined; - s: string; - $0: string; +interface CliArgs extends ArgumentsCamelCase { + c?: string; + p?: number; + r?: string; + s?: string; + ro?: string; } -const argv: Arguments = yargs(process.argv.slice(2)) +const argv = yargs(process.argv.slice(2)) .usage("Usage: $0 -p [PORT]") .options({ c: { type: "string", - alias: "cache location", + alias: "cache-location", default: "data/cache.json", description: "(Optional) Cache location - default is data/cache.json", @@ -25,6 +25,7 @@ const argv: Arguments = yargs(process.argv.slice(2)) description: "(Optional) Port Number - default is 3000", }, r: { + type: "string", alias: "region", default: undefined, description: "(Optional) Region - default is undefined", @@ -35,7 +36,14 @@ const argv: Arguments = yargs(process.argv.slice(2)) default: "AAD", description: "(Optional) Scenario name - default is AAD", }, + ro: { + type: "string", + alias: "runtime-options", + default: null, + description: + "(Optional) Runtime options to inject into the application - default is null", + }, }) - .parseSync(); + .parseSync() as CliArgs; export default argv; diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/package.json b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/package.json index 58386ed158..a2d1697795 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/package.json +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/package.json @@ -7,14 +7,14 @@ "scripts": { "test:e2e": "jest", "build": "npx tsc", - "start": "npm run build && node dist/client-credentials-with-cert-from-key-vault/app.js", + "start": "npm run build && node dist/app.js", "build:package": "cd ../../.. && npm run build:all --workspace=lib/msal-node" }, - "type": "module", "author": "", "license": "MIT", "dependencies": { - "@azure/msal-node": "^3.0.0" + "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0" }, "devDependencies": { "@types/jest": "^29.5.0", diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/client-credentials-with-cert-from-key-vault-aad.spec.ts b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/client-credentials-with-cert-from-key-vault-aad.spec.ts index 84962a6a50..75ace3a87c 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/client-credentials-with-cert-from-key-vault-aad.spec.ts +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/test/client-credentials-with-cert-from-key-vault-aad.spec.ts @@ -2,13 +2,13 @@ import { RETRY_TIMES, validateCacheLocation, NodeCacheTestUtils, -} from "e2e-test-utils"; + getKeyVaultSecretClient, +} from "../../../e2eTestUtils/src"; import { AuthenticationResult, ConfidentialClientApplication, Configuration, } from "@azure/msal-node"; -import { getKeyVaultSecretClient } from "../../../e2eTestUtils/src/KeyVaultUtils"; import { getCertificateInfo } from "../../../e2eTestUtils/src/CertificateUtils"; import { ENV_VARIABLES, @@ -70,12 +70,12 @@ describe("Client Credentials AAD Prod Tests", () => { config ); - const authenticationResult: AuthenticationResult = + const authenticationResult: AuthenticationResult | null = await getClientCredentialsToken( confidentialClientApplication, clientCredentialRequestScopes ); - expect(authenticationResult.accessToken).toBeTruthy(); + expect(authenticationResult?.accessToken).toBeTruthy(); }); }); }); diff --git a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/tsconfig.json b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/tsconfig.json index e75f486295..967580f7c0 100644 --- a/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/tsconfig.json +++ b/samples/msal-node-samples/client-credentials-with-cert-from-key-vault/tsconfig.json @@ -1,7 +1,12 @@ { - "extends": "../tsconfig.json", - "files": [ - "app.ts", - "../cliArgs.ts" - ], -} \ No newline at end of file + "extends": "../tsconfig.json", + "compilerOptions": { + "target": "ES2020", + "outDir": "./dist", + "skipLibCheck": true + }, + "include": [ + "./app.ts", + "./cliArgs.ts" + ] +} diff --git a/samples/msal-node-samples/client-credentials/README.md b/samples/msal-node-samples/client-credentials/README.md index effbb4884a..c2e87752cd 100644 --- a/samples/msal-node-samples/client-credentials/README.md +++ b/samples/msal-node-samples/client-credentials/README.md @@ -43,11 +43,17 @@ const config = { auth: { clientId: "ENTER_CLIENT_ID", authority: "https://login.microsoftonline.com/ENTER_TENANT_INFO", - clientSecret: "ENTER_CLIENT_SECRET", + process.env.CLIENT_SECRET, }, }; ``` +You will also need to add the client secret to your **.env file**: + +``` +CLIENT_SECRET= +``` + ## Run the app In the same folder, type: diff --git a/samples/msal-node-samples/client-credentials/index.js b/samples/msal-node-samples/client-credentials/index.js index 19239033d3..cb3e237fc6 100644 --- a/samples/msal-node-samples/client-credentials/index.js +++ b/samples/msal-node-samples/client-credentials/index.js @@ -16,6 +16,8 @@ const argv = require("../cliArgs"); const cacheLocation = argv.c || "./data/cache.json"; const cachePlugin = require('../cachePlugin')(cacheLocation); +require('dotenv').config(); + /** * The scenario string is the name of a .json file which contains the MSAL client configuration * For an example of what a configuration file should look like, check out the customConfig.json file in the @@ -55,7 +57,11 @@ if(argv.$0 === "index.js") { // Build MSAL ClientApplication Configuration object const clientConfig = { - auth: config.authOptions, + auth: { + clientId: "ENTER_CLIENT_ID", + authority: "https://login.microsoftonline.com/ENTER_TENANT_INFO", + clientSecret: process.env.CLIENT_SECRET, + }, cache: { cachePlugin }, diff --git a/samples/msal-node-samples/client-credentials/package.json b/samples/msal-node-samples/client-credentials/package.json index 920da5d9ee..0f1ee8a13a 100644 --- a/samples/msal-node-samples/client-credentials/package.json +++ b/samples/msal-node-samples/client-credentials/package.json @@ -14,7 +14,8 @@ "author": "", "license": "MIT", "dependencies": { - "@azure/msal-node": "^3.0.0" + "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0" }, "devDependencies": { "@types/jest": "^29.5.0", diff --git a/samples/msal-node-samples/on-behalf-of-distributed-cache/package.json b/samples/msal-node-samples/on-behalf-of-distributed-cache/package.json index 05e526f489..3d4e663d05 100644 --- a/samples/msal-node-samples/on-behalf-of-distributed-cache/package.json +++ b/samples/msal-node-samples/on-behalf-of-distributed-cache/package.json @@ -16,7 +16,6 @@ "@types/express": "^4.17.17", "@types/jsonwebtoken": "^9.0.1", "@types/node": "^18.14.1", - "dotenv": "^16.0.3", "nodemon": "^2.0.20", "rimraf": "^4.1.2", "ts-node": "^10.9.1", @@ -24,10 +23,11 @@ }, "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.0.1", "redis": "^4.6.5" } -} \ No newline at end of file +} diff --git a/samples/msal-node-samples/on-behalf-of/package.json b/samples/msal-node-samples/on-behalf-of/package.json index 424deab420..d93eced0ea 100644 --- a/samples/msal-node-samples/on-behalf-of/package.json +++ b/samples/msal-node-samples/on-behalf-of/package.json @@ -13,7 +13,7 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", - "dotenv": "^16.0.3", + "dotenv": "^16.5.0", "express": "^4.20.0", "jsonwebtoken": "^9.0.0", "jwks-rsa": "^3.0.1", diff --git a/samples/msal-node-samples/refresh-token/package.json b/samples/msal-node-samples/refresh-token/package.json index 0944533fc2..a55745d36e 100644 --- a/samples/msal-node-samples/refresh-token/package.json +++ b/samples/msal-node-samples/refresh-token/package.json @@ -14,11 +14,11 @@ "@azure/msal-node": "^3.0.0", "adal-node": "^0.2.3", "cookie-parser": "^1.4.6", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-session": "^1.17.2", "underscore": "^1.13.3" }, "devDependencies": { - "dotenv": "^16.3.1" } -} \ No newline at end of file +} diff --git a/samples/msal-node-samples/silent-flow/package.json b/samples/msal-node-samples/silent-flow/package.json index e44d692340..872e9a40e8 100644 --- a/samples/msal-node-samples/silent-flow/package.json +++ b/samples/msal-node-samples/silent-flow/package.json @@ -14,14 +14,14 @@ "license": "MIT", "dependencies": { "@azure/msal-node": "^3.0.0", - "axios": "^1.7.4", + "axios": "^1.9.0", + "dotenv": "^16.5.0", "express": "^4.20.0", "express-handlebars": "^5.3.5", "express-promise-router": "^4.0.1" }, "devDependencies": { "@types/jest": "^29.5.0", - "dotenv": "^16.3.1", "e2e-test-utils": "^0.0.1", "jest": "^29.5.0", "jest-junit": "^16.0.0", diff --git a/samples/msal-node-samples/tsconfig.json b/samples/msal-node-samples/tsconfig.json index 1609257cb5..60f21f5532 100644 --- a/samples/msal-node-samples/tsconfig.json +++ b/samples/msal-node-samples/tsconfig.json @@ -6,7 +6,7 @@ "lib": [ "ES2021", "dom", - "es2015.promise" + "ES2021.Promise" ], "allowUnusedLabels": false, "noImplicitReturns": true, diff --git a/samples/msal-node-samples/username-password-cca/package.json b/samples/msal-node-samples/username-password-cca/package.json index bc06b25edd..bf263f7e94 100644 --- a/samples/msal-node-samples/username-password-cca/package.json +++ b/samples/msal-node-samples/username-password-cca/package.json @@ -12,9 +12,9 @@ "author": "Microsoft", "license": "MIT", "dependencies": { - "@azure/msal-node": "^3.0.0" + "@azure/msal-node": "^3.0.0", + "dotenv": "^16.5.0" }, "devDependencies": { - "dotenv": "^16.3.1" } } diff --git a/samples/msal-react-samples/b2c-sample/package.json b/samples/msal-react-samples/b2c-sample/package.json index bf9b8d3418..0f6531b83f 100644 --- a/samples/msal-react-samples/b2c-sample/package.json +++ b/samples/msal-react-samples/b2c-sample/package.json @@ -9,8 +9,8 @@ "@emotion/styled": "^11.9.3", "@mui/icons-material": "^5.8.4", "@mui/material": "^5.9.0", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-router-dom": "^6.7.0" }, "devDependencies": { diff --git a/samples/msal-react-samples/b2c-sample/src/ui-components/WelcomeName.jsx b/samples/msal-react-samples/b2c-sample/src/ui-components/WelcomeName.jsx index 718ccf97b1..13d83df587 100644 --- a/samples/msal-react-samples/b2c-sample/src/ui-components/WelcomeName.jsx +++ b/samples/msal-react-samples/b2c-sample/src/ui-components/WelcomeName.jsx @@ -1,12 +1,13 @@ import { useEffect, useState } from "react"; import { useMsal } from "@azure/msal-react"; +import { InteractionStatus } from "@azure/msal-browser"; import Typography from "@mui/material/Typography"; const WelcomeName = () => { - const { instance } = useMsal(); + const { instance, inProgress } = useMsal(); const [name, setName] = useState(null); - const activeAccount = instance.getActiveAccount(); + const activeAccount = inProgress === InteractionStatus.None ? instance.getActiveAccount() : null; useEffect(() => { if (activeAccount && activeAccount.name) { setName(activeAccount.name.split(' ')[0]); diff --git a/samples/msal-react-samples/b2c-sample/test/msa-account.spec.ts b/samples/msal-react-samples/b2c-sample/test/msa-account.spec.ts index f49d230b42..bc546f233b 100644 --- a/samples/msal-react-samples/b2c-sample/test/msa-account.spec.ts +++ b/samples/msal-react-samples/b2c-sample/test/msa-account.spec.ts @@ -107,15 +107,16 @@ describe("B2C user-flow tests (msa account)", () => { const editProfileButton = await page.waitForSelector( "#editProfileButton" ); - if (editProfileButton) { - await editProfileButton.click(); - } + await editProfileButton.click(); let displayName = (Math.random() + 1).toString(36).substring(7); // generate a random string await page.waitForNavigation(); + await screenshot.takeScreenshot(page, "Edit profile button clicked"); await page.waitForSelector("#attributeVerification", { visible: true }); - await page.$eval("#displayName", (el: any) => (el.value = "")), // clear the text field - await page.type("#displayName", `${displayName}`), - await page.click("#continue"); + await page.$eval("#displayName", (el: any) => (el.value = "")); // clear the text field + await page.type("#displayName", `${displayName}`); + await page.click("#continue"); + await page.waitForNetworkIdle(); + await screenshot.takeScreenshot(page, "Edit profile page filled"); await Promise.all([ page.waitForFunction( `window.location.href.startsWith("http://localhost:${port}")` diff --git a/samples/msal-react-samples/nextjs-sample/next-env.d.ts b/samples/msal-react-samples/nextjs-sample/next-env.d.ts index 4f11a03dc6..52e831b434 100644 --- a/samples/msal-react-samples/nextjs-sample/next-env.d.ts +++ b/samples/msal-react-samples/nextjs-sample/next-env.d.ts @@ -2,4 +2,4 @@ /// // NOTE: This file should not be edited -// see https://nextjs.org/docs/basic-features/typescript for more information. +// see https://nextjs.org/docs/pages/api-reference/config/typescript for more information. diff --git a/samples/msal-react-samples/nextjs-sample/package.json b/samples/msal-react-samples/nextjs-sample/package.json index b5c4fe7c80..9bf7cbb034 100644 --- a/samples/msal-react-samples/nextjs-sample/package.json +++ b/samples/msal-react-samples/nextjs-sample/package.json @@ -18,9 +18,9 @@ "@emotion/styled": "^11.10.5", "@mui/icons-material": "^5.10.14", "@mui/material": "^5.10.14", - "next": "^14.2.10", - "react": "^18.2.0", - "react-dom": "^18.2.0" + "next": "^15.3.3", + "react": "^19.1.0", + "react-dom": "^19.1.0" }, "devDependencies": { "@types/jest": "^29.5.2", diff --git a/samples/msal-react-samples/nextjs-sample/src/ui-components/WelcomeName.jsx b/samples/msal-react-samples/nextjs-sample/src/ui-components/WelcomeName.jsx index 718ccf97b1..13d83df587 100644 --- a/samples/msal-react-samples/nextjs-sample/src/ui-components/WelcomeName.jsx +++ b/samples/msal-react-samples/nextjs-sample/src/ui-components/WelcomeName.jsx @@ -1,12 +1,13 @@ import { useEffect, useState } from "react"; import { useMsal } from "@azure/msal-react"; +import { InteractionStatus } from "@azure/msal-browser"; import Typography from "@mui/material/Typography"; const WelcomeName = () => { - const { instance } = useMsal(); + const { instance, inProgress } = useMsal(); const [name, setName] = useState(null); - const activeAccount = instance.getActiveAccount(); + const activeAccount = inProgress === InteractionStatus.None ? instance.getActiveAccount() : null; useEffect(() => { if (activeAccount && activeAccount.name) { setName(activeAccount.name.split(' ')[0]); diff --git a/samples/msal-react-samples/nextjs-sample/test/profile.spec.ts b/samples/msal-react-samples/nextjs-sample/test/profile.spec.ts index 39773095ba..0896c494a9 100644 --- a/samples/msal-react-samples/nextjs-sample/test/profile.spec.ts +++ b/samples/msal-react-samples/nextjs-sample/test/profile.spec.ts @@ -24,7 +24,7 @@ async function verifyTokenStore( "b5c2e510-4a17-4feb-b219-e55aa5b74144" ); expect(telemetryCacheEntry).not.toBeNull; - expect(telemetryCacheEntry["cacheHits"]).toBe(1); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); } describe("/profile", () => { diff --git a/samples/msal-react-samples/react-router-sample/package.json b/samples/msal-react-samples/react-router-sample/package.json index 56f4ac8c2f..3eed7ef3a0 100644 --- a/samples/msal-react-samples/react-router-sample/package.json +++ b/samples/msal-react-samples/react-router-sample/package.json @@ -9,8 +9,8 @@ "@emotion/styled": "^11.10.5", "@mui/icons-material": "^5.10.16", "@mui/material": "^5.10.17", - "react": "^18.0.0", - "react-dom": "^18.0.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-router-dom": "^6.7.0", "react-scripts": "^5.0.1" }, diff --git a/samples/msal-react-samples/react-router-sample/src/ui-components/WelcomeName.jsx b/samples/msal-react-samples/react-router-sample/src/ui-components/WelcomeName.jsx index a9ca901882..3efd6c884e 100644 --- a/samples/msal-react-samples/react-router-sample/src/ui-components/WelcomeName.jsx +++ b/samples/msal-react-samples/react-router-sample/src/ui-components/WelcomeName.jsx @@ -1,12 +1,13 @@ import { useEffect, useState } from "react"; import { useMsal } from "@azure/msal-react"; +import { InteractionStatus } from "@azure/msal-browser"; import Typography from "@mui/material/Typography"; const WelcomeName = () => { - const { instance } = useMsal(); + const { instance, inProgress } = useMsal(); const [name, setName] = useState(null); - const activeAccount = instance.getActiveAccount(); + const activeAccount = inProgress === InteractionStatus.None ? instance.getActiveAccount() : null; useEffect(() => { if (activeAccount) { setName(activeAccount.name.split(' ')[0]); diff --git a/samples/msal-react-samples/react-router-sample/test/profile.spec.ts b/samples/msal-react-samples/react-router-sample/test/profile.spec.ts index 03c6e612f4..e38be2d133 100644 --- a/samples/msal-react-samples/react-router-sample/test/profile.spec.ts +++ b/samples/msal-react-samples/react-router-sample/test/profile.spec.ts @@ -24,7 +24,7 @@ async function verifyTokenStore( "b5c2e510-4a17-4feb-b219-e55aa5b74144" ); expect(telemetryCacheEntry).not.toBeNull; - expect(telemetryCacheEntry["cacheHits"]).toBe(1); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); } describe("/profile", () => { diff --git a/samples/msal-react-samples/react-router-sample/test/profileRawContext.spec.ts b/samples/msal-react-samples/react-router-sample/test/profileRawContext.spec.ts index 40ecfb15d4..daca110064 100644 --- a/samples/msal-react-samples/react-router-sample/test/profileRawContext.spec.ts +++ b/samples/msal-react-samples/react-router-sample/test/profileRawContext.spec.ts @@ -24,7 +24,7 @@ async function verifyTokenStore( "b5c2e510-4a17-4feb-b219-e55aa5b74144" ); expect(telemetryCacheEntry).not.toBeNull; - expect(telemetryCacheEntry["cacheHits"]).toBe(1); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); } describe("/profileRawContext", () => { diff --git a/samples/msal-react-samples/react-router-sample/test/profileWithMsal.spec.ts b/samples/msal-react-samples/react-router-sample/test/profileWithMsal.spec.ts index 11fa315bc0..8c1d824994 100644 --- a/samples/msal-react-samples/react-router-sample/test/profileWithMsal.spec.ts +++ b/samples/msal-react-samples/react-router-sample/test/profileWithMsal.spec.ts @@ -24,7 +24,7 @@ async function verifyTokenStore( "b5c2e510-4a17-4feb-b219-e55aa5b74144" ); expect(telemetryCacheEntry).not.toBeNull; - expect(telemetryCacheEntry["cacheHits"]).toBe(1); + expect(telemetryCacheEntry["cacheHits"]).toBeGreaterThanOrEqual(1); } describe("/profileWithMsal", () => { diff --git a/samples/msal-react-samples/tsconfig.json b/samples/msal-react-samples/tsconfig.json index bafed899c0..3a5538f7d5 100644 --- a/samples/msal-react-samples/tsconfig.json +++ b/samples/msal-react-samples/tsconfig.json @@ -4,9 +4,9 @@ "module": "commonjs", "target": "es5", "lib": [ - "es2015", + "ES2021", "dom", - "es2015.promise" + "ES2021.Promise" ], "allowUnusedLabels": false, "noImplicitReturns": true, diff --git a/samples/msal-react-samples/typescript-sample/README.md b/samples/msal-react-samples/typescript-sample/README.md index 676f9bd06b..fc1415ad94 100644 --- a/samples/msal-react-samples/typescript-sample/README.md +++ b/samples/msal-react-samples/typescript-sample/README.md @@ -8,7 +8,7 @@ This sample was bootstrapped with [Create React App](https://github.com/facebook ## Notable files and what they demonstrate 1. `./src/App.tsx` - Shows implementation of `MsalProvider`, all children will have access to `@azure/msal-react` context, hooks and components. -1. `./src/index.tsx` - Shows intialization of the `PublicClientApplication` that is passed to `App.js` +1. `./src/index.tsx` - Shows intialization of the `PublicClientApplication` that is passed to `App.tsx` 1. `./src/pages/Home.tsx` - Homepage, shows how to conditionally render content using `AuthenticatedTemplate` and `UnauthenticatedTemplate` depending on whether or not a user is signed in. 1. `./src/pages/Profile.tsx` - Example of a protected route using `MsalAuthenticationTemplate`. If a user is not yet signed in, signin will be invoked automatically. If a user is signed in it will acquire an access token and make a call to MS Graph to fetch user profile data. 1. `./src/authConfig.ts` - Configuration options for `PublicClientApplication` and token requests. @@ -68,4 +68,4 @@ You will also see any lint errors in the console. - [TypeScript documentation](https://www.typescriptlang.org/docs/) - [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started) - [React Router documentation](https://reactrouter.com/web/guides/quick-start) -- [Material-UI documentation](https://material-ui.com/getting-started/installation/) \ No newline at end of file +- [Material-UI documentation](https://material-ui.com/getting-started/installation/) diff --git a/samples/msal-react-samples/typescript-sample/package.json b/samples/msal-react-samples/typescript-sample/package.json index 22e5dc9967..ff3a0f4d49 100644 --- a/samples/msal-react-samples/typescript-sample/package.json +++ b/samples/msal-react-samples/typescript-sample/package.json @@ -10,11 +10,11 @@ "@mui/icons-material": "^5.10.16", "@mui/material": "^5.10.17", "@types/node": "^16.18.10", - "@types/react": "^18.0.26", - "@types/react-dom": "^18.0.9", + "@types/react": "^19.1.3", + "@types/react-dom": "^19.1.3", "@types/react-router-dom": "^5.1.7", - "react": "^18.2.0", - "react-dom": "^18.2.0", + "react": "^19.1.0", + "react-dom": "^19.1.0", "react-router-dom": "^6.7.0", "react-scripts": "5.0.1", "typescript": "^4.9.4" @@ -50,5 +50,5 @@ "suiteNameTemplate": "TypeScript Sample Tests", "outputDirectory": ".", "outputName": "test-results.xml" - } + } } diff --git a/samples/msal-react-samples/typescript-sample/test/profile.spec.ts b/samples/msal-react-samples/typescript-sample/test/profile.spec.ts index 200f08df30..712fdb5cf5 100644 --- a/samples/msal-react-samples/typescript-sample/test/profile.spec.ts +++ b/samples/msal-react-samples/typescript-sample/test/profile.spec.ts @@ -34,7 +34,7 @@ async function verifyTokenStore( "b5c2e510-4a17-4feb-b219-e55aa5b74144" ); expect(telemetryCacheEntry).not.toBeNull; - expect(telemetryCacheEntry!.cacheHits).toBe(1); + expect(telemetryCacheEntry!.cacheHits).toBeGreaterThanOrEqual(1); } describe("/profile", () => { diff --git a/shared-configs/eslint-config-msal/index.js b/shared-configs/eslint-config-msal/index.js index 82b7c4824c..fedd338448 100644 --- a/shared-configs/eslint-config-msal/index.js +++ b/shared-configs/eslint-config-msal/index.js @@ -81,6 +81,7 @@ module.exports = { ], "import/first": 2, "import/no-commonjs": 2, + "import/no-cycle": 2, "import/no-duplicates": 2, "import/no-extraneous-dependencies": 2, "import/no-unresolved": 2, diff --git a/shared-configs/jest-config/jest.config.cjs b/shared-configs/jest-config/jest.config.cjs index ebab6be377..6a029c0422 100644 --- a/shared-configs/jest-config/jest.config.cjs +++ b/shared-configs/jest-config/jest.config.cjs @@ -8,6 +8,7 @@ const path = require("path"); module.exports = { preset: "ts-jest", resolver: "ts-jest-resolver", + reporters: ['default', 'jest-junit'], testEnvironment: "jsdom", testEnvironmentOptions: { url: "https://localhost:8081/index.html" diff --git a/shared-configs/jest-config/setupGlobals.cjs b/shared-configs/jest-config/setupGlobals.cjs index e9c9e78f5b..a4a36f9ed6 100644 --- a/shared-configs/jest-config/setupGlobals.cjs +++ b/shared-configs/jest-config/setupGlobals.cjs @@ -5,7 +5,7 @@ const crypto = require("crypto"); const { TextDecoder, TextEncoder } = require("util"); -const { BroadcastChannel } = require("worker_threads"); +const { BroadcastChannel, MessageChannel } = require("worker_threads"); try { Object?.defineProperties(global.self, { @@ -28,6 +28,9 @@ try { }, "BroadcastChannel": { value: BroadcastChannel + }, + "MessageChannel": { + value: MessageChannel } }); } catch (e) { From 680a359f13e90e3333aa881aea90b33db9c5d343 Mon Sep 17 00:00:00 2001 From: Lalima Sharda Date: Fri, 18 Jul 2025 11:31:11 -0700 Subject: [PATCH 3/7] updated package-lock --- package-lock.json | 42 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/package-lock.json b/package-lock.json index 4136bcaf24..4bad2eaf1c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -59730,6 +59730,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", From 8135ca1046501abd9fd475bc910899b44c4a1cb6 Mon Sep 17 00:00:00 2001 From: Lalima Sharda Date: Mon, 21 Jul 2025 12:17:52 -0700 Subject: [PATCH 4/7] testing coop headers --- .../VanillaJSTestApp2.0/app/default/authConfig.js | 2 +- samples/msal-browser-samples/popup-coop/app/auth.js | 2 +- samples/msal-browser-samples/popup-coop/server.js | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) 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/app/auth.js b/samples/msal-browser-samples/popup-coop/app/auth.js index c21fee1024..9220215e4b 100644 --- a/samples/msal-browser-samples/popup-coop/app/auth.js +++ b/samples/msal-browser-samples/popup-coop/app/auth.js @@ -48,7 +48,7 @@ async function signIn(method) { if (signInType === "popup") { return myMSALObj.loginPopup({ ...loginRequest, - redirectUri: "http://localhost:30662" + // redirectUri: "http://localhost:30662" }).then(handleResponse).catch(function (error) { console.log(error); }); diff --git a/samples/msal-browser-samples/popup-coop/server.js b/samples/msal-browser-samples/popup-coop/server.js index 3f481516f5..dddc66a1a0 100644 --- a/samples/msal-browser-samples/popup-coop/server.js +++ b/samples/msal-browser-samples/popup-coop/server.js @@ -42,9 +42,9 @@ app.use("/lib", express.static(path.join(__dirname, "../../../lib/msal-browser/l // })); app.use(express.static('app/', { - setHeaders: (res) => { - res.set('Cross-Origin-Opener-Policy', 'same-origin-allow-popups'); - } + // setHeaders: (res) => { + // res.set('Cross-Origin-Opener-Policy', 'same-origin-allow-popups'); + // } })); if (logHttpRequests) { @@ -55,12 +55,12 @@ if (logHttpRequests) { // 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")); + res.sendFile(path.join(APP_DIR + "/redirect.html")); }); // Set up a route for index.html. app.get('*', function (req, res) { - res.sendFile(path.join(__dirname + '/index.html')); + res.sendFile(path.join(APP_DIR + '/index.html')); }); // Start the server. From 3dad8eb369ac22ef9843cddb36772642c98587f6 Mon Sep 17 00:00:00 2001 From: Lalima Sharda Date: Wed, 23 Jul 2025 16:36:05 -0700 Subject: [PATCH 5/7] fixed sample to test coop headers with ATPopup --- .../msal-browser-samples/popup-coop/README.md | 60 +++++++++++++++++++ .../popup-coop/app/authConfig.js | 1 + .../msal-browser-samples/popup-coop/server.js | 13 +--- 3 files changed, 64 insertions(+), 10 deletions(-) create mode 100644 samples/msal-browser-samples/popup-coop/README.md 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/authConfig.js b/samples/msal-browser-samples/popup-coop/app/authConfig.js index 07fecb7d68..0605e1049c 100644 --- a/samples/msal-browser-samples/popup-coop/app/authConfig.js +++ b/samples/msal-browser-samples/popup-coop/app/authConfig.js @@ -10,6 +10,7 @@ const msalConfig = { 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) => { diff --git a/samples/msal-browser-samples/popup-coop/server.js b/samples/msal-browser-samples/popup-coop/server.js index dddc66a1a0..edab82fd17 100644 --- a/samples/msal-browser-samples/popup-coop/server.js +++ b/samples/msal-browser-samples/popup-coop/server.js @@ -33,18 +33,11 @@ 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'); - // } + setHeaders: (res) => { + res.set('Cross-Origin-Opener-Policy', 'same-origin-allow-popups'); + } })); if (logHttpRequests) { From 4e62006d039e7782dde20ba74fe9453d29e15c61 Mon Sep 17 00:00:00 2001 From: Lalima Sharda Date: Fri, 29 Aug 2025 13:02:55 -0700 Subject: [PATCH 6/7] adding certs for https --- samples/msal-browser-samples/popup-coop/package.json | 3 ++- samples/msal-browser-samples/popup-coop/server.js | 6 +++--- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/samples/msal-browser-samples/popup-coop/package.json b/samples/msal-browser-samples/popup-coop/package.json index 446af6f7de..b958a2b115 100644 --- a/samples/msal-browser-samples/popup-coop/package.json +++ b/samples/msal-browser-samples/popup-coop/package.json @@ -6,7 +6,8 @@ "main": "server.js", "scripts": { "start": "node server.js", - "test": "npx playwright test" + "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", diff --git a/samples/msal-browser-samples/popup-coop/server.js b/samples/msal-browser-samples/popup-coop/server.js index edab82fd17..524054ee65 100644 --- a/samples/msal-browser-samples/popup-coop/server.js +++ b/samples/msal-browser-samples/popup-coop/server.js @@ -36,7 +36,7 @@ app.use("/lib", express.static(path.join(__dirname, "../../../lib/msal-browser/l app.use(express.static('app/', { setHeaders: (res) => { - res.set('Cross-Origin-Opener-Policy', 'same-origin-allow-popups'); + res.set('Cross-Origin-Opener-Policy', 'unsafe-none'); } })); @@ -72,8 +72,8 @@ if (argv.https) { * 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 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); From 9e52686ca2038490b75747360067041831d23e5d Mon Sep 17 00:00:00 2001 From: Lalima Sharda Date: Thu, 25 Sep 2025 12:24:03 -0700 Subject: [PATCH 7/7] broadcast channel implementation --- .../src/error/BrowserAuthErrorCodes.ts | 2 ++ .../src/interaction_client/PopupClient.ts | 34 ++++++++++++++++--- .../StandardInteractionClient.ts | 4 +++ .../src/utils/BrowserProtocolUtils.ts | 1 + 4 files changed, 36 insertions(+), 5 deletions(-) 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; }; /**