diff --git a/debug/serve/main.ts b/debug/serve/main.ts index 492e9d21a..f9ad2ef42 100644 --- a/debug/serve/main.ts +++ b/debug/serve/main.ts @@ -1,4 +1,4 @@ -import { MSAL } from "@pnp/msaljsclient/index.js"; +import { MSAL, MSALOptions } from "@pnp/msaljsclient/index.js"; import { spfi, SPBrowser } from "@pnp/sp"; import "@pnp/sp/webs"; import { settings } from "../../settings.js"; @@ -31,8 +31,8 @@ document.onreadystatechange = async () => { // Make sure to add `https://localhost:8080/spa.html` as a Redirect URI in your testing's AAD App Registration const sp = spfi().using( SPBrowser({ baseUrl: settings.testing.sp.url}), - MSAL(settings.testing.sp.msal.init, {scopes: settings.testing.sp.msal.scopes}) - ); + MSAL({configuration:settings.testing.sp.msal.init, authParams: {scopes: settings.testing.sp.msal.scopes}}) + ); const r = await sp.web(); diff --git a/docs/concepts/auth-browser.md b/docs/concepts/auth-browser.md index 88420ec1a..383fd5804 100644 --- a/docs/concepts/auth-browser.md +++ b/docs/concepts/auth-browser.md @@ -1,42 +1,6 @@ # Authentication in a custom browser based application We support MSAL for both browser and nodejs by providing a thin wrapper around the official libraries. We won't document the fully possible MSAL configuration, but any parameters supplied are passed through to the underlying implementation. To use the browser MSAL package you'll need to install the @pnp/msaljsclient package which is deployed as a standalone due to the large MSAL dependency. +This library provides a thin wrapper around the [@azure/msal-browser](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser/docs) library to make it easy to integrate MSAL authentication in the browser. -`npm install @pnp/msaljsclient --save` - -At this time we're using version 1.x of the `msal` library which uses Implicit Flow. For more informaiton on the msal library please see the [AzureAD/microsoft-authentication-library-for-js](https://github.com/AzureAD/microsoft-authentication-library-for-js#readme). - -Each of the following samples reference a MSAL configuration that utilizes an Azure AD App Registration, these are samples that show the typings for those objects: - -```TypeScript -import { Configuration, AuthenticationParameters } from "msal"; - -const configuration: Configuration = { - auth: { - authority: "https://login.microsoftonline.com/{tenant Id}/", - clientId: "{AAD Application Id/Client Id}" - } -}; - -const authParams: AuthenticationParameters = { - scopes: ["https://graph.microsoft.com/.default"] -}; -``` - -## MSAL + Browser - -```TypeScript -import { spfi, SPBrowser } from "@pnp/sp"; -import { graphfi, GraphBrowser } from "@pnp/graph"; -import { MSAL } from "@pnp/msaljsclient"; -import "@pnp/sp/webs"; -import "@pnp/graph/users"; - -const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(SPBrowser(), MSAL(configuration, authParams)); - -// within a webpart, application customizer, or adaptive card extension where the context object is available -const graph = graphfi().using(GraphBrowser(), MSAL(configuration, authParams)); - -const webData = await sp.web(); -const meData = await graph.me(); -``` +Please see more scenarios in the [authentication article](../msaljsclient/index.md). diff --git a/docs/concepts/auth-spfx.md b/docs/concepts/auth-spfx.md index 3163b2146..38904a1ce 100644 --- a/docs/concepts/auth-spfx.md +++ b/docs/concepts/auth-spfx.md @@ -51,25 +51,26 @@ Each of the following samples reference a MSAL configuration that utilizes an Az ```TypeScript import { SPFx as graphSPFx, graphfi } from "@pnp/graph"; import { SPFx as spSPFx, spfi } from "@pnp/sp"; -import { MSAL } from "@pnp/msaljsclient"; -import { Configuration, AuthenticationParameters } from "msal"; +import { MSAL, MSALOptions } from "@pnp/msaljsclient"; + import "@pnp/graph/users"; import "@pnp/sp/webs"; -const configuration: Configuration = { - auth: { - authority: "https://login.microsoftonline.com/{tenant Id}/", - clientId: "{AAD Application Id/Client Id}" + const configuration: MSALOptions = { + configuration:{ + auth: { + authority: "https://login.microsoftonline.com/{tenant Id}/", + clientId: "{AAD Application Id/Client Id}" + }, + }, + authParams: { + scopes: ["https://graph.microsoft.com/.default"] } }; -const authParams: AuthenticationParameters = { - scopes: ["https://graph.microsoft.com/.default"] -}; - // within a webpart, application customizer, or adaptive card extension where the context object is available -const graph = graphfi().using(graphSPFx(this.context), MSAL(configuration, authParams)); -const sp = spfi().using(spSPFx(this.context), MSAL(configuration, authParams)); +const graph = graphfi().using(graphSPFx(this.context), MSAL(configuration)); +const sp = spfi().using(spSPFx(this.context), MSAL(configuration)); const meData = await graph.me(); const webData = await sp.web(); diff --git a/docs/msaljsclient/index.md b/docs/msaljsclient/index.md index 9df5e8465..1ca13eb9f 100644 --- a/docs/msaljsclient/index.md +++ b/docs/msaljsclient/index.md @@ -1,33 +1,48 @@ # @pnp/msaljsclient -This library provides a thin wrapper around the [msal](https://github.com/AzureAD/microsoft-authentication-library-for-js) library to make it easy to integrate MSAL authentication in the browser. +This library provides a thin wrapper around the [@azure/msal-browser](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser/docs) library to make it easy to integrate MSAL authentication in the browser. You will first need to install the package: `npm install @pnp/msaljsclient --save` -The configuration and authParams +You may also need to install the MSAL library in typescript for future development with full type support: + +`npm install @azure/msal-browser --save-dev` + +The configuration ```TypeScript +import type { MSALOptions } from "@pnp/msaljsclient"; import { spfi, SPBrowser } from "@pnp/sp"; -import { MSAL } from "@pnp/msaljsclient"; +import { MSAL, getMSAL } from "@pnp/msaljsclient"; import "@pnp/sp/webs"; - -const configuation = { - auth: { - authority: "https://login.microsoftonline.com/common", - clientId: "{client id}", +import "@pnp/sp/site-users/web"; + +const options: MSALOptions = { + configuration: { + auth: { + authority: "https://login.microsoftonline.com/{tanent_id}/", + clientId: "{client id}", + }, + cache: { + cacheLocation: "localStorage" // in order to avoid re-login after page refresh + } + }, + authParams: { + forceRefresh: false, + scopes: ["https://{tenant}.sharepoint.com/.default"], } }; -const authParams = { - scopes: ["https://{tenant}.sharepoint.com/.default"], -}; +const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(SPBrowser(), MSAL(options)); -const sp = spfi("https://tenant.sharepoint.com/sites/dev").using(SPBrowser(), MSAL(configuration, authParams)); +const user = await sp.web.currentUser(); -const webData = await sp.web(); +// For logout later on +const msalInstance = getMSAL(); +const currentAccount = msalInstance.getActiveAccount(); +msalInstance.logoutPopup({ account: currentAccount }); ``` Please see more scenarios in the [authentication article](../concepts/authentication.md). - diff --git a/package-lock.json b/package-lock.json index 0dbd29cfb..5b07e87e4 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,6 +10,7 @@ "license": "MIT", "devDependencies": { "@azure/identity": "3.3.0", + "@azure/msal-browser": "3.5.0", "@azure/msal-node": "1.18.3", "@microsoft/microsoft-graph-types": "2.38.0", "@pnp/buildsystem": "3.1.0", @@ -30,7 +31,6 @@ "eslint": "8.49.0", "findup-sync": "5.0.0", "mocha": "10.2.0", - "msal": "1.4.18", "node-fetch": "3.3.2", "prettyjson": "1.2.5", "string-replace-loader": "3.1.0", @@ -147,6 +147,7 @@ "version": "3.3.0", "resolved": "https://registry.npmjs.org/@azure/identity/-/identity-3.3.0.tgz", "integrity": "sha512-gISa/dAAxrWt6F2WiDXZY0y2xY4MLlN2wkNW4cPuq5OgPQKLSkxLc4I2WR04puTfZyQZnpXbAapAMEj1b96fgg==", + "deprecated": "Please upgrade to the latest version of this package to get necessary fixes", "dev": true, "dependencies": { "@azure/abort-controller": "^1.0.0", @@ -170,6 +171,28 @@ "node": ">=14.0.0" } }, + "node_modules/@azure/identity/node_modules/@azure/msal-browser": { + "version": "2.38.3", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.38.3.tgz", + "integrity": "sha512-2WuLFnWWPR1IdvhhysT18cBbkXx1z0YIchVss5AwVA95g7CU5CpT3d+5BcgVGNXDXbUU7/5p0xYHV99V5z8C/A==", + "deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.", + "dev": true, + "dependencies": { + "@azure/msal-common": "13.3.1" + }, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@azure/identity/node_modules/@azure/msal-common": { + "version": "13.3.1", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.1.tgz", + "integrity": "sha512-Lrk1ozoAtaP/cp53May3v6HtcFSVxdFrg2Pa/1xu5oIvsIwhxW6zSPibKefCOVgd5osgykMi5jjcZHv8XkzZEQ==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@azure/logger": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/@azure/logger/-/logger-1.0.4.tgz", @@ -183,21 +206,21 @@ } }, "node_modules/@azure/msal-browser": { - "version": "2.38.2", - "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-2.38.2.tgz", - "integrity": "sha512-71BeIn2we6LIgMplwCSaMq5zAwmalyJR3jFcVOZxNVfQ1saBRwOD+P77nLs5vrRCedVKTq8RMFhIOdpMLNno0A==", + "version": "3.5.0", + "resolved": "https://registry.npmjs.org/@azure/msal-browser/-/msal-browser-3.5.0.tgz", + "integrity": "sha512-2NtMuel4CI3UEelCPKkNRXgKzpWEX48fvxIvPz7s0/sTcCaI08r05IOkH2GkXW+czUOtuY6+oGafJCpumnjRLg==", "dev": true, "dependencies": { - "@azure/msal-common": "13.3.0" + "@azure/msal-common": "14.4.0" }, "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-common": { - "version": "13.3.0", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.0.tgz", - "integrity": "sha512-/VFWTicjcJbrGp3yQP7A24xU95NiDMe23vxIU1U6qdRPFsprMDNUohMudclnd+WSHE4/McqkZs/nUU3sAKkVjg==", + "version": "14.4.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-14.4.0.tgz", + "integrity": "sha512-ffCymScQuMKVj+YVfwNI52A5Tu+uiZO2eTf+c+3TXxdAssks4nokJhtr+uOOMxH0zDi6d1OjFKFKeXODK0YLSg==", "dev": true, "engines": { "node": ">=0.8.0" @@ -207,6 +230,7 @@ "version": "1.18.3", "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.18.3.tgz", "integrity": "sha512-lI1OsxNbS/gxRD4548Wyj22Dk8kS7eGMwD9GlBZvQmFV8FJUXoXySL1BiNzDsHUE96/DS/DHmA+F73p1Dkcktg==", + "deprecated": "A newer major version of this library is available. Please upgrade to the latest available version.", "dev": true, "dependencies": { "@azure/msal-common": "13.3.0", @@ -217,6 +241,15 @@ "node": "10 || 12 || 14 || 16 || 18" } }, + "node_modules/@azure/msal-node/node_modules/@azure/msal-common": { + "version": "13.3.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-13.3.0.tgz", + "integrity": "sha512-/VFWTicjcJbrGp3yQP7A24xU95NiDMe23vxIU1U6qdRPFsprMDNUohMudclnd+WSHE4/McqkZs/nUU3sAKkVjg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, "node_modules/@babel/code-frame": { "version": "7.22.13", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.22.13.tgz", @@ -7175,24 +7208,6 @@ "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", "dev": true }, - "node_modules/msal": { - "version": "1.4.18", - "resolved": "https://registry.npmjs.org/msal/-/msal-1.4.18.tgz", - "integrity": "sha512-QyWMWrZqpwtK6LEqhwtbikxIWqA1EOcdMvDeIDjIXdGU29wM4orwq538sPe1+JfKDIgPmJj1Fgi5B7luaw/IyA==", - "dev": true, - "dependencies": { - "tslib": "^1.9.3" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/msal/node_modules/tslib": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", - "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", - "dev": true - }, "node_modules/multicast-dns": { "version": "7.2.5", "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", diff --git a/package.json b/package.json index acd81816e..3181815ff 100644 --- a/package.json +++ b/package.json @@ -6,6 +6,7 @@ "description": "A JavaScript library for SharePoint & Graph development.", "devDependencies": { "@azure/identity": "3.3.0", + "@azure/msal-browser": "3.5.0", "@azure/msal-node": "1.18.3", "@microsoft/microsoft-graph-types": "2.38.0", "@pnp/buildsystem": "3.1.0", @@ -26,7 +27,6 @@ "eslint": "8.49.0", "findup-sync": "5.0.0", "mocha": "10.2.0", - "msal": "1.4.18", "node-fetch": "3.3.2", "prettyjson": "1.2.5", "string-replace-loader": "3.1.0", diff --git a/packages/msaljsclient/index.ts b/packages/msaljsclient/index.ts index 0bb9a2a47..5ecefba52 100644 --- a/packages/msaljsclient/index.ts +++ b/packages/msaljsclient/index.ts @@ -1,32 +1,89 @@ -import { AuthenticationParameters, Configuration, UserAgentApplication } from "msal"; +import type { Configuration, SilentRequest, PopupRequest } from "@azure/msal-browser"; +import { PublicClientApplication } from "@azure/msal-browser"; import { Queryable } from "@pnp/queryable"; -export function MSAL(config: Configuration, authParams: AuthenticationParameters = { scopes: ["https://graph.microsoft.com/.default"] }): (instance: Queryable) => Queryable { +export interface MSALOptions { + /** + * The name of the MSAL instance to use + * @default "main" + */ + name?: string; - const app = new UserAgentApplication(config); + /** + * The configuration for the PCA + */ + configuration: Configuration; + + /** + * The authentication parameters to use + */ + authParams: SilentRequest & PopupRequest; + + /** + * Whether or not to log errors to the console + * @default false + */ + logErrors?: boolean; +} + +/** + * Store for MSAL instances in order to have full power over the PCA + * @internal + */ +const instances = new Map(); + +/** + * MSAL behavior for PnPjs + * @param options The options to use when configuring MSAL + * @returns Instance of the behavior + * + * @see https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/README.md#advanced-topics + */ +export function MSAL(options: MSALOptions): (instance: Queryable) => Queryable { + + const name = options.name || "main"; return (instance: Queryable) => { instance.on.auth.replace(async (url: URL, init: RequestInit) => { + let app = instances.get(name) as PublicClientApplication; + + if (!app) { + instances.set(name, new PublicClientApplication(options.configuration)); + app = instances.get(name)!; + await app.initialize(); + } - let accessToken: string; + let accessToken = ""; try { + // Attempt to get the token silently + const tokenResponse = await app.acquireTokenSilent(options.authParams); + accessToken = tokenResponse.accessToken; + }catch (authError) { + // If silent token acquisition fails with InteractionRequiredAuthError, + // attempt to get the token interactively + const loginResponse = await app.loginPopup(options.authParams).catch((loginError) => { - // see if we have already the idtoken saved - const resp = await app.acquireTokenSilent(authParams); - accessToken = resp.accessToken; + if (options.logErrors) { + console.error(loginError); + } - } catch (e) { + throw loginError; + }); - // per examples we fall back to popup - const resp = await app.loginPopup(authParams); - if (resp.idToken) { - const resp2 = await app.acquireTokenSilent(authParams); - accessToken = resp2.accessToken; - } else { - // throw the error that brought us here - throw e; + if (loginResponse.accessToken) { + accessToken = loginResponse.accessToken; + app.setActiveAccount(loginResponse.account); + } + + if (!accessToken) { + + if (options.logErrors) { + console.error(authError); + } + + throw authError; } } @@ -38,3 +95,28 @@ export function MSAL(config: Configuration, authParams: AuthenticationParameters return instance; }; } + +/** + * Get an MSAL instance by name + * @param name The name of the instance to get (@default "main") + * @returns The MSAL instance if found, otherwise throws an error + * + * @see https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/README.md#advanced-topics + * + * @example Log out of an MSAL instance + * ```ts + * const msalInstance = getMSAL(); + * const currentAccount = msalInstance.getActiveAccount(); + * msalInstance.logoutRedirect({ account: currentAccount }); + * ``` + */ +export function getMSAL(name = "main"): PublicClientApplication { + + const pca = instances.get(name); + + if (!pca) { + throw Error(`No MSAL instance found with name '${name}'`); + } + + return pca; +} diff --git a/packages/msaljsclient/package.json b/packages/msaljsclient/package.json index 993e5d6f2..56068e95b 100644 --- a/packages/msaljsclient/package.json +++ b/packages/msaljsclient/package.json @@ -5,8 +5,8 @@ "main": "./index.js", "typings": "./index", "dependencies": { + "@azure/msal-browser": "3.5.0", "@pnp/queryable": "0.0.0-PLACEHOLDER", - "msal": "1.4.17", "tslib": "2.4.1" } -} \ No newline at end of file +}