From 284fd9db2f246169f37c5509a2b16b297b1d0df3 Mon Sep 17 00:00:00 2001 From: Robbie-Microsoft <87724641+Robbie-Microsoft@users.noreply.github.com> Date: Fri, 20 Dec 2024 18:56:30 -0500 Subject: [PATCH] Linted/Formatted all msal-node samples (#7450) I made no changes of my own, but saved every file. The linting/formatting happened automatically. --- samples/e2eTestUtils/src/LabConfig.ts | 6 +- samples/e2eTestUtils/src/MsidApp.ts | 18 +- samples/e2eTestUtils/src/MsidLab.ts | 18 +- samples/msal-node-samples/AGC-README.md | 14 +- .../ElectronSystemBrowserTestApp/src/App.ts | 4 +- .../src/AuthProvider.ts | 6 +- .../src/CachePlugin.ts | 6 +- .../src/Constants.ts | 9 +- .../src/CustomLoopbackClient.ts | 109 +++++++---- .../src/GraphReponseTypes.ts | 25 ++- .../ElectronSystemBrowserTestApp/src/Main.ts | 5 +- .../src/Renderer.ts | 2 - .../src/app/App.tsx | 10 +- .../src/app/components/NavigationBar.tsx | 11 +- .../src/app/components/PageLayout.tsx | 3 +- .../src/app/pages/Home.tsx | 4 +- .../src/app/pages/Profile.tsx | 2 - .../src/preload.ts | 2 +- .../ElectronTestApp/README.md | 32 ++-- .../ElectronTestApp/playwright.config.ts | 6 +- .../ElectronTestApp/src/App.ts | 4 +- .../ElectronTestApp/src/AuthCodeListener.ts | 2 +- .../ElectronTestApp/src/AuthProvider.ts | 91 +++++---- .../ElectronTestApp/src/CachePlugin.ts | 33 ++-- .../ElectronTestApp/src/Constants.ts | 5 +- .../src/CustomProtocolListener.ts | 10 +- .../ElectronTestApp/src/FetchManager.ts | 16 +- .../ElectronTestApp/src/GraphReponseTypes.ts | 25 ++- .../ElectronTestApp/src/Main.ts | 33 ++-- .../ElectronTestApp/src/Renderer.ts | 11 +- .../ElectronTestApp/src/UIManager.ts | 46 +++-- .../ElectronTestApp/src/preload.ts | 68 +++---- samples/msal-node-samples/README.md | 66 +++---- .../auth-code-cli-app/README.md | 11 +- .../auth-code-cli-brokered-app/README.md | 14 +- .../auth-code-distributed-cache/README.md | 30 +-- .../src/AuthProvider.ts | 146 +++++++++++---- .../src/AxiosHelper.ts | 13 +- .../src/PartitionManager.ts | 8 +- .../src/RedisClientWrapper.ts | 20 +- .../src/UrlUtils.ts | 6 +- .../src/middleware.ts | 176 +++++++++++------- .../auth-code-key-vault/README.md | 19 +- .../auth-code-pkce/README.md | 10 +- .../auth-code-pkce/src/index.ts | 74 +++++--- .../auth-code-pkce/src/types/index.ts | 14 +- .../auth-code-with-certs/README.md | 16 +- samples/msal-node-samples/auth-code/README.md | 151 ++++++++------- .../b2c-user-flows/README.md | 144 +++++++++----- .../test/user-flows-msa.spec.ts | 2 +- .../README.md | 37 ++-- .../src/AuthProvider.ts | 120 ++++++++---- .../src/AxiosHelper.ts | 15 +- .../src/CustomCachePlugin.ts | 31 ++- .../src/ProvisionHandler.ts | 96 ++++++---- .../src/RedisClientWrapper.ts | 20 +- .../src/index.ts | 140 ++++++++------ .../client-credentials/README.md | 28 +-- .../client-credentials/index.js | 14 +- .../HttpClientAxios.ts | 9 +- .../HttpClientCurrent.ts | 165 ++++++++++++---- .../README.md | 22 ++- .../app.ts | 12 +- .../express.ts | 13 +- .../msal-node-samples/device-code/README.md | 44 +++-- .../msal-node-samples/device-code/index.js | 10 +- .../on-behalf-of-distributed-cache/README.md | 40 ++-- .../src/AuthProvider.ts | 111 +++++++---- .../src/AxiosHelper.ts | 15 +- .../src/CustomCachePlugin.ts | 31 ++- .../src/RedisClientWrapper.ts | 20 +- .../src/TokenValidator.ts | 67 +++++-- .../on-behalf-of-distributed-cache/src/app.ts | 127 +++++++------ .../src/middleware.ts | 8 +- .../msal-node-samples/on-behalf-of/README.md | 63 ++++--- .../msal-node-samples/refresh-token/README.md | 20 +- .../msal-node-samples/silent-flow/README.md | 54 +++--- .../username-password-cca/README.md | 22 +-- .../username-password/README.md | 16 +- 79 files changed, 1796 insertions(+), 1130 deletions(-) diff --git a/samples/e2eTestUtils/src/LabConfig.ts b/samples/e2eTestUtils/src/LabConfig.ts index 102f61a6ad..ca0c14bb44 100644 --- a/samples/e2eTestUtils/src/LabConfig.ts +++ b/samples/e2eTestUtils/src/LabConfig.ts @@ -3,7 +3,7 @@ import { MsidApp } from "./MsidApp"; import { MsidLab } from "./MsidLab"; export type LabConfig = { - user: MsidUser, - app: MsidApp, - lab: MsidLab + user: MsidUser; + app: MsidApp; + lab: MsidLab; }; diff --git a/samples/e2eTestUtils/src/MsidApp.ts b/samples/e2eTestUtils/src/MsidApp.ts index 1cd67bf23e..640c24ffc6 100644 --- a/samples/e2eTestUtils/src/MsidApp.ts +++ b/samples/e2eTestUtils/src/MsidApp.ts @@ -1,11 +1,11 @@ export type MsidApp = { - appName?: string, - appId?: string, - authority?: string, - b2cAuthorities?: string, - defaultScopes?: string, - appRoles?: string, - multitenantApp?: string, - clientSecret?: string, - clientCertificate?: string + appName?: string; + appId?: string; + authority?: string; + b2cAuthorities?: string; + defaultScopes?: string; + appRoles?: string; + multitenantApp?: string; + clientSecret?: string; + clientCertificate?: string; }; diff --git a/samples/e2eTestUtils/src/MsidLab.ts b/samples/e2eTestUtils/src/MsidLab.ts index f30496fef1..fbaee24942 100644 --- a/samples/e2eTestUtils/src/MsidLab.ts +++ b/samples/e2eTestUtils/src/MsidLab.ts @@ -1,11 +1,11 @@ export type MsidLab = { - labName?: string, - region?: string, - id?: number, - tenantId?: string, - federationProvider?: string, - azureEnvironment?: string, - credentialKeyVaultKeyName?: string, - authority?: string, - adfsEndpoint?: string + labName?: string; + region?: string; + id?: number; + tenantId?: string; + federationProvider?: string; + azureEnvironment?: string; + credentialKeyVaultKeyName?: string; + authority?: string; + adfsEndpoint?: string; }; diff --git a/samples/msal-node-samples/AGC-README.md b/samples/msal-node-samples/AGC-README.md index 0e00afae94..fbfafbccff 100644 --- a/samples/msal-node-samples/AGC-README.md +++ b/samples/msal-node-samples/AGC-README.md @@ -3,12 +3,12 @@ The non-AGC E2E tests are not able to be run in the AGCE. In order to run - and The following seven environment variables must be set in powershell before running the AGC E2E tests. They can be set via the following commands: 1. $env:GRAPH_URL = "The URL of Microsoft Graph API" -This can be found in the Microsoft Entra admin center in the application's App Registration -Important to note: "/v1.0/me" and "/.default" should not be appended to the end of the URL. -These parts of the URL are already accounted for in the E2E tests. + This can be found in the Microsoft Entra admin center in the application's App Registration + Important to note: "/v1.0/me" and "/.default" should not be appended to the end of the URL. + These parts of the URL are already accounted for in the E2E tests. 2. $env:AUTHORITY = "The URL that indicates a directory that MSAL can request tokens from." -This can be found in the Microsoft Entra admin center in the application's App Registration + This can be found in the Microsoft Entra admin center in the application's App Registration 3. $env:KEY_VAULT_URL = "The URL to the key vault where the test user's credentials are stored" @@ -17,8 +17,8 @@ This can be found in the Microsoft Entra admin center in the application's App R 5. $env:AZURE_CLIENT_ID = "The application (client) ID registered in the Microsoft Entra tenant" 6. $env:AZURE_CLIENT_SECRET = "The client secret for the registered application" -It is important to note that the AZURE_CLIENT_ID and AZURE_CLIENT_SECRET values will change depending on if the E2E test is utilizing a confidential or public client. + It is important to note that the AZURE_CLIENT_ID and AZURE_CLIENT_SECRET values will change depending on if the E2E test is utilizing a confidential or public client. 7. $env:NODE_EXTRA_CA_CERTS = "pathToCert" -Certificate chains in the AGC are re-signed with an AGC Certificate Authority certificate. NodeJS does not interact with Windows to get a list of Certificate Authorities to trust. Therefore, you must use the NODE_EXTRA_CA_CERTS environment variable to pass the chain of certificates that was re-signed by the AGC certificate. -The following article provides more context and shows how to re-chain the certificates: https://medium.com/zowe/zowe-cli-providing-node-extra-ca-certs-117727d936e5. + Certificate chains in the AGC are re-signed with an AGC Certificate Authority certificate. NodeJS does not interact with Windows to get a list of Certificate Authorities to trust. Therefore, you must use the NODE_EXTRA_CA_CERTS environment variable to pass the chain of certificates that was re-signed by the AGC certificate. + The following article provides more context and shows how to re-chain the certificates: https://medium.com/zowe/zowe-cli-providing-node-extra-ca-certs-117727d936e5. diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/App.ts b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/App.ts index fbc020810e..19b28e1cc1 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/App.ts +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/App.ts @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License -import Main from './Main'; -Main.main(); \ No newline at end of file +import Main from "./Main"; +Main.main(); diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/AuthProvider.ts b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/AuthProvider.ts index 233a59842d..100008dcab 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/AuthProvider.ts +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/AuthProvider.ts @@ -121,7 +121,9 @@ export default class AuthProvider { * A loopback server of your own implementation, which can have custom logic * such as attempting to listen on a given port if it is available. */ - const customLoopbackClient = await CustomLoopbackClient.initialize(3874); + const customLoopbackClient = await CustomLoopbackClient.initialize( + 3874 + ); // opens a browser instance via Electron shell API const openBrowser = async (url: any) => { @@ -141,7 +143,7 @@ export default class AuthProvider { errorTemplate: fs .readFileSync("./public/errorTemplate.html", "utf8") .toString(), - loopbackClient: customLoopbackClient // overrides default loopback client + loopbackClient: customLoopbackClient, // overrides default loopback client }; const authResponse = diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/CachePlugin.ts b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/CachePlugin.ts index 1af1237eb7..25cd2a62e0 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/CachePlugin.ts +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/CachePlugin.ts @@ -6,8 +6,6 @@ import { ICachePlugin, TokenCacheContext } from "@azure/msal-node"; import * as fs from "fs"; import { CACHE_LOCATION } from "./Constants"; - - export const cachePlugin = (CACHE_LOCATION: string): ICachePlugin => { const beforeCacheAccess = async (cacheContext: TokenCacheContext) => { return new Promise(async (resolve, reject) => { @@ -27,7 +25,7 @@ export const cachePlugin = (CACHE_LOCATION: string): ICachePlugin => { (err) => { if (err) { reject(); - } + } } ); } @@ -42,7 +40,7 @@ export const cachePlugin = (CACHE_LOCATION: string): ICachePlugin => { (err) => { if (err) { console.log(err); - } + } } ); } diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Constants.ts b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Constants.ts index 1fc351c951..1176f371db 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Constants.ts +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Constants.ts @@ -6,10 +6,9 @@ // Add here the endpoints for MS Graph API services you would like to use. export const GRAPH_CONFIG = { GRAPH_ME_ENDPT: "/me", - GRAPH_MAIL_ENDPT: "/me/messages" + GRAPH_MAIL_ENDPT: "/me/messages", }; - export enum IpcMessages { SHOW_WELCOME_MESSAGE = "SHOW_WELCOME_MESSAGE", LOGIN = "LOGIN", @@ -23,7 +22,7 @@ export enum IpcMessages { export const APPLICATION_DIMENSIONS = { WIDTH: 1000, - HEIGHT: 1000 -} + HEIGHT: 1000, +}; -export const CACHE_LOCATION = "./data/cache.json"; \ No newline at end of file +export const CACHE_LOCATION = "./data/cache.json"; diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/CustomLoopbackClient.ts b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/CustomLoopbackClient.ts index 17c6ea142d..da96e1292d 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/CustomLoopbackClient.ts +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/CustomLoopbackClient.ts @@ -4,7 +4,10 @@ */ import http from "http"; -import { ILoopbackClient, ServerAuthorizationCodeResponse } from "@azure/msal-node"; +import { + ILoopbackClient, + ServerAuthorizationCodeResponse, +} from "@azure/msal-node"; /** * Implements ILoopbackClient interface to listen for authZ code response. @@ -25,13 +28,17 @@ export class CustomLoopbackClient implements ILoopbackClient { * @param logger * @returns */ - static async initialize(preferredPort: number | undefined): Promise { + static async initialize( + preferredPort: number | undefined + ): Promise { const loopbackClient = new CustomLoopbackClient(); if (preferredPort === 0 || preferredPort === undefined) { return loopbackClient; } - const isPortAvailable = await loopbackClient.isPortAvailable(preferredPort); + const isPortAvailable = await loopbackClient.isPortAvailable( + preferredPort + ); if (isPortAvailable) { loopbackClient.port = preferredPort; @@ -46,40 +53,67 @@ export class CustomLoopbackClient implements ILoopbackClient { * @param errorTemplate * @returns */ - async listenForAuthCode(successTemplate?: string, errorTemplate?: string): Promise { + async listenForAuthCode( + successTemplate?: string, + errorTemplate?: string + ): Promise { if (!!this.server) { - throw new Error('Loopback server already exists. Cannot create another.') + throw new Error( + "Loopback server already exists. Cannot create another." + ); } - const authCodeListener = new Promise((resolve, reject) => { - this.server = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => { - const url = req.url; - if (!url) { - res.end(errorTemplate || "Error occurred loading redirectUrl"); - reject(new Error('Loopback server callback was invoked without a url. This is unexpected.')); - return; - } else if (url === "/") { - res.end(successTemplate || "Auth code was successfully acquired. You can close this window now."); - return; - } - - const authCodeResponse = CustomLoopbackClient.getDeserializedQueryString(url); - if (authCodeResponse.code) { - const redirectUri = await this.getRedirectUri(); - res.writeHead(302, { location: redirectUri }); // Prevent auth code from being saved in the browser history - res.end(); - } - resolve(authCodeResponse); - }); - this.server.listen(this.port); - }); + const authCodeListener = new Promise( + (resolve, reject) => { + this.server = http.createServer( + async ( + req: http.IncomingMessage, + res: http.ServerResponse + ) => { + const url = req.url; + if (!url) { + res.end( + errorTemplate || + "Error occurred loading redirectUrl" + ); + reject( + new Error( + "Loopback server callback was invoked without a url. This is unexpected." + ) + ); + return; + } else if (url === "/") { + res.end( + successTemplate || + "Auth code was successfully acquired. You can close this window now." + ); + return; + } + + const authCodeResponse = + CustomLoopbackClient.getDeserializedQueryString( + url + ); + if (authCodeResponse.code) { + const redirectUri = await this.getRedirectUri(); + res.writeHead(302, { location: redirectUri }); // Prevent auth code from being saved in the browser history + res.end(); + } + resolve(authCodeResponse); + } + ); + this.server.listen(this.port); + } + ); // Wait for server to be listening await new Promise((resolve) => { let ticks = 0; const id = setInterval(() => { - if ((5000 / 100) < ticks) { - throw new Error('Timed out waiting for auth code listener to be registered.'); + if (5000 / 100 < ticks) { + throw new Error( + "Timed out waiting for auth code listener to be registered." + ); } if (this.server.listening) { @@ -99,13 +133,15 @@ export class CustomLoopbackClient implements ILoopbackClient { */ getRedirectUri(): string { if (!this.server) { - throw new Error('No loopback server exists yet.') + throw new Error("No loopback server exists yet."); } const address = this.server.address(); if (!address || typeof address === "string" || !address.port) { this.closeServer(); - throw new Error('Loopback server address is not type string. This is unexpected.') + throw new Error( + "Loopback server address is not type string. This is unexpected." + ); } const port = address && address.port; @@ -128,8 +164,9 @@ export class CustomLoopbackClient implements ILoopbackClient { * @returns */ isPortAvailable(port: number): Promise { - return new Promise(resolve => { - const server = http.createServer() + return new Promise((resolve) => { + const server = http + .createServer() .listen(port, () => { server.close(); resolve(true); @@ -154,9 +191,7 @@ export class CustomLoopbackClient implements ILoopbackClient { const parsedQueryString = this.parseQueryString(query); // If ? symbol was not present, above will return empty string, so give original query value const deserializedQueryString: ServerAuthorizationCodeResponse = - this.queryStringToObject( - parsedQueryString || query - ); + this.queryStringToObject(parsedQueryString || query); // Check if deserialization didn't work if (!deserializedQueryString) { throw "Unable to deserialize query string"; @@ -185,7 +220,7 @@ export class CustomLoopbackClient implements ILoopbackClient { * @param query */ static queryStringToObject(query: string): ServerAuthorizationCodeResponse { - const obj: {[key:string]:string} = {}; + const obj: { [key: string]: string } = {}; const params = query.split("&"); const decode = (s: string) => decodeURIComponent(s.replace(/\+/g, " ")); params.forEach((pair) => { diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/GraphReponseTypes.ts b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/GraphReponseTypes.ts index 1cf1f445f7..a5c6f4506a 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/GraphReponseTypes.ts +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/GraphReponseTypes.ts @@ -7,23 +7,22 @@ * Graph data about the user. */ export type UserInfo = { - businessPhones?: Array, - displayName?: string, - givenName?: string, - id?: string, - jobTitle?: string, - mail?: string, - mobilePhone?: string, - officeLocation?: string, - preferredLanguage?: string, - surname?: string, - userPrincipalName?: string + businessPhones?: Array; + displayName?: string; + givenName?: string; + id?: string; + jobTitle?: string; + mail?: string; + mobilePhone?: string; + officeLocation?: string; + preferredLanguage?: string; + surname?: string; + userPrincipalName?: string; }; - /** * Mail data from MS Graph */ export type MailInfo = { - value?: Array + value?: Array; }; diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Main.ts b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Main.ts index 0b2912d1d2..0be690b74c 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Main.ts +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Main.ts @@ -55,7 +55,7 @@ export default class Main { // Windows and Linux will quit the application when all windows are closed. In macOS requires explicit quitting if (process.platform !== "darwin") { Main.application.quit(); - } + } } private static requestSingleInstance(): void { @@ -91,7 +91,7 @@ export default class Main { if (Main.mainWindow) { if (Main.mainWindow.isMinimized()) { Main.mainWindow.restore(); - } + } Main.mainWindow.focus(); } } @@ -137,7 +137,6 @@ export default class Main { Main.mainWindow.webContents.send(message, payload); } - private static async attemptSSOSilent(): Promise { const tokenRequest = { scopes: authConfig.resourceApi.scopes, diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Renderer.ts b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Renderer.ts index 64ad6f7d68..0175dd890b 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Renderer.ts +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/Renderer.ts @@ -20,5 +20,3 @@ import "./app/index.tsx"; import "./index.css"; - - diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/App.tsx b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/App.tsx index fa5f4d89c5..be7eaa63f1 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/App.tsx +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/App.tsx @@ -4,7 +4,7 @@ import { AccountInfo } from "@azure/msal-node"; import { IpcMessages } from "../Constants"; import { Profile } from "./pages/Profile"; import { Home } from "./pages/Home"; -import { PageLayout} from "./components/PageLayout"; +import { PageLayout } from "./components/PageLayout"; import "./styles/App.css"; @@ -17,14 +17,16 @@ const Pages = () => { } /> ); -} +}; const App = () => { const [account, setAccount] = useState(null); useEffect(() => { //leveraging IPC channels to communication between the Main React window.api.send(IpcMessages.GET_ACCOUNT); - window.api.receive( IpcMessages.SHOW_WELCOME_MESSAGE,(account: AccountInfo) => { + window.api.receive( + IpcMessages.SHOW_WELCOME_MESSAGE, + (account: AccountInfo) => { setAccount(account); } ); @@ -48,6 +50,6 @@ const App = () => { ); -} +}; export default App; diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/components/NavigationBar.tsx b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/components/NavigationBar.tsx index 25ff048795..79cc13ec92 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/components/NavigationBar.tsx +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/components/NavigationBar.tsx @@ -36,13 +36,14 @@ export const NavigationBar = (props: NavigationBarProps) => { - + Sign out 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 76f47c6efc..194d07c982 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/components/PageLayout.tsx +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/components/PageLayout.tsx @@ -5,7 +5,6 @@ import { NavigationBar } from "./NavigationBar"; type PageLayoutProps = { account: AccountInfo; children: JSX.Element; - }; export const PageLayout = (props: PageLayoutProps) => { @@ -18,4 +17,4 @@ export const PageLayout = (props: PageLayoutProps) => {
); -} +}; diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/pages/Home.tsx b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/pages/Home.tsx index 9c0e5f63eb..33c22f1cb1 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/pages/Home.tsx +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/pages/Home.tsx @@ -1,4 +1,4 @@ -import React from 'react'; +import React from "react"; export const Home = () => { return ( @@ -8,4 +8,4 @@ export const Home = () => { ); -} +}; diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/pages/Profile.tsx b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/pages/Profile.tsx index dc128c1b6f..d6c3e35bc6 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/pages/Profile.tsx +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/app/pages/Profile.tsx @@ -19,6 +19,4 @@ export const Profile = () => { }, []); return <>{graphData ? : null}; - - }; diff --git a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/preload.ts b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/preload.ts index 3d35810257..a6a0dccf6d 100644 --- a/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/preload.ts +++ b/samples/msal-node-samples/ElectronSystemBrowserTestApp/src/preload.ts @@ -36,4 +36,4 @@ contextBridge.exposeInMainWorld("api", { ipcRenderer.removeAllListeners(channel); } }, -}); \ No newline at end of file +}); diff --git a/samples/msal-node-samples/ElectronTestApp/README.md b/samples/msal-node-samples/ElectronTestApp/README.md index 44582ce3de..510ec86d36 100644 --- a/samples/msal-node-samples/ElectronTestApp/README.md +++ b/samples/msal-node-samples/ElectronTestApp/README.md @@ -1,6 +1,6 @@ # MSAL Node Electron Sample -An Electron application built with TypeScript that uses the MSAL Node library to acquire and store access tokens to authenticate with the Microsoft Graph API. +An Electron application built with TypeScript that uses the MSAL Node library to acquire and store access tokens to authenticate with the Microsoft Graph API. > :warning: This sample doesn't follow the best practices for native application authentication using an external user agent for handling the authentication/authorization requests. To follow the best practices, please see the [Electron System Browser Sample](../ElectronSystemBrowserTestApp/README.md). @@ -29,17 +29,22 @@ $ git clone https://github.com/AzureAD/microsoft-authentication-library-for-js.g You can also download the repository as a zip file by selecting "Download ZIP" from the root repository's dropdown "Code" menu. Once you've downloaded the ZIP file, you can decompress it locally and explore the code. ### Pre-requisites -- By using MSAL Node, you are working with the Microsoft Identity ecosystem. Read about [App Registrations](https://docs.microsoft.com/en-us/graph/auth-register-app-v2) and register one for use with this code. -- Install [Node.js](https://nodejs.org/en/), [Electron.js](https://www.electronjs.org/) and [TypeScript](https://www.typescriptlang.org/) if needed. -- Install the MSAL Node package: + +- By using MSAL Node, you are working with the Microsoft Identity ecosystem. Read about [App Registrations](https://docs.microsoft.com/en-us/graph/auth-register-app-v2) and register one for use with this code. +- Install [Node.js](https://nodejs.org/en/), [Electron.js](https://www.electronjs.org/) and [TypeScript](https://www.typescriptlang.org/) if needed. +- Install the MSAL Node package: + ```bash npm install @azure/msal-node ``` -- If you are customizing or building locally, navigate to the `lib/msal-node` directory and build it using the following command: + +- If you are customizing or building locally, navigate to the `lib/msal-node` directory and build it using the following command: + ```bash npm run build:package ``` -- From the repository's root directory, navigate to the Electron sample application: + +- From the repository's root directory, navigate to the Electron sample application: ```bash $ cd samples/msal-node-samples/standalone-samples/ElectronTestApp @@ -50,11 +55,12 @@ $ cd samples/msal-node-samples/standalone-samples/ElectronTestApp The MSAL configuration object in the `ElectronTestApp` is defined in the `AuthProvider.ts` file. However, the configuration values used to build the object are defined in and imported from JSON files in the `config/` directory. The `ElectronTestApp` loads the `config/customConfig.json` configuration by default. You can update the configuration attributes to match your [App Registration](https://docs.microsoft.com/en-us/graph/auth-register-app-v2) directly in the `config/customConfg.json` file, or you can add your own configuration file and change the import path like so: AuthProvider.ts + ```javascript // Change this to load the desired MSAL Client Configuration import * as APP_CONFIG from "./config/customConfig.json"; // Change this -import * as APP_CONFIG from "./config/YOUR_CUSTOM_CONFIG_FILE.json"; // To this +import * as APP_CONFIG from "./config/YOUR_CUSTOM_CONFIG_FILE.json"; // To this ``` This application uses the `User.Read` and `Mail.Read` Microsoft Graph Scopes, so make sure they are enabled in your App Registration. @@ -113,16 +119,16 @@ For example, the following pairs of values should work: ```typescript /// Ex 1: -const CUSTOM_FILE_PROTOCOL = "msal" -const redirectUri = "msal://auth" +const CUSTOM_FILE_PROTOCOL = "msal"; +const redirectUri = "msal://auth"; /// Ex 2: -const CUSTOM_FILE_PROTOCOL = "sampleapp" -const redirectUri = "sampleapp://redirect" +const CUSTOM_FILE_PROTOCOL = "sampleapp"; +const redirectUri = "sampleapp://redirect"; /// Ex 3: -const CUSTOM_FILE_PROTOCOL = "com.sampleapp" -const redirectUri = "com.sampleapp://auth" +const CUSTOM_FILE_PROTOCOL = "com.sampleapp"; +const redirectUri = "com.sampleapp://auth"; ``` #### Registering a custom file protocol URI as a Redirect URI diff --git a/samples/msal-node-samples/ElectronTestApp/playwright.config.ts b/samples/msal-node-samples/ElectronTestApp/playwright.config.ts index e1520146c1..39b24a3c05 100644 --- a/samples/msal-node-samples/ElectronTestApp/playwright.config.ts +++ b/samples/msal-node-samples/ElectronTestApp/playwright.config.ts @@ -1,11 +1,11 @@ import { PlaywrightTestConfig } from "@playwright/test"; -import * as path from 'path'; +import * as path from "path"; const config: PlaywrightTestConfig = { - testDir: path.join(__dirname, '/test'), + testDir: path.join(__dirname, "/test"), retries: 1, use: { - trace: 'on-first-retry', + trace: "on-first-retry", }, timeout: 30000, globalTimeout: 5400000, diff --git a/samples/msal-node-samples/ElectronTestApp/src/App.ts b/samples/msal-node-samples/ElectronTestApp/src/App.ts index fbc020810e..19b28e1cc1 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/App.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/App.ts @@ -1,5 +1,5 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License -import Main from './Main'; -Main.main(); \ No newline at end of file +import Main from "./Main"; +Main.main(); diff --git a/samples/msal-node-samples/ElectronTestApp/src/AuthCodeListener.ts b/samples/msal-node-samples/ElectronTestApp/src/AuthCodeListener.ts index 6ec5511d1e..3b9b11e9da 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/AuthCodeListener.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/AuthCodeListener.ts @@ -23,4 +23,4 @@ export abstract class AuthCodeListener { public abstract start(): Promise; public abstract close(): void; -} \ No newline at end of file +} diff --git a/samples/msal-node-samples/ElectronTestApp/src/AuthProvider.ts b/samples/msal-node-samples/ElectronTestApp/src/AuthProvider.ts index 32c7dc6939..3bd78604e6 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/AuthProvider.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/AuthProvider.ts @@ -10,14 +10,13 @@ import { AuthorizationUrlRequest, AuthenticationResult, SilentFlowRequest, - CryptoProvider + CryptoProvider, } from "@azure/msal-node"; import { cachePlugin } from "./CachePlugin"; import { BrowserWindow } from "electron"; import { CustomProtocolListener } from "./CustomProtocolListener"; export default class AuthProvider { - private clientApplication: PublicClientApplication; private account: AccountInfo; private authCodeUrlParams: AuthorizationUrlRequest; @@ -27,12 +26,12 @@ export default class AuthProvider { private authConfig: any; constructor(authConfig: any) { - this.authConfig = authConfig + this.authConfig = authConfig; this.clientApplication = new PublicClientApplication({ auth: this.authConfig.authOptions, cache: { - cachePlugin: cachePlugin(this.authConfig.cache.cacheLocation) + cachePlugin: cachePlugin(this.authConfig.cache.cacheLocation), }, system: { loggerOptions: { @@ -41,8 +40,8 @@ export default class AuthProvider { }, piiLoggingEnabled: false, logLevel: LogLevel.Info, - } - } + }, + }, }); this.account = null; @@ -72,17 +71,16 @@ export default class AuthProvider { * Initialize request objects used by this AuthModule. */ private setRequestObjects(): void { - const baseSilentRequest = { account: null, - forceRefresh: false + forceRefresh: false, }; this.authCodeUrlParams = this.authConfig.request.authCodeUrlParameters; this.authCodeRequest = { ...this.authConfig.request.authCodeRequest, - code: null + code: null, }; this.silentProfileRequest = { @@ -106,7 +104,7 @@ export default class AuthProvider { async getToken(request: SilentFlowRequest): Promise { let authResponse: AuthenticationResult; - const account = this.account || await this.getAccount(); + const account = this.account || (await this.getAccount()); if (account) { request.account = account; authResponse = await this.getTokenSilent(request); @@ -118,20 +116,32 @@ export default class AuthProvider { return authResponse.accessToken || null; } - async getTokenSilent(tokenRequest: SilentFlowRequest): Promise { + async getTokenSilent( + tokenRequest: SilentFlowRequest + ): Promise { try { - return await this.clientApplication.acquireTokenSilent(tokenRequest); + return await this.clientApplication.acquireTokenSilent( + tokenRequest + ); } catch (error) { - console.log("Silent token acquisition failed, acquiring token using pop up"); - const authCodeRequest = { ...this.authCodeUrlParams, ...tokenRequest }; + console.log( + "Silent token acquisition failed, acquiring token using pop up" + ); + const authCodeRequest = { + ...this.authCodeUrlParams, + ...tokenRequest, + }; return await this.getTokenInteractive(authCodeRequest); } } - async getTokenInteractive(tokenRequest: AuthorizationUrlRequest): Promise { + async getTokenInteractive( + tokenRequest: AuthorizationUrlRequest + ): Promise { // Generate PKCE Challenge and Verifier before request const cryptoProvider = new CryptoProvider(); - const { challenge, verifier } = await cryptoProvider.generatePkceCodes(); + const { challenge, verifier } = + await cryptoProvider.generatePkceCodes(); const authWindow = await AuthProvider.createAuthWindow(); // Add PKCE params to Auth Code URL request @@ -139,21 +149,27 @@ export default class AuthProvider { ...this.authCodeUrlParams, scopes: tokenRequest.scopes, codeChallenge: challenge, - codeChallengeMethod: "S256" + codeChallengeMethod: "S256", }; try { // Get Auth Code URL - const authCodeUrl = await this.clientApplication.getAuthCodeUrl(authCodeUrlParams); + const authCodeUrl = await this.clientApplication.getAuthCodeUrl( + authCodeUrlParams + ); - const authCode = await this.listenForAuthCode(authCodeUrl, authWindow); + const authCode = await this.listenForAuthCode( + authCodeUrl, + authWindow + ); // Use Authorization Code and PKCE Code verifier to make token request - const authResult: AuthenticationResult = await this.clientApplication.acquireTokenByCode({ - ...this.authCodeRequest, - code: authCode, - codeVerifier: verifier - }); + const authResult: AuthenticationResult = + await this.clientApplication.acquireTokenByCode({ + ...this.authCodeRequest, + code: authCode, + codeVerifier: verifier, + }); authWindow.close(); return authResult; @@ -164,7 +180,9 @@ export default class AuthProvider { } async login(): Promise { - const authResult = await this.getTokenInteractive(this.authCodeUrlParams); + const authResult = await this.getTokenInteractive( + this.authCodeUrlParams + ); return this.handleResponse(authResult); } @@ -178,14 +196,21 @@ export default class AuthProvider { async logout(): Promise { if (this.account) { - await this.clientApplication.getTokenCache().removeAccount(this.account); + await this.clientApplication + .getTokenCache() + .removeAccount(this.account); this.account = null; } } - private async listenForAuthCode(navigateUrl: string, authWindow: BrowserWindow): Promise { + private async listenForAuthCode( + navigateUrl: string, + authWindow: BrowserWindow + ): Promise { // Set up custom file protocol to listen for redirect response - const authCodeListener = new CustomProtocolListener(this.authConfig.customProtocol.name); + const authCodeListener = new CustomProtocolListener( + this.authConfig.customProtocol.name + ); const codePromise = authCodeListener.start(); authWindow.loadURL(navigateUrl); const code = await codePromise; @@ -194,9 +219,9 @@ export default class AuthProvider { } /** - * Handles the response from a popup or redirect. If response is null, will check if we have any accounts and attempt to sign in. - * @param response - */ + * Handles the response from a popup or redirect. If response is null, will check if we have any accounts and attempt to sign in. + * @param response + */ private async handleResponse(response: AuthenticationResult) { if (response !== null) { this.account = response.account; @@ -225,7 +250,9 @@ export default class AuthProvider { if (currentAccounts.length > 1) { // Add choose account code here - console.log("Multiple accounts detected, need to add choose account code."); + console.log( + "Multiple accounts detected, need to add choose account code." + ); return currentAccounts[0]; } else if (currentAccounts.length === 1) { return currentAccounts[0]; diff --git a/samples/msal-node-samples/ElectronTestApp/src/CachePlugin.ts b/samples/msal-node-samples/ElectronTestApp/src/CachePlugin.ts index 066c8cd5ac..2db490d4a7 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/CachePlugin.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/CachePlugin.ts @@ -3,7 +3,7 @@ * Licensed under the MIT License. */ import { ICachePlugin } from "@azure/msal-node"; -import * as fs from 'fs'; +import * as fs from "fs"; export const cachePlugin = (CACHE_LOCATION: string): ICachePlugin => { const beforeCacheAccess = async (cacheContext) => { @@ -18,28 +18,35 @@ export const cachePlugin = (CACHE_LOCATION: string): ICachePlugin => { } }); } else { - fs.writeFile(CACHE_LOCATION, cacheContext.tokenCache.serialize(), (err) => { - if (err) { - reject(); + fs.writeFile( + CACHE_LOCATION, + cacheContext.tokenCache.serialize(), + (err) => { + if (err) { + reject(); + } } - }); + ); } }); }; const afterCacheAccess = async (cacheContext) => { if (cacheContext.cacheHasChanged) { - fs.writeFile(CACHE_LOCATION, cacheContext.tokenCache.serialize(), (err) => { - if (err) { - console.log(err); + fs.writeFile( + CACHE_LOCATION, + cacheContext.tokenCache.serialize(), + (err) => { + if (err) { + console.log(err); + } } - }); + ); } }; return { beforeCacheAccess, - afterCacheAccess - } -} - + afterCacheAccess, + }; +}; diff --git a/samples/msal-node-samples/ElectronTestApp/src/Constants.ts b/samples/msal-node-samples/ElectronTestApp/src/Constants.ts index 9ed8bbbdd6..ef368cacbe 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/Constants.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/Constants.ts @@ -6,10 +6,9 @@ // Add here the endpoints for MS Graph API services you would like to use. export const GRAPH_CONFIG = { GRAPH_ME_ENDPT: "/me", - GRAPH_MAIL_ENDPT: "/me/messages" + GRAPH_MAIL_ENDPT: "/me/messages", }; - export enum IpcMessages { SHOW_WELCOME_MESSAGE = "SHOW_WELCOME_MESSAGE", LOGIN = "LOGIN", @@ -17,5 +16,5 @@ export enum IpcMessages { GET_PROFILE = "GET_PROFILE", SET_PROFILE = "SET_PROFILE", GET_MAIL = "GET_MAIL", - SET_MAIL = "SET_MAIL" + SET_MAIL = "SET_MAIL", } diff --git a/samples/msal-node-samples/ElectronTestApp/src/CustomProtocolListener.ts b/samples/msal-node-samples/ElectronTestApp/src/CustomProtocolListener.ts index 868022dff6..2e1290a2e8 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/CustomProtocolListener.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/CustomProtocolListener.ts @@ -1,15 +1,15 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. -import { AuthCodeListener } from './AuthCodeListener'; +import { AuthCodeListener } from "./AuthCodeListener"; -import { protocol } from 'electron'; +import { protocol } from "electron"; /** * CustomProtocolListener can be instantiated in order * to register and unregister a custom typed protocol on which * MSAL can listen for Auth Code reponses. - * + * * For information on available protocol types, check the Electron * protcol docs: https://www.electronjs.org/docs/latest/api/protocol/ */ @@ -26,10 +26,10 @@ export class CustomProtocolListener extends AuthCodeListener { const codePromise = new Promise((resolve, reject) => { protocol.registerStringProtocol(this.host, (req, callback) => { const requestUrl = new URL(req.url); - const authCode = requestUrl.searchParams.get('code'); + const authCode = requestUrl.searchParams.get("code"); if (authCode) { - resolve(authCode) + resolve(authCode); } else { protocol.unregisterProtocol(this.host); reject(new Error("No code found in URL")); diff --git a/samples/msal-node-samples/ElectronTestApp/src/FetchManager.ts b/samples/msal-node-samples/ElectronTestApp/src/FetchManager.ts index 6590c07eda..0d67612474 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/FetchManager.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/FetchManager.ts @@ -10,19 +10,21 @@ import * as axios from "axios"; * Class that handles Bearer requests for data using Fetch. */ export class FetchManager { - /** * Makes an Authorization "Bearer" request with the given accessToken to the given endpoint. - * @param endpoint - * @param accessToken + * @param endpoint + * @param accessToken */ - async callEndpointWithToken(endpoint: string, accessToken: string): Promise { + async callEndpointWithToken( + endpoint: string, + accessToken: string + ): Promise { const options = { headers: { - Authorization: `Bearer ${accessToken}` - } + Authorization: `Bearer ${accessToken}`, + }, }; - console.log('Request made at: ' + new Date().toString()); + console.log("Request made at: " + new Date().toString()); const response = await axios.default.get(endpoint, options); return (await response.data) as UserInfo | MailInfo; diff --git a/samples/msal-node-samples/ElectronTestApp/src/GraphReponseTypes.ts b/samples/msal-node-samples/ElectronTestApp/src/GraphReponseTypes.ts index 1cf1f445f7..a5c6f4506a 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/GraphReponseTypes.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/GraphReponseTypes.ts @@ -7,23 +7,22 @@ * Graph data about the user. */ export type UserInfo = { - businessPhones?: Array, - displayName?: string, - givenName?: string, - id?: string, - jobTitle?: string, - mail?: string, - mobilePhone?: string, - officeLocation?: string, - preferredLanguage?: string, - surname?: string, - userPrincipalName?: string + businessPhones?: Array; + displayName?: string; + givenName?: string; + id?: string; + jobTitle?: string; + mail?: string; + mobilePhone?: string; + officeLocation?: string; + preferredLanguage?: string; + surname?: string; + userPrincipalName?: string; }; - /** * Mail data from MS Graph */ export type MailInfo = { - value?: Array + value?: Array; }; diff --git a/samples/msal-node-samples/ElectronTestApp/src/Main.ts b/samples/msal-node-samples/ElectronTestApp/src/Main.ts index 2d7baa200b..e2a92b0a9e 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/Main.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/Main.ts @@ -9,7 +9,7 @@ import * as path from "path"; import AuthProvider from "./AuthProvider"; import { FetchManager } from "./FetchManager"; import { IpcMessages, GRAPH_CONFIG } from "./Constants"; -import * as authConfig from './config/customConfig.json'; +import * as authConfig from "./config/customConfig.json"; export default class Main { static application: Electron.App; @@ -20,20 +20,22 @@ export default class Main { static main(): void { Main.application = app; - Main.application.on('window-all-closed', Main.onWindowAllClosed); - Main.application.on('ready', Main.onReady); + Main.application.on("window-all-closed", Main.onWindowAllClosed); + Main.application.on("ready", Main.onReady); // if in automation, read the config from environment - Main.authConfig = process.env.authConfig ? JSON.parse(process.env.authConfig) : authConfig; + Main.authConfig = process.env.authConfig + ? JSON.parse(process.env.authConfig) + : authConfig; } private static async loadBaseUI(): Promise { - await Main.mainWindow.loadFile(path.join(__dirname, '../index.html')); + await Main.mainWindow.loadFile(path.join(__dirname, "../index.html")); } private static onWindowAllClosed(): void { // Windows and Linux will quit the application when all windows are closed - if (process.platform !== 'darwin') { + if (process.platform !== "darwin") { // macOS requires explicit quitting Main.application.quit(); } @@ -45,8 +47,8 @@ export default class Main { private static onReady(): void { Main.createMainWindow(); - Main.mainWindow.loadFile(path.join(__dirname, '../index.html')); - Main.mainWindow.on('closed', Main.onClose); + Main.mainWindow.loadFile(path.join(__dirname, "../index.html")); + Main.mainWindow.on("closed", Main.onClose); Main.authProvider = new AuthProvider(Main.authConfig); Main.fetchManager = new FetchManager(); Main.registerSubscriptions(); @@ -65,7 +67,7 @@ export default class Main { * the user interface but is otherwise not trustworthy of directly handling * the Node API. */ - webPreferences: { preload: path.join(__dirname, 'preload.js') } + webPreferences: { preload: path.join(__dirname, "preload.js") }, }); } @@ -84,7 +86,7 @@ export default class Main { } private static async login(): Promise { - const account = await Main.authProvider.login() + const account = await Main.authProvider.login(); await Main.loadBaseUI(); Main.publish(IpcMessages.SHOW_WELCOME_MESSAGE, account); } @@ -94,7 +96,10 @@ export default class Main { const account = Main.authProvider.currentAccount; await Main.loadBaseUI(); Main.publish(IpcMessages.SHOW_WELCOME_MESSAGE, account); - const graphResponse = await Main.fetchManager.callEndpointWithToken(`${Main.authConfig.resourceApi.endpoint}${GRAPH_CONFIG.GRAPH_ME_ENDPT}`, token); + const graphResponse = await Main.fetchManager.callEndpointWithToken( + `${Main.authConfig.resourceApi.endpoint}${GRAPH_CONFIG.GRAPH_ME_ENDPT}`, + token + ); Main.publish(IpcMessages.SET_PROFILE, graphResponse); } @@ -103,7 +108,10 @@ export default class Main { const account = Main.authProvider.currentAccount; await Main.loadBaseUI(); Main.publish(IpcMessages.SHOW_WELCOME_MESSAGE, account); - const graphResponse = await Main.fetchManager.callEndpointWithToken(`${Main.authConfig.resourceApi.endpoint}${GRAPH_CONFIG.GRAPH_ME_ENDPT}`, token); + const graphResponse = await Main.fetchManager.callEndpointWithToken( + `${Main.authConfig.resourceApi.endpoint}${GRAPH_CONFIG.GRAPH_ME_ENDPT}`, + token + ); Main.publish(IpcMessages.SET_MAIL, graphResponse); } @@ -119,5 +127,4 @@ export default class Main { ipcMain.on(IpcMessages.GET_MAIL, Main.getMail); ipcMain.on(IpcMessages.LOGOUT, Main.logout); } - } diff --git a/samples/msal-node-samples/ElectronTestApp/src/Renderer.ts b/samples/msal-node-samples/ElectronTestApp/src/Renderer.ts index 49fd05f5da..8ae69c5546 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/Renderer.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/Renderer.ts @@ -3,7 +3,7 @@ /** * The renderer API is exposed by the preload script found in the preload.ts - * file in order to give the renderer access to the Node API in a secure and + * file in order to give the renderer access to the Node API in a secure and * controlled way */ // @ts-ignore @@ -12,19 +12,18 @@ const renderer = window.renderer; window.renderer.startUiManager(); // UI event handlers -document.querySelector('#SignIn').addEventListener('click', () => { +document.querySelector("#SignIn").addEventListener("click", () => { renderer.sendLoginMessage(); }); -document.querySelector('#SignOut').addEventListener('click', () => { +document.querySelector("#SignOut").addEventListener("click", () => { renderer.sendSignoutMessage(); }); -document.querySelector('#seeProfile').addEventListener('click', () => { +document.querySelector("#seeProfile").addEventListener("click", () => { renderer.sendSeeProfileMessage(); }); -document.querySelector('#readMail').addEventListener('click', () => { +document.querySelector("#readMail").addEventListener("click", () => { renderer.sendReadMailMessage(); }); - diff --git a/samples/msal-node-samples/ElectronTestApp/src/UIManager.ts b/samples/msal-node-samples/ElectronTestApp/src/UIManager.ts index bbfa70bb06..e58b8b57aa 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/UIManager.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/UIManager.ts @@ -16,7 +16,7 @@ export class UIManager { private signInButton: HTMLElement; private signOutButton: HTMLElement; private cardDiv: HTMLElement; - private mailButton: HTMLElement; + private mailButton: HTMLElement; private profileButton: HTMLElement; private profileDiv: HTMLElement; private tabList: HTMLElement; @@ -36,15 +36,15 @@ export class UIManager { public showWelcomeMessage(account: AccountInfo) { // Reconfiguring DOM elements - this.cardDiv.style.display = 'initial'; + this.cardDiv.style.display = "initial"; this.welcomeDiv.innerHTML = `Welcome ${account.username}`; this.signInButton.hidden = true; this.signOutButton.hidden = false; } public clearTabs() { - this.tabList.innerHTML = ''; - this.tabContent.innerHTML = ''; + this.tabList.innerHTML = ""; + this.tabContent.innerHTML = ""; } public updateUI(data: UserInfo | MailInfo, endpoint: string) { @@ -67,36 +67,44 @@ export class UIManager { public setMail(data: MailInfo) { const mailInfo = data as MailInfo; if (mailInfo.value.length < 1) { - alert("Your mailbox is empty!") + alert("Your mailbox is empty!"); } else { this.clearTabs(); mailInfo.value.slice(0, 10).forEach((d: any, i) => { - this.createAndAppendListItem(d, i); - this.createAndAppendContentItem(d, i); + this.createAndAppendListItem(d, i); + this.createAndAppendContentItem(d, i); }); } } public createAndAppendListItem(d: any, i: Number) { 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.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; - this.tabList.appendChild(listItem) + this.tabList.appendChild(listItem); } public createAndAppendContentItem(d: any, i: Number) { 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.setAttribute("class", "tab-pane fade"); + contentItem.setAttribute("id", "list" + i); + contentItem.setAttribute("role", "tabpanel"); + contentItem.setAttribute("aria-labelledby", "list" + i + "list"); if (d.from) { - contentItem.innerHTML = " from: " + d.from.emailAddress.address + "

" + d.bodyPreview + "..."; + contentItem.innerHTML = + " from: " + + d.from.emailAddress.address + + "

" + + d.bodyPreview + + "..."; this.tabContent.appendChild(contentItem); } } diff --git a/samples/msal-node-samples/ElectronTestApp/src/preload.ts b/samples/msal-node-samples/ElectronTestApp/src/preload.ts index 653c715436..66a05ef1b7 100644 --- a/samples/msal-node-samples/ElectronTestApp/src/preload.ts +++ b/samples/msal-node-samples/ElectronTestApp/src/preload.ts @@ -1,7 +1,7 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License -import { contextBridge, ipcRenderer } from 'electron'; +import { contextBridge, ipcRenderer } from "electron"; import { UIManager } from "./UIManager"; import { GRAPH_CONFIG, IpcMessages } from "./Constants"; @@ -13,41 +13,41 @@ import { GRAPH_CONFIG, IpcMessages } from "./Constants"; * communication between the Main and Renderer processes. */ contextBridge.exposeInMainWorld("renderer", { - sendLoginMessage: () => { - ipcRenderer.send(IpcMessages.LOGIN); - }, - sendSignoutMessage: () => { - ipcRenderer.send(IpcMessages.LOGOUT); - }, - sendSeeProfileMessage: () => { - ipcRenderer.send(IpcMessages.GET_PROFILE); - }, - sendReadMailMessage: () => { - ipcRenderer.send(IpcMessages.GET_MAIL); - }, - /** - * This method will be called by the Renderer - * to give the preload script access to the UI manager - */ - startUiManager: () => { + sendLoginMessage: () => { + ipcRenderer.send(IpcMessages.LOGIN); + }, + sendSignoutMessage: () => { + ipcRenderer.send(IpcMessages.LOGOUT); + }, + sendSeeProfileMessage: () => { + ipcRenderer.send(IpcMessages.GET_PROFILE); + }, + sendReadMailMessage: () => { + ipcRenderer.send(IpcMessages.GET_MAIL); + }, /** - * The UI Manager is declared within this API because - * although it's used in the listeners below, it must be initialized by the Renderer - * process in order for the DOM to be accessible through JavaScript. + * This method will be called by the Renderer + * to give the preload script access to the UI manager */ - const uiManager = new UIManager(); + startUiManager: () => { + /** + * The UI Manager is declared within this API because + * although it's used in the listeners below, it must be initialized by the Renderer + * process in order for the DOM to be accessible through JavaScript. + */ + const uiManager = new UIManager(); - // Main process message subscribers - ipcRenderer.on(IpcMessages.SHOW_WELCOME_MESSAGE, (event, account) => { - uiManager.showWelcomeMessage(account); - }); + // Main process message subscribers + ipcRenderer.on(IpcMessages.SHOW_WELCOME_MESSAGE, (event, account) => { + uiManager.showWelcomeMessage(account); + }); - ipcRenderer.on(IpcMessages.SET_PROFILE, (event, graphResponse) => { - uiManager.updateUI(graphResponse, GRAPH_CONFIG.GRAPH_ME_ENDPT); - }); + ipcRenderer.on(IpcMessages.SET_PROFILE, (event, graphResponse) => { + uiManager.updateUI(graphResponse, GRAPH_CONFIG.GRAPH_ME_ENDPT); + }); - ipcRenderer.on(IpcMessages.SET_MAIL, (event, graphResponse) => { - uiManager.updateUI(graphResponse, GRAPH_CONFIG.GRAPH_MAIL_ENDPT); - }); - } -}); \ No newline at end of file + ipcRenderer.on(IpcMessages.SET_MAIL, (event, graphResponse) => { + uiManager.updateUI(graphResponse, GRAPH_CONFIG.GRAPH_MAIL_ENDPT); + }); + }, +}); diff --git a/samples/msal-node-samples/README.md b/samples/msal-node-samples/README.md index 882d051f60..9ac6c97d9c 100644 --- a/samples/msal-node-samples/README.md +++ b/samples/msal-node-samples/README.md @@ -6,42 +6,42 @@ The sample applications contained in this directory are independent samples of M Review our [scenario docs](https://docs.microsoft.com/azure/active-directory/develop/authentication-flows-app-scenarios) to pick an app type and authentication flow. Continue to the appropriate sample to learn how it works, or to build it in to your existing app. -| sample | app type | auth flow | -|------------------------------------------------------------|-----------------------------------|-------------------------------------| -| [auth-code](./auth-code/README.md) | web app (public client) | authorization code | -| [auth-code-pkce](./auth-code-pkce/README.md) | web app (public client) (typescript) | authorization code with PKCE in typescript | -| [auth-code-with-certs](./auth-code-with-certs/README.md) | web app (confidential client) | authorization code | -| [auth-code-key-vault](./auth-code-key-vault/README.md) | web app (confidential client) | authorization code | -| [auth-code-distributed-cache](./auth-code-distributed-cache/README.md) | web app (confidential client) (typescript) | authorization code | -| [auth-code-cli-app](./auth-code-cli-app/README.md) | console app (public client) | authorization code | -| [auth-code-cli-brokered-app](./auth-code-cli-brokered-app/README.md) | console app (public client) | authorization code | -| [silent-flow](./silent-flow/README.md) | web app (confidential client) | authorization code | -| [on-behalf-of](./on-behalf-of/README.md) | web API (confidential client) | on-behalf-of | -| [on-behalf-of-distributed-cache](./on-behalf-of-distributed-cache/README.md) | web API (confidential client) (typescript) | on-behalf-of | -| [refresh-token](./refresh-token/README.md) | web app (confidential client) | refresh token | -| [username-password](./username-password/README.md) | console app (public client) | resource owner password credentials | -| [username-password-cca](./username-password-cca/README.md) | console app (confidential client) | resource owner password credentials | -| [device-code](./device-code/README.md) | browserless app (public client) | device code | -| [client-credentials](./client-credentials/README.md) | daemon app (confidential client) | client credentials | -| [client-credentials-distributed-cache](./client-credentials-distributed-cache/README.md) | daemon app (confidential client) (typescript) | client credentials | -| [b2c-user-flows](./b2c-user-flows/README.md) | web app (confidential client) | authorization code | -| [ElectronTestApp](./ElectronTestApp/README.md) | desktop app (public client) | authorization code with PKCE | +| sample | app type | auth flow | +| ---------------------------------------------------------------------------------------- | --------------------------------------------- | ------------------------------------------ | +| [auth-code](./auth-code/README.md) | web app (public client) | authorization code | +| [auth-code-pkce](./auth-code-pkce/README.md) | web app (public client) (typescript) | authorization code with PKCE in typescript | +| [auth-code-with-certs](./auth-code-with-certs/README.md) | web app (confidential client) | authorization code | +| [auth-code-key-vault](./auth-code-key-vault/README.md) | web app (confidential client) | authorization code | +| [auth-code-distributed-cache](./auth-code-distributed-cache/README.md) | web app (confidential client) (typescript) | authorization code | +| [auth-code-cli-app](./auth-code-cli-app/README.md) | console app (public client) | authorization code | +| [auth-code-cli-brokered-app](./auth-code-cli-brokered-app/README.md) | console app (public client) | authorization code | +| [silent-flow](./silent-flow/README.md) | web app (confidential client) | authorization code | +| [on-behalf-of](./on-behalf-of/README.md) | web API (confidential client) | on-behalf-of | +| [on-behalf-of-distributed-cache](./on-behalf-of-distributed-cache/README.md) | web API (confidential client) (typescript) | on-behalf-of | +| [refresh-token](./refresh-token/README.md) | web app (confidential client) | refresh token | +| [username-password](./username-password/README.md) | console app (public client) | resource owner password credentials | +| [username-password-cca](./username-password-cca/README.md) | console app (confidential client) | resource owner password credentials | +| [device-code](./device-code/README.md) | browserless app (public client) | device code | +| [client-credentials](./client-credentials/README.md) | daemon app (confidential client) | client credentials | +| [client-credentials-distributed-cache](./client-credentials-distributed-cache/README.md) | daemon app (confidential client) (typescript) | client credentials | +| [b2c-user-flows](./b2c-user-flows/README.md) | web app (confidential client) | authorization code | +| [ElectronTestApp](./ElectronTestApp/README.md) | desktop app (public client) | authorization code with PKCE | For in-depth tutorials, see: -- [Developing a web app with MSAL Node](https://docs.microsoft.com/azure/active-directory/develop/tutorial-v2-nodejs-webapp-msal) -- [Developing a daemon app with MSAL Node](https://docs.microsoft.com/azure/active-directory/develop/tutorial-v2-nodejs-console) -- [Developing a desktop app with MSAL Node](https://docs.microsoft.com/azure/active-directory/develop/tutorial-v2-nodejs-desktop) -- [Tutorial: Enable your Express web app to sign-in users and call APIs with the Microsoft identity platform](https://github.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial) +- [Developing a web app with MSAL Node](https://docs.microsoft.com/azure/active-directory/develop/tutorial-v2-nodejs-webapp-msal) +- [Developing a daemon app with MSAL Node](https://docs.microsoft.com/azure/active-directory/develop/tutorial-v2-nodejs-console) +- [Developing a desktop app with MSAL Node](https://docs.microsoft.com/azure/active-directory/develop/tutorial-v2-nodejs-desktop) +- [Tutorial: Enable your Express web app to sign-in users and call APIs with the Microsoft identity platform](https://github.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial) ### Other samples These samples use MSAL Node in a variety of scenarios: -- [React SPA with Express web app using the Backend For Frontend (BFF) Proxy architecture to authenticate users with Microsoft Entra ID and call Microsoft Graph](https://github.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/tree/main/5-AdvancedScenarios/1-call-graph-bff): Sample illustrating the BFF pattern to acquire tokens in a secure backend and share authentication state with a React single-page application. -- [B2C user management sample](https://github.com/Azure-Samples/ms-identity-b2c-javascript-nodejs-management/tree/main/Chapter2): Command line app using OAuth 2.0 client credentials flow for performing user management operations in an Azure Active Directory B2C tenant -- [Function API sample deployed to Azure Static Web Apps](https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/4-Deployment/2-deploy-static): Azure Function web API using on-behalf-of flow -- [Teams Tab SSO sample](https://github.com/pnp/teams-dev-samples/tree/main/samples/tab-sso/src/nodejs): Teams tab app demonstrating how integrate MSAL React and MSAL Node to achieve single sign-on +- [React SPA with Express web app using the Backend For Frontend (BFF) Proxy architecture to authenticate users with Microsoft Entra ID and call Microsoft Graph](https://github.com/Azure-Samples/ms-identity-javascript-nodejs-tutorial/tree/main/5-AdvancedScenarios/1-call-graph-bff): Sample illustrating the BFF pattern to acquire tokens in a secure backend and share authentication state with a React single-page application. +- [B2C user management sample](https://github.com/Azure-Samples/ms-identity-b2c-javascript-nodejs-management/tree/main/Chapter2): Command line app using OAuth 2.0 client credentials flow for performing user management operations in an Azure Active Directory B2C tenant +- [Function API sample deployed to Azure Static Web Apps](https://github.com/Azure-Samples/ms-identity-javascript-react-tutorial/tree/main/4-Deployment/2-deploy-static): Azure Function web API using on-behalf-of flow +- [Teams Tab SSO sample](https://github.com/pnp/teams-dev-samples/tree/main/samples/tab-sso/src/nodejs): Teams tab app demonstrating how integrate MSAL React and MSAL Node to achieve single sign-on ## How to run the samples? @@ -73,9 +73,9 @@ From the repository's root directory, navigate to a sample application: ### Pre-requisites -- By using MSAL Node, you are working with the Microsoft identity platform. Read about [App Registration](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app#register-an-application) and register one to use with this code. -- Install [Node.js](https://nodejs.org/en/) if needed. -- Build MSAL Node. See the [guide](../../lib/msal-node/README.md#build-and-test). +- By using MSAL Node, you are working with the Microsoft identity platform. Read about [App Registration](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app#register-an-application) and register one to use with this code. +- Install [Node.js](https://nodejs.org/en/) if needed. +- Build MSAL Node. See the [guide](../../lib/msal-node/README.md#build-and-test). ### Configure the application @@ -93,8 +93,8 @@ const config = { clientId: "YOUR_CLIENT_ID", authority: "YOUR_AUTHORITY_URL", knownAuthorities: ["YOUR_KNOWN_AUTHORITY"], // typically applies to apps on Azure Active Directory B2C - clientSecret: process.env.CLIENT_SECRET // only applies to Confidential Client applications, such as backend web applications - } + clientSecret: process.env.CLIENT_SECRET, // only applies to Confidential Client applications, such as backend web applications + }, }; ``` diff --git a/samples/msal-node-samples/auth-code-cli-app/README.md b/samples/msal-node-samples/auth-code-cli-app/README.md index 4a103ac943..b4ed562dc4 100644 --- a/samples/msal-node-samples/auth-code-cli-app/README.md +++ b/samples/msal-node-samples/auth-code-cli-app/README.md @@ -4,7 +4,6 @@ This sample demonstrates an MSAL Node [public client application](../../../lib/m This sample uses the [OAuth 2.0 Authorization Code Grant](https://oauth.net/2/grant-types/authorization-code/) flow with [Proof-Key For Code Exchange](https://oauth.net/2/pkce/) (PKCE). This flow is particularly suitable for public client applications like desktop and mobile apps. - ## Setup Locate the folder where `package.json` resides in your terminal. Then type: @@ -18,9 +17,9 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-webapp`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. - - In the **Redirect URI (optional)** section, select **Public client/native (mobile & desktop)** in the combo-box and enter the following redirect URI: `http://localhost`. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-webapp`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Redirect URI (optional)** section, select **Public client/native (mobile & desktop)** in the combo-box and enter the following redirect URI: `http://localhost`. 1. Select **Register** to create the application. 1. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later. @@ -31,7 +30,7 @@ const config = { auth: { clientId: "ENTER_CLIENT_ID", authority: "https://login.microsoftonline.com/ENTER_TENANT_ID", - } + }, }; ``` @@ -47,4 +46,4 @@ The app should attempt to acquire a token interactively and make a call to Micro ## More information -- [Microsoft identity platform OAuth 2.0 Authorization Code Grant](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) +- [Microsoft identity platform OAuth 2.0 Authorization Code Grant](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) diff --git a/samples/msal-node-samples/auth-code-cli-brokered-app/README.md b/samples/msal-node-samples/auth-code-cli-brokered-app/README.md index da5e7c00b6..08fd49a2de 100644 --- a/samples/msal-node-samples/auth-code-cli-brokered-app/README.md +++ b/samples/msal-node-samples/auth-code-cli-brokered-app/README.md @@ -15,9 +15,9 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-cliapp`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. - - In the **Redirect URI (optional)** section, select **Public client/native (mobile & desktop)** in the combo-box and enter the following redirect URI: `http://localhost`. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-cliapp`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Redirect URI (optional)** section, select **Public client/native (mobile & desktop)** in the combo-box and enter the following redirect URI: `http://localhost`. 1. Select **Register** to create the application. 1. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later. @@ -28,14 +28,14 @@ const config = { auth: { clientId: "ENTER_CLIENT_ID", authority: "https://login.microsoftonline.com/ENTER_TENANT_INFO", - } + }, }; ``` On the Authenticaion Page of your app, add the following: -- In the **Redirect URI** section, select **Public client/native (mobile & desktop)** in the combo-box and enter the following redirect URI: `ms-appx-web://Microsoft.AAD.BrokerPlugin/`, replacing `` with the **Application (client) ID** from your app's registration screen. -- In the **Advanced Settings** section, set the **Allow public client flows** to **Yes**. +- In the **Redirect URI** section, select **Public client/native (mobile & desktop)** in the combo-box and enter the following redirect URI: `ms-appx-web://Microsoft.AAD.BrokerPlugin/`, replacing `` with the **Application (client) ID** from your app's registration screen. +- In the **Advanced Settings** section, set the **Allow public client flows** to **Yes**. ## Run the app @@ -49,4 +49,4 @@ The app should attempt to acquire a token interactively and make a call to Micro ## More information -- [Microsoft identity platform OAuth 2.0 Authorization Code Grant](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) +- [Microsoft identity platform OAuth 2.0 Authorization Code Grant](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) diff --git a/samples/msal-node-samples/auth-code-distributed-cache/README.md b/samples/msal-node-samples/auth-code-distributed-cache/README.md index 58843056e9..3612388f90 100644 --- a/samples/msal-node-samples/auth-code-distributed-cache/README.md +++ b/samples/msal-node-samples/auth-code-distributed-cache/README.md @@ -1,6 +1,6 @@ # MSAL Node Standalone Sample: Web app using Authorization Code Flow -This sample demonstrates how to implement an MSAL Node [confidential client application](../../../lib/msal-node/docs/initialize-confidential-client-application.md) calling (1) Microsoft Graph directly using the [OAuth 2.0 Authorization code grant](https://learn.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) and (2) a protected web API (aka *middle-tier*) which in turn calls Microsoft Graph using the [OAuth 2.0 on-behalf-of flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) (see also the sample: [on-behalf-of-distributed-cache](../on-behalf-of-distributed-cache)). +This sample demonstrates how to implement an MSAL Node [confidential client application](../../../lib/msal-node/docs/initialize-confidential-client-application.md) calling (1) Microsoft Graph directly using the [OAuth 2.0 Authorization code grant](https://learn.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) and (2) a protected web API (aka _middle-tier_) which in turn calls Microsoft Graph using the [OAuth 2.0 on-behalf-of flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow) (see also the sample: [on-behalf-of-distributed-cache](../on-behalf-of-distributed-cache)). In addition, this sample uses the MSAL Node [DistributedCachePlugin](../../../lib/msal-node/src/cache/distributed/DistributedCachePlugin.ts) to implement the [distributed token caching](../../../lib/msal-node/docs/caching.md#performance-and-security) pattern. Here, the cache is persisted via [Redis](https://redis.io/) and [node-redis](https://github.com/NodeRedis/node-redis). @@ -19,24 +19,24 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-webapp`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. - - Under **Redirect URI (optional)**, select **Web** and set the redirect URI to **http://localhost:3000/redirect** + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-webapp`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. + - Under **Redirect URI (optional)**, select **Web** and set the redirect URI to **http://localhost:3000/redirect** 1. Select **Register** to create the application. 1. 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. 1. Select **Save** to save your changes. 1. In the app's registration screen, select the **Certificates & secrets** blade in the left to open the page where we can generate secrets and upload certificates. 1. In the **Client secrets** section, select **New client secret**: - - Type a key description (for instance `app secret`), - - Select one of the available key durations (**In 1 year**, **In 2 years**, or **Never Expires**) as per your security posture. - - The generated key value will be displayed when you select the **Add** button. Copy the generated value for use in the steps later. - > :warning: In production, use certificates with Azure Key Vault instead of secrets. See [certificate-credentials.md](../../../lib/msal-node/docs/certificate-credentials.md) and [key-vault.md](../../../lib/msal-node/docs/key-vault-managed-identity.md) for more information and examples. + - Type a key description (for instance `app secret`), + - Select one of the available key durations (**In 1 year**, **In 2 years**, or **Never Expires**) as per your security posture. + - The generated key value will be displayed when you select the **Add** button. Copy the generated value for use in the steps later. + > :warning: In production, use certificates with Azure Key Vault instead of secrets. See [certificate-credentials.md](../../../lib/msal-node/docs/certificate-credentials.md) and [key-vault.md](../../../lib/msal-node/docs/key-vault-managed-identity.md) for more information and examples. 1. (Optional) In the app's registration screen, select the **API permissions** blade in the left to open the page where we add access to the APIs that your application needs. - - Select the **Add a permission** button and then, - - Ensure that the **My APIs** tab is selected. - - In the list of APIs, select the API `msal-node-webapi`. - - In the **Delegated permissions** section, select the **access_as_user** in the list. Use the search box if necessary. - - Select the **Add permissions** button at the bottom. + - Select the **Add a permission** button and then, + - Ensure that the **My APIs** tab is selected. + - In the list of APIs, select the API `msal-node-webapi`. + - In the **Delegated permissions** section, select the **access_as_user** in the list. Use the search box if necessary. + - Select the **Add permissions** button at the bottom. Before running the sample, you will need to replace the values in the configuration object (see [app.ts](./src/app.ts)): @@ -80,5 +80,5 @@ For persisting tokens using a distributed cache, web apps using auth code flow s ## More information -- [Scenario: Web app that signs in users](https://learn.microsoft.com/azure/active-directory/develop/scenario-web-app-sign-user-overview) -- [Scenario: A web app that authenticates users and calls web APIs](https://learn.microsoft.com/azure/active-directory/develop/scenario-web-app-call-api-overview) +- [Scenario: Web app that signs in users](https://learn.microsoft.com/azure/active-directory/develop/scenario-web-app-sign-user-overview) +- [Scenario: A web app that authenticates users and calls web APIs](https://learn.microsoft.com/azure/active-directory/develop/scenario-web-app-call-api-overview) diff --git a/samples/msal-node-samples/auth-code-distributed-cache/src/AuthProvider.ts b/samples/msal-node-samples/auth-code-distributed-cache/src/AuthProvider.ts index dcb292cc4a..5bb58f77c3 100644 --- a/samples/msal-node-samples/auth-code-distributed-cache/src/AuthProvider.ts +++ b/samples/msal-node-samples/auth-code-distributed-cache/src/AuthProvider.ts @@ -15,11 +15,11 @@ import { SilentFlowRequest, CryptoProvider, AuthorizationUrlRequest, - AuthorizationCodePayload + AuthorizationCodePayload, } from "@azure/msal-node"; import RedisClientWrapper from "./RedisClientWrapper"; import PartitionManager from "./PartitionManager"; -import AxiosHelper from './AxiosHelper'; +import AxiosHelper from "./AxiosHelper"; export type AppConfig = { instance: string; @@ -35,14 +35,20 @@ export class AuthProvider { private cacheClient: RedisClientType; private cacheClientWrapper: RedisClientWrapper; - private constructor(msalConfig: Configuration, cacheClient: RedisClientType) { + private constructor( + msalConfig: Configuration, + cacheClient: RedisClientType + ) { this.msalConfig = msalConfig; this.cacheClient = cacheClient; this.cacheClientWrapper = new RedisClientWrapper(cacheClient); this.cryptoProvider = new CryptoProvider(); } - static async initialize(appConfig: AppConfig, cacheClient: RedisClientType): Promise { + static async initialize( + appConfig: AppConfig, + cacheClient: RedisClientType + ): Promise { const msalConfig = { auth: { clientId: appConfig.clientId, @@ -55,13 +61,16 @@ export class AuthProvider { console.log(message); }, logLevel: LogLevel.Verbose, - piiLoggingEnabled: false + piiLoggingEnabled: false, }, // proxyUrl: "http://localhost:8888" // uncomment to capture traffic with Fiddler - } + }, } as Configuration; - const msalConfigWithMetadata = await AuthProvider.getMetadata(msalConfig, cacheClient); + const msalConfigWithMetadata = await AuthProvider.getMetadata( + msalConfig, + cacheClient + ); return new AuthProvider(msalConfigWithMetadata, cacheClient); } @@ -73,13 +82,16 @@ export class AuthProvider { cachePlugin: new DistributedCachePlugin( this.cacheClientWrapper, new PartitionManager(this.cacheClientWrapper, sessionId) // partitionKey homeAccountId - ) - } + ), + }, }); } - async prepareTokenRequest(authCodeUrlParameters: AuthorizationUrlRequest, sessionId: string, requestRoute?: string): Promise<{ authCodeUrl: string, state: string }> { - + async prepareTokenRequest( + authCodeUrlParameters: AuthorizationUrlRequest, + sessionId: string, + requestRoute?: string + ): Promise<{ authCodeUrl: string; state: string }> { /** * MSAL Node allows you to pass your custom state as state parameter in the Request object. * The state parameter can also be used to encode information about the app's state before redirect. @@ -88,7 +100,7 @@ export class AuthProvider { const state = this.cryptoProvider.base64Encode( JSON.stringify({ csrfToken: this.cryptoProvider.createNewGuid(), // create a GUID for csrf - redirectTo: requestRoute || '/', + redirectTo: requestRoute || "/", }) ); @@ -102,10 +114,20 @@ export class AuthProvider { return { authCodeUrl, state }; } - async getTokenInteractive(tokenRequest: AuthorizationCodeRequest, authCodePayLoad: AuthorizationCodePayload, sessionId: string): Promise { - if (tokenRequest.authority && tokenRequest.authority !== this.msalConfig.auth.authority) { + async getTokenInteractive( + tokenRequest: AuthorizationCodeRequest, + authCodePayLoad: AuthorizationCodePayload, + sessionId: string + ): Promise { + if ( + tokenRequest.authority && + tokenRequest.authority !== this.msalConfig.auth.authority + ) { this.msalConfig.auth.authority = tokenRequest.authority; - this.msalConfig = await AuthProvider.getMetadata(this.msalConfig, this.cacheClient); + this.msalConfig = await AuthProvider.getMetadata( + this.msalConfig, + this.cacheClient + ); } const msalInstance = this.getMsalInstance(sessionId); @@ -113,50 +135,89 @@ export class AuthProvider { performance.mark("acquireTokenByCode-start"); await msalInstance.getTokenCache().getAllAccounts(); // required for triggering beforeCacheACcess - tokenResponse = await msalInstance.acquireTokenByCode(tokenRequest, authCodePayLoad); + tokenResponse = await msalInstance.acquireTokenByCode( + tokenRequest, + authCodePayLoad + ); performance.mark("acquireTokenByCode-end"); - performance.measure("acquireTokenByCode", "acquireTokenByCode-start", "acquireTokenByCode-end"); + performance.measure( + "acquireTokenByCode", + "acquireTokenByCode-start", + "acquireTokenByCode-end" + ); return tokenResponse; } - async getTokenSilent(silentRequest: SilentFlowRequest, sessionId: string): Promise { + async getTokenSilent( + silentRequest: SilentFlowRequest, + sessionId: string + ): Promise { const msalInstance = this.getMsalInstance(sessionId); let tokenResponse = null; performance.mark("acquireTokenSilent-start"); await msalInstance.getTokenCache().getAllAccounts(); // required for triggering beforeCacheACcess tokenResponse = await msalInstance.acquireTokenSilent(silentRequest); performance.mark("acquireTokenSilent-end"); - performance.measure("acquireTokenSilent", "acquireTokenSilent-start", "acquireTokenSilent-end"); + performance.measure( + "acquireTokenSilent", + "acquireTokenSilent-start", + "acquireTokenSilent-end" + ); return tokenResponse; } - private static async getMetadata(msalConfig: Configuration, cacheClient: RedisClientType): Promise { + private static async getMetadata( + msalConfig: Configuration, + cacheClient: RedisClientType + ): Promise { const msalConfigWithMetadata = msalConfig; const clientId = msalConfigWithMetadata.auth.clientId; - const tenantId = msalConfigWithMetadata.auth.authority!.split("/").pop()!; + const tenantId = msalConfigWithMetadata.auth + .authority!.split("/") + .pop()!; try { - let [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([ - cacheClient.get(`${clientId}.${tenantId}.discovery-metadata`), - cacheClient.get(`${clientId}.${tenantId}.authority-metadata`) - ]); + let [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all( + [ + cacheClient.get( + `${clientId}.${tenantId}.discovery-metadata` + ), + cacheClient.get( + `${clientId}.${tenantId}.authority-metadata` + ), + ] + ); if (!cloudDiscoveryMetadata || !authorityMetadata) { - [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([ - AuthProvider.fetchCloudDiscoveryMetadata(tenantId), - AuthProvider.fetchOIDCMetadata(tenantId) - ]); + [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all( + [ + AuthProvider.fetchCloudDiscoveryMetadata(tenantId), + AuthProvider.fetchOIDCMetadata(tenantId), + ] + ); if (cloudDiscoveryMetadata && authorityMetadata) { - await cacheClient.set(`${clientId}.${tenantId}.discovery-metadata`, JSON.stringify(cloudDiscoveryMetadata)); - await cacheClient.set(`${clientId}.${tenantId}.authority-metadata`, JSON.stringify(authorityMetadata)); + await cacheClient.set( + `${clientId}.${tenantId}.discovery-metadata`, + JSON.stringify(cloudDiscoveryMetadata) + ); + await cacheClient.set( + `${clientId}.${tenantId}.authority-metadata`, + JSON.stringify(authorityMetadata) + ); } } - msalConfigWithMetadata.auth.cloudDiscoveryMetadata = typeof cloudDiscoveryMetadata === 'string' ? cloudDiscoveryMetadata : JSON.stringify(cloudDiscoveryMetadata); - msalConfigWithMetadata.auth.authorityMetadata = typeof authorityMetadata === 'string' ? authorityMetadata : JSON.stringify(authorityMetadata); + msalConfigWithMetadata.auth.cloudDiscoveryMetadata = + typeof cloudDiscoveryMetadata === "string" + ? cloudDiscoveryMetadata + : JSON.stringify(cloudDiscoveryMetadata); + msalConfigWithMetadata.auth.authorityMetadata = + typeof authorityMetadata === "string" + ? authorityMetadata + : JSON.stringify(authorityMetadata); } catch (error) { console.log(error); } @@ -164,14 +225,21 @@ export class AuthProvider { return msalConfigWithMetadata; } - private static async fetchCloudDiscoveryMetadata(tenantId: string): Promise { - const endpoint = 'https://login.microsoftonline.com/common/discovery/instance'; + private static async fetchCloudDiscoveryMetadata( + tenantId: string + ): Promise { + const endpoint = + "https://login.microsoftonline.com/common/discovery/instance"; try { - const response = await AxiosHelper.callDownstreamApi(endpoint, undefined, { - 'api-version': '1.1', - 'authorization_endpoint': `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize` - }); + const response = await AxiosHelper.callDownstreamApi( + endpoint, + undefined, + { + "api-version": "1.1", + authorization_endpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`, + } + ); return response; } catch (error) { diff --git a/samples/msal-node-samples/auth-code-distributed-cache/src/AxiosHelper.ts b/samples/msal-node-samples/auth-code-distributed-cache/src/AxiosHelper.ts index 175e2f983b..fbd11fa72c 100644 --- a/samples/msal-node-samples/auth-code-distributed-cache/src/AxiosHelper.ts +++ b/samples/msal-node-samples/auth-code-distributed-cache/src/AxiosHelper.ts @@ -6,18 +6,23 @@ import axios from "axios"; class AxiosHelper { - /** * Makes an Authorization "Bearer" request with the given accessToken to the given endpoint. * @param endpoint * @param accessToken */ - static async callDownstreamApi(endpoint: string, accessToken?: string, params?: Record): Promise { + static async callDownstreamApi( + endpoint: string, + accessToken?: string, + params?: Record + ): Promise { console.log(`Request to ${endpoint} made at: ${new Date()}`); const response = await axios.get(endpoint, { - headers: (accessToken && { Authorization: `Bearer ${accessToken}` }) || undefined, - params: params || undefined + headers: + (accessToken && { Authorization: `Bearer ${accessToken}` }) || + undefined, + params: params || undefined, }); if (response.status !== 200) { diff --git a/samples/msal-node-samples/auth-code-distributed-cache/src/PartitionManager.ts b/samples/msal-node-samples/auth-code-distributed-cache/src/PartitionManager.ts index 7fbd74d7ff..d38bd833df 100644 --- a/samples/msal-node-samples/auth-code-distributed-cache/src/PartitionManager.ts +++ b/samples/msal-node-samples/auth-code-distributed-cache/src/PartitionManager.ts @@ -14,7 +14,7 @@ const EMPTY_STRING = ""; interface SessionCacheData { account: AccountInfo; [key: string]: any; -}; +} class PartitionManager implements IPartitionManager { sessionId: string; @@ -31,8 +31,10 @@ class PartitionManager implements IPartitionManager { * express-session keys are prefixed with "sess:"" by default. * You can configure this via the session middleware configurations. */ - const sessionData = await this.redisClient.get(`${SESSION_KEY_PREFIX}${this.sessionId}`); - const session = JSON.parse(sessionData) as SessionCacheData ; + const sessionData = await this.redisClient.get( + `${SESSION_KEY_PREFIX}${this.sessionId}` + ); + const session = JSON.parse(sessionData) as SessionCacheData; return session.account?.homeAccountId || EMPTY_STRING; } catch (error) { console.log(error); diff --git a/samples/msal-node-samples/auth-code-distributed-cache/src/RedisClientWrapper.ts b/samples/msal-node-samples/auth-code-distributed-cache/src/RedisClientWrapper.ts index 3884a293e9..c6df5326bf 100644 --- a/samples/msal-node-samples/auth-code-distributed-cache/src/RedisClientWrapper.ts +++ b/samples/msal-node-samples/auth-code-distributed-cache/src/RedisClientWrapper.ts @@ -13,11 +13,11 @@ const EVICTION_POLICY = "volatile-lru"; const EMPTY_STRING = ""; /** -* Simple persistence client helper, using Redis (node-redis). You must have redis installed -* on your machine and have redis server listening. Note that this is only for illustration, -* and you'll need to consider cache eviction policies and handle cache server connection -* issues. For more information, visit: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md -*/ + * Simple persistence client helper, using Redis (node-redis). You must have redis installed + * on your machine and have redis server listening. Note that this is only for illustration, + * and you'll need to consider cache eviction policies and handle cache server connection + * issues. For more information, visit: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md + */ class RedisClientWrapper implements ICacheClient { cacheClient: RedisClientType; @@ -39,7 +39,7 @@ class RedisClientWrapper implements ICacheClient { */ public async get(key: string): Promise { try { - return await this.cacheClient.get(key) || EMPTY_STRING; + return (await this.cacheClient.get(key)) || EMPTY_STRING; } catch (error) { console.log(error); } @@ -55,9 +55,11 @@ class RedisClientWrapper implements ICacheClient { */ public async set(key: string, value: string): Promise { try { - return await this.cacheClient.set(key, value, { - EX: CACHE_TTL // Expire in 24 hours - }) || EMPTY_STRING; + return ( + (await this.cacheClient.set(key, value, { + EX: CACHE_TTL, // Expire in 24 hours + })) || EMPTY_STRING + ); } catch (error) { console.log(error); } diff --git a/samples/msal-node-samples/auth-code-distributed-cache/src/UrlUtils.ts b/samples/msal-node-samples/auth-code-distributed-cache/src/UrlUtils.ts index 6f34510d15..04b3cd0e6d 100644 --- a/samples/msal-node-samples/auth-code-distributed-cache/src/UrlUtils.ts +++ b/samples/msal-node-samples/auth-code-distributed-cache/src/UrlUtils.ts @@ -8,8 +8,10 @@ import { Request } from "express"; class UrlUtils { static getCanonicalUrlFromRequest = (req: Request): string => { - return UrlString.canonicalizeUri(`${req.protocol}://${req.get("host")}${req.path}`); - } + return UrlString.canonicalizeUri( + `${req.protocol}://${req.get("host")}${req.path}` + ); + }; static getPathFromUrl = (url: string): string => { const urlComponents: IUri = new UrlString(url).getUrlComponents(); diff --git a/samples/msal-node-samples/auth-code-distributed-cache/src/middleware.ts b/samples/msal-node-samples/auth-code-distributed-cache/src/middleware.ts index 0227b95f28..f04ef7897a 100644 --- a/samples/msal-node-samples/auth-code-distributed-cache/src/middleware.ts +++ b/samples/msal-node-samples/auth-code-distributed-cache/src/middleware.ts @@ -3,23 +3,27 @@ * Licensed under the MIT License. */ -import { UrlString } from '@azure/msal-common'; -import { InteractionRequiredAuthError, ResponseMode, AuthorizationCodeRequest } from '@azure/msal-node'; -import express, { Request, Response, NextFunction, Router } from 'express'; +import { UrlString } from "@azure/msal-common"; +import { + InteractionRequiredAuthError, + ResponseMode, + AuthorizationCodeRequest, +} from "@azure/msal-node"; +import express, { Request, Response, NextFunction, Router } from "express"; -import { AppConfig, AuthProvider } from './AuthProvider'; -import UrlUtils from './UrlUtils'; +import { AppConfig, AuthProvider } from "./AuthProvider"; +import UrlUtils from "./UrlUtils"; type TokenRequest = Omit; const EMPTY_STRING = ""; export type AuthOptions = { - appConfig: AppConfig - authProvider: AuthProvider + appConfig: AppConfig; + authProvider: AuthProvider; protectedResources: { [route: string]: [string, TokenRequest]; - } + }; }; export const auth = (options: AuthOptions): Router => { @@ -28,46 +32,64 @@ export const auth = (options: AuthOptions): Router => { // ensure session is available appRouter.use((req: Request, res: Response, next: NextFunction) => { if (!req.session) { - throw new Error("Session not found. Please check your session middleware configuration."); + throw new Error( + "Session not found. Please check your session middleware configuration." + ); } next(); }); // handle redirect response from AAD - appRouter.post(UrlUtils.getPathFromUrl(options.appConfig.redirectUri), async (req: Request, res: Response, next: NextFunction) => { - try { - const tokenResponse = await options.authProvider.getTokenInteractive({ - ...req.session.tokenRequest!, - code: req.body.code, - }, req.body, req.session.id); - - req.session.isAuthenticated = true; - req.session.account = tokenResponse?.account!; // account won't be null in this grant type - - const { redirectTo } = JSON.parse(Buffer.from(req.body.state, "base64").toString("utf8")); - res.redirect(redirectTo); // redirect back to original route - } catch (error) { - next(error); + appRouter.post( + UrlUtils.getPathFromUrl(options.appConfig.redirectUri), + async (req: Request, res: Response, next: NextFunction) => { + try { + const tokenResponse = + await options.authProvider.getTokenInteractive( + { + ...req.session.tokenRequest!, + code: req.body.code, + }, + req.body, + req.session.id + ); + + req.session.isAuthenticated = true; + req.session.account = tokenResponse?.account!; // account won't be null in this grant type + + const { redirectTo } = JSON.parse( + Buffer.from(req.body.state, "base64").toString("utf8") + ); + res.redirect(redirectTo); // redirect back to original route + } catch (error) { + next(error); + } } - }); + ); // ensure user is authenticated appRouter.use(async (req: Request, res: Response, next: NextFunction) => { - const isRedirectUri = UrlUtils.getCanonicalUrlFromRequest(req) === UrlString.canonicalizeUri(options.appConfig.redirectUri); + const isRedirectUri = + UrlUtils.getCanonicalUrlFromRequest(req) === + UrlString.canonicalizeUri(options.appConfig.redirectUri); if (!req.session.isAuthenticated && !isRedirectUri) { - const { authCodeUrl, state } = await options.authProvider.prepareTokenRequest({ - responseMode: ResponseMode.FORM_POST, - redirectUri: options.appConfig.redirectUri, - scopes: [], - }, req.session.id); + const { authCodeUrl, state } = + await options.authProvider.prepareTokenRequest( + { + responseMode: ResponseMode.FORM_POST, + redirectUri: options.appConfig.redirectUri, + scopes: [], + }, + req.session.id + ); req.session.tokenRequest = { redirectUri: options.appConfig.redirectUri, scopes: [], state, - code: EMPTY_STRING + code: EMPTY_STRING, }; // save token request params to session, which will be used to acquire token after redirect return res.redirect(authCodeUrl); @@ -80,48 +102,66 @@ export const auth = (options: AuthOptions): Router => { Object.entries(options.protectedResources).forEach((value) => { const [route, [resource, tokenRequest]] = value; - appRouter.get(route, async (req: Request, res: Response, next: NextFunction) => { - try { - if (tokenRequest.authority && !tokenRequest.authority.includes(options.appConfig.tenantId)) { - throw new InteractionRequiredAuthError("New authority set - requires interaction."); - } - - const tokenResponse = await options.authProvider.getTokenSilent({ - ...tokenRequest, - account: req.session.account!, - }, req.session.id); - - req.session.protectedResources = { - ...req.session.protectedResources, - [resource]: { - callingRoute: route, - accessToken: tokenResponse?.accessToken!, + appRouter.get( + route, + async (req: Request, res: Response, next: NextFunction) => { + try { + if ( + tokenRequest.authority && + !tokenRequest.authority.includes( + options.appConfig.tenantId + ) + ) { + throw new InteractionRequiredAuthError( + "New authority set - requires interaction." + ); } - }; - - next(); - } catch (error) { - if (error instanceof InteractionRequiredAuthError) { - - const { authCodeUrl, state } = await options.authProvider.prepareTokenRequest({ - ...tokenRequest, - responseMode: ResponseMode.FORM_POST, - redirectUri: options.appConfig.redirectUri, - }, req.session.id, route); - req.session.tokenRequest = { - ...tokenRequest, - redirectUri: options.appConfig.redirectUri, - state, - code: EMPTY_STRING - }; // save token request params to session, which will be used to acquire token after redirect + const tokenResponse = + await options.authProvider.getTokenSilent( + { + ...tokenRequest, + account: req.session.account!, + }, + req.session.id + ); + + req.session.protectedResources = { + ...req.session.protectedResources, + [resource]: { + callingRoute: route, + accessToken: tokenResponse?.accessToken!, + }, + }; + + next(); + } catch (error) { + if (error instanceof InteractionRequiredAuthError) { + const { authCodeUrl, state } = + await options.authProvider.prepareTokenRequest( + { + ...tokenRequest, + responseMode: ResponseMode.FORM_POST, + redirectUri: options.appConfig.redirectUri, + }, + req.session.id, + route + ); + + req.session.tokenRequest = { + ...tokenRequest, + redirectUri: options.appConfig.redirectUri, + state, + code: EMPTY_STRING, + }; // save token request params to session, which will be used to acquire token after redirect + + return res.redirect(authCodeUrl); + } - return res.redirect(authCodeUrl); + next(error); } - - next(error); } - }); + ); }); return appRouter; diff --git a/samples/msal-node-samples/auth-code-key-vault/README.md b/samples/msal-node-samples/auth-code-key-vault/README.md index 7d62464d64..045424d9bd 100644 --- a/samples/msal-node-samples/auth-code-key-vault/README.md +++ b/samples/msal-node-samples/auth-code-key-vault/README.md @@ -19,20 +19,23 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-webapp`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. - - In the **Redirect URI (optional)** section, select **Web** in the combo-box and enter the following redirect URI: `http://localhost:3000/redirect`. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-webapp`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Redirect URI (optional)** section, select **Web** in the combo-box and enter the following redirect URI: `http://localhost:3000/redirect`. 1. Select **Register** to create the application. 1. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later. 1. In the app's registration screen, select the **Certificates & secrets** blade in the left. - - Click on **Upload** certificate and select the certificate file to upload. - - Click **Add**. Once the certificate is uploaded, the *thumbprint*, *start date*, and *expiration* values are displayed. + - Click on **Upload** certificate and select the certificate file to upload. + - Click **Add**. Once the certificate is uploaded, the _thumbprint_, _start date_, and _expiration_ values are displayed. Before running the sample, you will need to replace the values in the config: ```javascript -const KEY_VAULT_NAME = process.env["KEY_VAULT_NAME"] || "ENTER_YOUR_KEY_VAULT_NAME"; -const CERTIFICATE_NAME = process.env["CERTIFICATE_NAME"] || "ENTER_THE_NAME_OF_YOUR_CERTIFICATE_ON_KEY_VAULT"; +const KEY_VAULT_NAME = + process.env["KEY_VAULT_NAME"] || "ENTER_YOUR_KEY_VAULT_NAME"; +const CERTIFICATE_NAME = + process.env["CERTIFICATE_NAME"] || + "ENTER_THE_NAME_OF_YOUR_CERTIFICATE_ON_KEY_VAULT"; const config = { auth: { @@ -54,4 +57,4 @@ The server should start at port **3000**. Navigate to `https://localhost:3000` i ## More information -- [Microsoft identity platform application authentication certificate credentials](https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials) +- [Microsoft identity platform application authentication certificate credentials](https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials) diff --git a/samples/msal-node-samples/auth-code-pkce/README.md b/samples/msal-node-samples/auth-code-pkce/README.md index 588ad2822e..8d7cc92751 100644 --- a/samples/msal-node-samples/auth-code-pkce/README.md +++ b/samples/msal-node-samples/auth-code-pkce/README.md @@ -19,9 +19,9 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-webapp`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. - - In the **Redirect URI (optional)** section, select **Public client/native (mobile & desktop)** in the combo-box and enter the following redirect URI: `http://localhost:3000/redirect`. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-webapp`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Redirect URI (optional)** section, select **Public client/native (mobile & desktop)** in the combo-box and enter the following redirect URI: `http://localhost:3000/redirect`. 1. Select **Register** to create the application. 1. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later. @@ -32,7 +32,7 @@ const config = { auth: { clientId: "ENTER_CLIENT_ID", authority: "https://login.microsoftonline.com/ENTER_TENANT_ID", - } + }, }; ``` @@ -48,4 +48,4 @@ The server should start at port **3000**. Navigate to `http://localhost:3000` in ## More information -- [Microsoft identity platform OAuth 2.0 Authorization Code Grant](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) +- [Microsoft identity platform OAuth 2.0 Authorization Code Grant](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) diff --git a/samples/msal-node-samples/auth-code-pkce/src/index.ts b/samples/msal-node-samples/auth-code-pkce/src/index.ts index e1e4dfe9f8..a38d3b66a3 100644 --- a/samples/msal-node-samples/auth-code-pkce/src/index.ts +++ b/samples/msal-node-samples/auth-code-pkce/src/index.ts @@ -4,9 +4,16 @@ */ import express from "express"; import session from "express-session"; -import { PublicClientApplication, AuthorizationCodeRequest, LogLevel, CryptoProvider, AuthorizationUrlRequest, Configuration } from "@azure/msal-node"; +import { + PublicClientApplication, + AuthorizationCodeRequest, + LogLevel, + CryptoProvider, + AuthorizationUrlRequest, + Configuration, +} from "@azure/msal-node"; import { RequestWithPKCE } from "./types"; -import 'dotenv/config'; +import "dotenv/config"; const SERVER_PORT = process.env.PORT || 3000; @@ -14,17 +21,21 @@ const SERVER_PORT = process.env.PORT || 3000; const config: Configuration = { auth: { clientId: "ENTER_CLIENT_ID", - authority: "https://login.microsoftonline.com/ENTER_TENANT_ID" + authority: "https://login.microsoftonline.com/ENTER_TENANT_ID", }, system: { loggerOptions: { - loggerCallback(loglevel: LogLevel, message: string, containsPii: boolean) { + loggerCallback( + loglevel: LogLevel, + message: string, + containsPii: boolean + ) { console.log(message); }, piiLoggingEnabled: false, logLevel: LogLevel.Verbose, - } - } + }, + }, }; // Create msal application object @@ -43,18 +54,17 @@ const sessionConfig = { saveUninitialized: false, cookie: { secure: false, // set this to true on production - } -} + }, +}; -if (app.get('env') === 'production') { - app.set('trust proxy', 1) // trust first proxy e.g. App Service - sessionConfig.cookie.secure = true // serve secure cookies +if (app.get("env") === "production") { + app.set("trust proxy", 1); // trust first proxy e.g. App Service + sessionConfig.cookie.secure = true; // serve secure cookies } app.use(session(sessionConfig)); -app.get('/', (req: RequestWithPKCE, res) => { - +app.get("/", (req: RequestWithPKCE, res) => { /** * Proof Key for Code Exchange (PKCE) Setup * @@ -77,7 +87,7 @@ app.get('/', (req: RequestWithPKCE, res) => { // create session object if does not exist if (!req.session.pkceCodes) { req.session.pkceCodes = { - challengeMethod: 'S256' + challengeMethod: "S256", }; } @@ -90,33 +100,41 @@ app.get('/', (req: RequestWithPKCE, res) => { scopes: ["user.read"], redirectUri: "http://localhost:3000/redirect", codeChallenge: req.session.pkceCodes.challenge, // PKCE Code Challenge - codeChallengeMethod: req.session.pkceCodes.challengeMethod // PKCE Code Challenge Method + codeChallengeMethod: req.session.pkceCodes.challengeMethod, // PKCE Code Challenge Method }; // Get url to sign user in and consent to scopes needed for applicatio - pca.getAuthCodeUrl(authCodeUrlParameters).then((response) => { - res.redirect(response); - }).catch((error) => console.log(JSON.stringify(error))); + pca.getAuthCodeUrl(authCodeUrlParameters) + .then((response) => { + res.redirect(response); + }) + .catch((error) => console.log(JSON.stringify(error))); }); }); -app.get('/redirect', (req: RequestWithPKCE, res) => { +app.get("/redirect", (req: RequestWithPKCE, res) => { // Add PKCE code verifier to token request object const tokenRequest: AuthorizationCodeRequest = { code: req.query.code as string, scopes: ["user.read"], redirectUri: "http://localhost:3000/redirect", codeVerifier: req.session.pkceCodes.verifier, // PKCE Code Verifier - clientInfo: req.query.client_info as string + clientInfo: req.query.client_info as string, }; - pca.acquireTokenByCode(tokenRequest).then((response) => { - console.log("\nResponse: \n:", response); - res.sendStatus(200); - }).catch((error) => { - console.log(error); - res.status(500).send(error); - }); + pca.acquireTokenByCode(tokenRequest) + .then((response) => { + console.log("\nResponse: \n:", response); + res.sendStatus(200); + }) + .catch((error) => { + console.log(error); + res.status(500).send(error); + }); }); -app.listen(SERVER_PORT, () => console.log(`Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!`)) +app.listen(SERVER_PORT, () => + console.log( + `Msal Node Auth Code Sample app listening on port ${SERVER_PORT}!` + ) +); diff --git a/samples/msal-node-samples/auth-code-pkce/src/types/index.ts b/samples/msal-node-samples/auth-code-pkce/src/types/index.ts index b3e6097da5..6ca1bad83c 100644 --- a/samples/msal-node-samples/auth-code-pkce/src/types/index.ts +++ b/samples/msal-node-samples/auth-code-pkce/src/types/index.ts @@ -1,12 +1,12 @@ -import { Request} from "express"; +import { Request } from "express"; import { Session } from "express-session"; -export type RequestWithPKCE = Request & { +export type RequestWithPKCE = Request & { session: Session & { pkceCodes: { - challengeMethod: string, - challenge?: string, - verifier?: string - } - } + challengeMethod: string; + challenge?: string; + verifier?: string; + }; + }; }; diff --git a/samples/msal-node-samples/auth-code-with-certs/README.md b/samples/msal-node-samples/auth-code-with-certs/README.md index da9b4e2b42..01a5de5ebe 100644 --- a/samples/msal-node-samples/auth-code-with-certs/README.md +++ b/samples/msal-node-samples/auth-code-with-certs/README.md @@ -17,14 +17,14 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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 `ms-identity-nodejs-webapp`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. - - In the **Redirect URI (optional)** section, select **Web** in the combo-box and enter the following redirect URI: `https://localhost:3000/redirect`. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `ms-identity-nodejs-webapp`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Redirect URI (optional)** section, select **Web** in the combo-box and enter the following redirect URI: `https://localhost:3000/redirect`. 1. Select **Register** to create the application. 1. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later. 1. In the app's registration screen, select the **Certificates & secrets** blade in the left. - - Click on **Upload** certificate and select the certificate file to upload. - - Click **Add**. Once the certificate is uploaded, the *thumbprint*, *start date*, and *expiration* values are displayed. + - Click on **Upload** certificate and select the certificate file to upload. + - Click **Add**. Once the certificate is uploaded, the _thumbprint_, _start date_, and _expiration_ values are displayed. ### Using secrets and certificates securely @@ -46,8 +46,8 @@ const config = { clientCertificate: { thumbprint: process.env.clientCertificate, privateKey: process.env.privateKey, - } - } + }, + }, }; ``` @@ -63,4 +63,4 @@ The server should start at port **3000**. Navigate to `https://localhost:3000` i ## More information -- [Microsoft identity platform application authentication certificate credentials](https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials) +- [Microsoft identity platform application authentication certificate credentials](https://docs.microsoft.com/azure/active-directory/develop/active-directory-certificate-credentials) diff --git a/samples/msal-node-samples/auth-code/README.md b/samples/msal-node-samples/auth-code/README.md index cafccbe269..0c31fd52c2 100644 --- a/samples/msal-node-samples/auth-code/README.md +++ b/samples/msal-node-samples/auth-code/README.md @@ -1,13 +1,14 @@ -# MSAL Node Sample: Auth Code +# MSAL Node Sample: Auth Code This sample application demonstrates how to use the Authorization Code Grant APIs provided by MSAL Node.js in a Node application. Once MSAL Node is installed, and you have the right files, come here to learn about this scenario. ### How is this scenario used? -The Auth Code flow is most commonly used for a web app that signs in users. General information about this scenario is available [here](https://docs.microsoft.com/azure/active-directory/develop/scenario-web-app-sign-user-overview?tabs=aspnetcore). ->**Note: Although this sample application has a web server component that allows the user to input their credentials in the browser, it is important to remember that MSAL Node does not support browser-based Single Page Applications. If you are looking to use the authorization code grant to acquire tokens in a Single-Page Application, please use [MSAL Browser](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser).** +The Auth Code flow is most commonly used for a web app that signs in users. General information about this scenario is available [here](https://docs.microsoft.com/azure/active-directory/develop/scenario-web-app-sign-user-overview?tabs=aspnetcore). + +> **Note: Although this sample application has a web server component that allows the user to input their credentials in the browser, it is important to remember that MSAL Node does not support browser-based Single Page Applications. If you are looking to use the authorization code grant to acquire tokens in a Single-Page Application, please use [MSAL Browser](https://github.com/AzureAD/microsoft-authentication-library-for-js/tree/dev/lib/msal-browser).** ## Test the Sample @@ -17,42 +18,43 @@ Open the `config/customConfig.json` file. We will change this to add details about our app registration and deployment. -By default, this configuration is set to support all Microsoft accounts. This includes Microsoft Entra accounts used by organizations, and MSA accounts typically used by consumers. +By default, this configuration is set to support all Microsoft accounts. This includes Microsoft Entra accounts used by organizations, and MSA accounts typically used by consumers. Before proceeding, go to the Microsoft Entra admin center, and open the app registration for this app. #### **Client ID** -Within the "Overview" you will see a GUID labeled **Application (client) ID**. Copy this GUID to the clientId field in the config. +Within the "Overview" you will see a GUID labeled **Application (client) ID**. Copy this GUID to the clientId field in the config. Click the **Authentication** link in the left nav. #### **Authority** + Check that supported account types are: **Accounts in any organizational directory (Any Microsoft Entra ID directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)** If so, then set the authority attribute in the JSON configuraiton file to `https://login.microsoftonline.com/common` -For other supported account types, review the other [Authority options](https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration). Unless there is a specific need to restrict users of your app to an organization, we strongly suggest that everyone use the default authority. User restrictions can be placed later in the application flow if needed. +For other supported account types, review the other [Authority options](https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration). Unless there is a specific need to restrict users of your app to an organization, we strongly suggest that everyone use the default authority. User restrictions can be placed later in the application flow if needed. #### **Client Secret** If your AzureAD app registration is configured as a Confidential Client Application, you'll have to add a `clientSecret` attribute to a `.env` file and change the `PublicClientApplication` object in the sample's `index.js` file into a `ConfidentialClientApplication` object. This secret helps prevent third parties from using your app registration. + 1. Click on `Certificates and Secrets` in the left nav. 1. Click `New Client Secret` and pick an expiry. 1. Click the `Copy to Clipboard` icon, add this client secret to the `.env` file as `CLIENT_SECRET`. **auth-code/config/customConfig.json** + ```json { - "authOptions": - { - "clientId": "YOUR_CLIENT_ID", - "authority": "YOUR_AUTHORITY" - }, - "request": - { + "authOptions": { + "clientId": "YOUR_CLIENT_ID", + "authority": "YOUR_AUTHORITY" + }, + "request": { "authCodeUrlParameters": { "scopes": ["user.read"], "redirectUri": "http://localhost:3000/redirect" @@ -62,8 +64,7 @@ This secret helps prevent third parties from using your app registration. "scopes": ["user.read"] } }, - "resourceApi": - { + "resourceApi": { "endpoint": "https://graph.microsoft.com/v1.0/me" } } @@ -78,23 +79,27 @@ CLIENT_SECRET= **auth-code/index.js** ```javascript - // Change this - const publicClientApplication = new msal.PublicClientApplication(clientConfig); - return getTokenAuthCode(config, publicClientApplication, null); - - // To this - const confidentialClientApplication = new msal.ConfidentialClientApplication(clientConfig); - return getTokenAuthCode(config, confidentialClientApplication, null); +// Change this +const publicClientApplication = new msal.PublicClientApplication(clientConfig); +return getTokenAuthCode(config, publicClientApplication, null); + +// To this +const confidentialClientApplication = new msal.ConfidentialClientApplication( + clientConfig +); +return getTokenAuthCode(config, confidentialClientApplication, null); ``` + 🎉You have finished the basic configuration!🎉 ### Executing the application -From the command line, let npm install any needed dependencies. This only needs to be done once. +From the command line, let npm install any needed dependencies. This only needs to be done once. ```bash $ npm install ``` + 1. Once the dependencies are installed, you can run the sample application by using the following command: ```bash @@ -111,25 +116,24 @@ To customize the start script, review the `package.json` file. ### Import the Configuration Object -If you set up the sample with your app registration, you may be able to copy this object directly into your application. - +If you set up the sample with your app registration, you may be able to copy this object directly into your application. ```javascript const config = { auth: { clientId: "YOUR_CLIENT_ID", authority: "YOUR_AUTHORITY", - clientSecret: process.env.CLIENT_SECRET // Only for Confidential Client Applications + clientSecret: process.env.CLIENT_SECRET, // Only for Confidential Client Applications + }, + system: { + loggerOptions: { + loggerCallback(loglevel, message, containsPii) { + console.log(message); + }, + piiLoggingEnabled: false, + logLevel: msal.LogLevel.Verbose, + }, }, -    system: { -        loggerOptions: { -            loggerCallback(loglevel, message, containsPii) { -                console.log(message); -            }, -         piiLoggingEnabled: false, -         logLevel: msal.LogLevel.Verbose, -        } -    } }; ``` @@ -138,12 +142,11 @@ const config = { Add the dependency on MSAL Node to your Node app. ```js -const msal = require('@azure/msal-node'); +const msal = require("@azure/msal-node"); ``` ### Initialize MSAL Node at runtime - Initialize the app object within your web app. If you've configured a Public Client Application: @@ -160,18 +163,19 @@ const cca = new msal.ConfidentialClientApplication(config); ### Configure Sign In Request -Choose the route that requires sign in. Within that route, set up permissions, and direct the MSAL Node app object to attempt sign in. +Choose the route that requires sign in. Within that route, set up permissions, and direct the MSAL Node app object to attempt sign in. -In our sample, we immediately sign in the user. If you want all users to be logged in before they view anything, then you can use the same process. -We add our sign in code to the default route. +In our sample, we immediately sign in the user. If you want all users to be logged in before they view anything, then you can use the same process. +We add our sign in code to the default route. ```js app.get('/', (req, res) => { ``` -Next, we have to pick the `scopes` related to the user. If we are logging in a user, then we must at least request access to basic user information. The default scope of `user.read` grants that basic access. To learn more see the [Microsoft Graph permissions reference](https://docs.microsoft.com/graph/permissions-reference). +Next, we have to pick the `scopes` related to the user. If we are logging in a user, then we must at least request access to basic user information. The default scope of `user.read` grants that basic access. To learn more see the [Microsoft Graph permissions reference](https://docs.microsoft.com/graph/permissions-reference). **auth-code/config/customConfig.json:** + ```json { ..., @@ -186,48 +190,57 @@ Next, we have to pick the `scopes` related to the user. If we are logging in a ... ``` -The ```redirectUri``` is the return route. After logging in a user, they will hit this route. Your application logic will take over here. You will want to customize the redirectUri for your application. +The `redirectUri` is the return route. After logging in a user, they will hit this route. Your application logic will take over here. You will want to customize the redirectUri for your application. -Next we direct the user to authenticate. The following code block directs the user based on the Authority we set in the config, and directs the user as needed. +Next we direct the user to authenticate. The following code block directs the user based on the Authority we set in the config, and directs the user as needed. **auth-code/index.js:** ```javascript - clientApplication.getAuthCodeUrl(authCodeUrlParameters).then((response) => { +clientApplication + .getAuthCodeUrl(authCodeUrlParameters) + .then((response) => { res.redirect(response); - }).catch((error) => console.log(JSON.stringify(error))); + }) + .catch((error) => console.log(JSON.stringify(error))); ``` Putting together the routing and all the logic for starting the sign in yields the following code: ```javascript -app.get('/', (req, res) => { +app.get("/", (req, res) => { // You can also build the authCodeUrlParameters object directly in the JavaScript file like this const authCodeUrlParameters = { scopes: ["user.read"], redirectUri: "http://localhost:3000/redirect", }; - clientApplication.getAuthCodeUrl(authCodeUrlParameters).then((response) => { - res.redirect(response); - }).catch((error) => console.log(JSON.stringify(error))); + clientApplication + .getAuthCodeUrl(authCodeUrlParameters) + .then((response) => { + res.redirect(response); + }) + .catch((error) => console.log(JSON.stringify(error))); }); ``` + ### Configure Sign In Response The next step occurs after the redirect. -Your application must first *complete* the sign in flow by processing the code and validating the incoming request. +Your application must first _complete_ the sign in flow by processing the code and validating the incoming request. -First, configure the route where you will receive the response. This must match your application configuration on the Microsoft Entra admin center. +First, configure the route where you will receive the response. This must match your application configuration on the Microsoft Entra admin center. **auth-code/index.js:** + ```javascript app.get('/redirect', (req, res) => { ``` -Next, your app logic will validate the scopes and route. These settings must match the request. Make sure the `scopes` match the request. Make sure the `redirectUri` matches the app registration, and the route. +Next, your app logic will validate the scopes and route. These settings must match the request. Make sure the `scopes` match the request. Make sure the `redirectUri` matches the app registration, and the route. **auth-code/config/customConfig.json:** + ```json { ..., @@ -242,24 +255,29 @@ Next, your app logic will validate the scopes and route. These settings must m ... ``` -The above JSON is the *configuration* for the access token request. The following code validates and executes the token request to complete Sign In. +The above JSON is the _configuration_ for the access token request. The following code validates and executes the token request to complete Sign In. **auth-code/index.js** + ```javascript - tokenRequest.code = "AUTH_CODE_FROM_RESPONSE"; +tokenRequest.code = "AUTH_CODE_FROM_RESPONSE"; - clientApplication.acquireTokenByCode(tokenRequest).then((response) => { +clientApplication + .acquireTokenByCode(tokenRequest) + .then((response) => { console.log("\nResponse: \n:", response); res.sendStatus(200); - }).catch((error) => { + }) + .catch((error) => { console.log(error); res.status(500).send(error); }); ``` + Putting together the routing and all the logic for completing the sign in yields the following code: ```js -app.get('/redirect', (req, res) => { +app.get("/redirect", (req, res) => { // You can also build the tokenRequest object directly in the JavaScript file like this const tokenRequest = { // The URL from the redirect will contain the Auth Code in the query parameters @@ -269,18 +287,21 @@ app.get('/redirect', (req, res) => { }; // Pass the tokenRequest object with the Auth Code, scopes and redirectUri to acquireTokenByCode API - clientApplication.acquireTokenByCode(tokenRequest).then((response) => { - console.log("\nResponse: \n:", response); - res.sendStatus(200); - }).catch((error) => { - console.log(error); - res.status(500).send(error); - }); + clientApplication + .acquireTokenByCode(tokenRequest) + .then((response) => { + console.log("\nResponse: \n:", response); + res.sendStatus(200); + }) + .catch((error) => { + console.log(error); + res.status(500).send(error); + }); }); ``` ### The User Experience -What happens if the user logs in, closes the window, returns to the site, and logs in again? Microsoft supports many, many complex scenarios with many, many forms of authentication: certificates, hardware keys, federated experiences, and even biometrics in some cases. Let our library handle the complexity of deciding the simplest way of logging in the user. +What happens if the user logs in, closes the window, returns to the site, and logs in again? Microsoft supports many, many complex scenarios with many, many forms of authentication: certificates, hardware keys, federated experiences, and even biometrics in some cases. Let our library handle the complexity of deciding the simplest way of logging in the user. Silent flows are not used with the this scenario. See [Authentication Flows](https://docs.microsoft.com/en-us/azure/active-directory/develop/msal-authentication-flows) for a discussion of the interaction between flows. diff --git a/samples/msal-node-samples/b2c-user-flows/README.md b/samples/msal-node-samples/b2c-user-flows/README.md index c1d69ae405..32dcb912ac 100644 --- a/samples/msal-node-samples/b2c-user-flows/README.md +++ b/samples/msal-node-samples/b2c-user-flows/README.md @@ -4,9 +4,9 @@ This sample demonstrates a [confidential client application](../../../lib/msal-n 1. [OIDC Connect protocol](https://docs.microsoft.com/azure/active-directory-b2c/openid-connect) to implement standard B2C [user-flows](https://docs.microsoft.com/azure/active-directory-b2c/user-flow-overview) for: -- sign-up/sign-in a user -- reset/recover a user password -- edit a user profile +- sign-up/sign-in a user +- reset/recover a user password +- edit a user profile 2. [Authorization code grant](https://docs.microsoft.com/azure/active-directory-b2c/authorization-code-flow) to acquire an [Access Token](https://docs.microsoft.com/azure/active-directory-b2c/tokens-overview) to call a [protected web API](https://docs.microsoft.com/azure/active-directory-b2c/add-web-api-application?tabs=app-reg-ga) (also on Azure AD B2C). @@ -50,31 +50,33 @@ In `index.js`, we setup the configuration object expected by MSAL Node `confiden **index.js** ```javascript - const confidentialClientConfig = { - auth: { - clientId: config.authOptions.clientId, - authority: config.policies.authorities.signUpSignIn.authority, - clientSecret: process.env.CLIENT_SECRET, - knownAuthorities: [config.policies.authorityDomain], - } - }; +const confidentialClientConfig = { + auth: { + clientId: config.authOptions.clientId, + authority: config.policies.authorities.signUpSignIn.authority, + clientSecret: process.env.CLIENT_SECRET, + knownAuthorities: [config.policies.authorityDomain], + }, +}; - // Create an MSAL PublicClientApplication object - const confidentialClientApp = new msal.ConfidentialClientApplication(confidentialClientConfig); +// Create an MSAL PublicClientApplication object +const confidentialClientApp = new msal.ConfidentialClientApplication( + confidentialClientConfig +); ``` Implementing B2C user-flows is a matter of initiating authorization requests against the corresponding authorities. Some user-flows are slightly more complex. For example, to initiate the **password-reset**, the user first needs to click on the **forgot my password** link on the Azure sign-in screen, which causes B2C service to respond with an error. We then catch this error, and trigger another authorization request, this time against the `https://fabrikamb2c.b2clogin.com/fabrikamb2c.onmicrosoft.com/B2C_1_reset` authority. > :information_source: This sample demonstrates the legacy password-reset user-flow. There's now a [new password reset experience](https://docs.microsoft.com/azure/active-directory-b2c/add-password-reset-policy?pivots=b2c-user-flow#self-service-password-reset-recommended) that is part of the sign-up or sign-in policy. As such, you don't need a separate policy for password reset anymore. -In order to keep track of these *flows*, we create request objects, attach them to the session variable and manipulate them in the rest of the application. +In order to keep track of these _flows_, we create request objects, attach them to the session variable and manipulate them in the rest of the application. ```javascript const APP_STAGES = { - SIGN_IN: 'sign_in', - PASSWORD_RESET: 'password_reset', - EDIT_PROFILE: 'edit_profile', - ACQUIRE_TOKEN: 'acquire_token' + SIGN_IN: "sign_in", + PASSWORD_RESET: "password_reset", + EDIT_PROFILE: "edit_profile", + ACQUIRE_TOKEN: "acquire_token", }; ``` @@ -91,7 +93,7 @@ const cca = new msal.ConfidentialClientApplication(confidentialClientConfig); Setup an Express route for initiating the sign-in flow: ```javascript -app.get('/sign-in', (req, res, next) => { +app.get("/sign-in", (req, res, next) => { // create a GUID against crsf req.session.csrfToken = cryptoProvider.createNewGuid(); @@ -112,10 +114,15 @@ app.get('/sign-in', (req, res, next) => { state: state, }; - const authCodeRequestParams = { - }; + const authCodeRequestParams = {}; - return redirectToAuthCodeUrl(req, res, next, authCodeUrlRequestParams, authCodeRequestParams); + return redirectToAuthCodeUrl( + req, + res, + next, + authCodeUrlRequestParams, + authCodeRequestParams + ); }); ``` @@ -124,13 +131,19 @@ app.get('/sign-in', (req, res, next) => { Create a helper method to prepare request parameters that will be passed to MSAL Node's `getAuthCodeUrl` API, which triggers the first leg of auth code flow. ```javascript -const redirectToAuthCodeUrl = async (req, res, next, authCodeUrlRequestParams, authCodeRequestParams) => { +const redirectToAuthCodeUrl = async ( + req, + res, + next, + authCodeUrlRequestParams, + authCodeRequestParams +) => { // Generate PKCE Codes before starting the authorization flow const { verifier, challenge } = await cryptoProvider.generatePkceCodes(); // Set generated PKCE codes and method as session vars req.session.pkceCodes = { - challengeMethod: 'S256', + challengeMethod: "S256", verifier: verifier, challenge: challenge, }; @@ -146,7 +159,7 @@ const redirectToAuthCodeUrl = async (req, res, next, authCodeUrlRequestParams, a redirectUri: REDIRECT_URI, codeChallenge: req.session.pkceCodes.challenge, codeChallengeMethod: req.session.pkceCodes.challengeMethod, - responseMode: 'form_post', // recommended for confidential clients + responseMode: "form_post", // recommended for confidential clients ...authCodeUrlRequestParams, }; @@ -158,7 +171,9 @@ const redirectToAuthCodeUrl = async (req, res, next, authCodeUrlRequestParams, a // Get url to sign user in and consent to scopes needed for application try { - const authCodeUrlResponse = await clientApplication.getAuthCodeUrl(req.session.authCodeUrlRequest); + const authCodeUrlResponse = await clientApplication.getAuthCodeUrl( + req.session.authCodeUrlRequest + ); res.redirect(authCodeUrlResponse); } catch (error) { next(error); @@ -174,9 +189,9 @@ The second leg of the auth code flow consists of handling the redirect response ```javascript // Second leg of auth code grant -app.post('/redirect', async (req, res, next) => { +app.post("/redirect", async (req, res, next) => { if (!req.body.state) { - return next(new Error('State not found')); + return next(new Error("State not found")); } // read the state object and determine the stage of the flow @@ -186,24 +201,32 @@ app.post('/redirect', async (req, res, next) => { switch (state.appStage) { case APP_STAGES.SIGN_IN: req.session.authCodeRequest.code = req.body.code; // authZ code - req.session.authCodeRequest.codeVerifier = req.session.pkceCodes.verifier // PKCE Code Verifier + req.session.authCodeRequest.codeVerifier = + req.session.pkceCodes.verifier; // PKCE Code Verifier try { - const tokenResponse = await clientApplication.acquireTokenByCode(req.session.authCodeRequest); + const tokenResponse = + await clientApplication.acquireTokenByCode( + req.session.authCodeRequest + ); req.session.account = tokenResponse.account; req.session.isAuthenticated = true; - res.redirect('/'); + res.redirect("/"); } catch (error) { if (req.body.error) { - /** * When the user selects 'forgot my password' on the sign-in page, B2C service will throw an error. * We are to catch this error and redirect the user to LOGIN again with the resetPassword authority. * For more information, visit: https://docs.microsoft.com/azure/active-directory-b2c/user-flow-overview#linking-user-flows */ - if (JSON.stringify(req.body.error_description).includes('AADB2C90118')) { + if ( + JSON.stringify(req.body.error_description).includes( + "AADB2C90118" + ) + ) { // create a GUID against crsf - req.session.csrfToken = cryptoProvider.createNewGuid(); + req.session.csrfToken = + cryptoProvider.createNewGuid(); const state = cryptoProvider.base64Encode( JSON.stringify({ @@ -213,15 +236,22 @@ app.post('/redirect', async (req, res, next) => { ); const authCodeUrlRequestParams = { - authority: scenarioConfig.policies.authorities.resetPassword.authority, + authority: + scenarioConfig.policies.authorities + .resetPassword.authority, state: state, }; - const authCodeRequestParams = { - }; + const authCodeRequestParams = {}; // if coming for password reset, set the authority to password reset - return redirectToAuthCodeUrl(req, res, next, authCodeUrlRequestParams, authCodeRequestParams); + return redirectToAuthCodeUrl( + req, + res, + next, + authCodeUrlRequestParams, + authCodeRequestParams + ); } } next(error); @@ -230,12 +260,16 @@ app.post('/redirect', async (req, res, next) => { break; case APP_STAGES.ACQUIRE_TOKEN: req.session.authCodeRequest.code = req.body.code; // authZ code - req.session.authCodeRequest.codeVerifier = req.session.pkceCodes.verifier // PKCE Code Verifier + req.session.authCodeRequest.codeVerifier = + req.session.pkceCodes.verifier; // PKCE Code Verifier try { - const tokenResponse = await clientApplication.acquireTokenByCode(req.session.authCodeRequest); + const tokenResponse = + await clientApplication.acquireTokenByCode( + req.session.authCodeRequest + ); req.session.accessToken = tokenResponse.accessToken; - res.redirect('/call-api'); + res.redirect("/call-api"); } catch (error) { next(error); } @@ -244,13 +278,13 @@ app.post('/redirect', async (req, res, next) => { case APP_STAGES.PASSWORD_RESET: case APP_STAGES.EDIT_PROFILE: // redirect the user to sign-in again - res.redirect('/sign-in'); + res.redirect("/sign-in"); break; default: - next(new Error('cannot determine app stage')); + next(new Error("cannot determine app stage")); } } else { - next(new Error('crsf token mismatch')); + next(new Error("crsf token mismatch")); } }); ``` @@ -260,11 +294,12 @@ app.post('/redirect', async (req, res, next) => { The `getToken` method below first checks if there is a non-expired access token in the cache for this user via msal-node's `acquireTokenSilent` API; if the access token is expired but the refresh token is not, it exchanges the refresh token for a new access token. If the refresh token is expired as well, it initiates the first leg of authorization code flow to request a new access token from Azure AD B2C: ```javascript - const getToken = async (req, res, next, scopes) => { try { const tokenCache = clientApplication.getTokenCache(); - const account = await tokenCache.getAccountByHomeId(req.session.account.homeAccountId); + const account = await tokenCache.getAccountByHomeId( + req.session.account.homeAccountId + ); const silentRequest = { account: account, @@ -272,7 +307,9 @@ const getToken = async (req, res, next, scopes) => { }; // acquire token silently to be used in resource call - const tokenResponse = await clientApplication.acquireTokenSilent(silentRequest); + const tokenResponse = await clientApplication.acquireTokenSilent( + silentRequest + ); return tokenResponse; } catch (error) { if (error instanceof msal.InteractionRequiredAuthError) { @@ -286,7 +323,8 @@ const getToken = async (req, res, next, scopes) => { ); const authCodeUrlRequestParams = { - authority: scenarioConfig.policies.authorities.signUpSignIn.authority, + authority: + scenarioConfig.policies.authorities.signUpSignIn.authority, state: state, }; @@ -294,10 +332,16 @@ const getToken = async (req, res, next, scopes) => { scopes: scopes, }; - return redirectToAuthCodeUrl(req, res, next, authCodeUrlRequestParams, authCodeRequestParams); + return redirectToAuthCodeUrl( + req, + res, + next, + authCodeUrlRequestParams, + authCodeRequestParams + ); } next(error); } -} +}; ``` diff --git a/samples/msal-node-samples/b2c-user-flows/test/user-flows-msa.spec.ts b/samples/msal-node-samples/b2c-user-flows/test/user-flows-msa.spec.ts index f626e8b0db..3cf38d71c1 100644 --- a/samples/msal-node-samples/b2c-user-flows/test/user-flows-msa.spec.ts +++ b/samples/msal-node-samples/b2c-user-flows/test/user-flows-msa.spec.ts @@ -74,7 +74,7 @@ describe("B2C User Flow Tests", () => { envResponse[0], labClient ); - + // TODO: Remove when B2C MSA account is available in the lab username = B2C_MSA_TEST_UPN; }); diff --git a/samples/msal-node-samples/client-credentials-distributed-cache/README.md b/samples/msal-node-samples/client-credentials-distributed-cache/README.md index 80859281c0..78010e0a8c 100644 --- a/samples/msal-node-samples/client-credentials-distributed-cache/README.md +++ b/samples/msal-node-samples/client-credentials-distributed-cache/README.md @@ -17,30 +17,31 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-daemon`. - - Under **Supported account types**, select **Accounts in any organizational directory (Any Microsoft Entra ID directory - Multitenant)**. - - In the **Redirect URI (optional)** section, select **Public client/native (mobile & desktop)** in the combo-box and enter the following redirect URI: `http://localhost` (this is required for provisioning the app into other tenants via admin consent). + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-daemon`. + - Under **Supported account types**, select **Accounts in any organizational directory (Any Microsoft Entra ID directory - Multitenant)**. + - In the **Redirect URI (optional)** section, select **Public client/native (mobile & desktop)** in the combo-box and enter the following redirect URI: `http://localhost` (this is required for provisioning the app into other tenants via admin consent). 1. Select **Register** to create the application. 1. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later. 1. In the app's registration screen, select the **Certificates & secrets** blade in the left. - - In the **Client secrets** section, select **New client secret**. - - Type a key description (for instance `app secret`), - - Select one of the available key durations (6 months, 12 months or Custom) as per your security posture. - - The generated key value will be displayed when you select the **Add** button. Record this value for use in a later step (it's shown only once). - > :warning: In production, use certificates with Azure Key Vault instead of secrets. See [certificate-credentials.md](../../../lib/msal-node/docs/certificate-credentials.md) and [key-vault.md](../../../lib/msal-node/docs/key-vault-managed-identity.md) for more information and examples. + - In the **Client secrets** section, select **New client secret**. + - Type a key description (for instance `app secret`), + - Select one of the available key durations (6 months, 12 months or Custom) as per your security posture. + - The generated key value will be displayed when you select the **Add** button. Record this value for use in a later step (it's shown only once). + > :warning: In production, use certificates with Azure Key Vault instead of secrets. See [certificate-credentials.md](../../../lib/msal-node/docs/certificate-credentials.md) and [key-vault.md](../../../lib/msal-node/docs/key-vault-managed-identity.md) for more information and examples. 1. In the app's registration screen, select the API permissions blade in the left to open the page where we add access to the APIs that your application needs. - - Select the **Add a permission** button and then, - - Ensure that the **Microsoft APIs** tab is selected. - - In the **Commonly used Microsoft APIs** section, select **Microsoft Graph** - - In the **Application permissions** section, select the **User.Read.All** in the list. Use the search box if necessary. - - Select the **Add permissions** button at the bottom. - - Finally, grant **admin consent** for this scope. + - Select the **Add a permission** button and then, + - Ensure that the **Microsoft APIs** tab is selected. + - In the **Commonly used Microsoft APIs** section, select **Microsoft Graph** + - In the **Application permissions** section, select the **User.Read.All** in the list. Use the search box if necessary. + - 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 the configuration object (see [index.ts](./src/index.ts)): ```typescript const appConfig: AppConfig = { - instance: options.instance || process.env.INSTANCE || "ENTER_CLOUD_INSTANCE_HERE", + instance: + options.instance || process.env.INSTANCE || "ENTER_CLOUD_INSTANCE_HERE", tenantId: options.tenant || process.env.TENANT_ID || "ENTER_TENANT_ID_HERE", clientId: process.env.CLIENT_ID || "ENTER_CLIENT_ID_HERE", clientSecret: process.env.CLIENT_SECRET || "ENTER_CLIENT_SECRET_HERE", @@ -84,6 +85,6 @@ For persisting tokens using a distributed cache, multitenant daemon apps should ## More information -- [Tenancy in Microsoft Entra ID](https://learn.microsoft.com/azure/active-directory/develop/single-and-multi-tenant-apps) -- [Making your application multi-tenant](https://learn.microsoft.com/azure/active-directory/develop/howto-convert-app-to-be-multi-tenant) -- [Admin consent on the Microsoft identity platform](https://learn.microsoft.com/azure/active-directory/develop/v2-admin-consent) +- [Tenancy in Microsoft Entra ID](https://learn.microsoft.com/azure/active-directory/develop/single-and-multi-tenant-apps) +- [Making your application multi-tenant](https://learn.microsoft.com/azure/active-directory/develop/howto-convert-app-to-be-multi-tenant) +- [Admin consent on the Microsoft identity platform](https://learn.microsoft.com/azure/active-directory/develop/v2-admin-consent) diff --git a/samples/msal-node-samples/client-credentials-distributed-cache/src/AuthProvider.ts b/samples/msal-node-samples/client-credentials-distributed-cache/src/AuthProvider.ts index cce72394bc..175485897e 100644 --- a/samples/msal-node-samples/client-credentials-distributed-cache/src/AuthProvider.ts +++ b/samples/msal-node-samples/client-credentials-distributed-cache/src/AuthProvider.ts @@ -10,12 +10,12 @@ import { LogLevel, ConfidentialClientApplication, ClientCredentialRequest, - AuthenticationResult + AuthenticationResult, } from "@azure/msal-node"; import CustomCachePlugin from "./CustomCachePlugin"; import RedisClientWrapper from "./RedisClientWrapper"; -import AxiosHelper from './AxiosHelper'; +import AxiosHelper from "./AxiosHelper"; export type AppConfig = { instance: string; @@ -29,7 +29,11 @@ export class AuthProvider { private cacheClientWrapper: RedisClientWrapper; private partitionKey: string; - private constructor(msalConfig: Configuration, cacheClient: RedisClientType, partitionKey: string) { + private constructor( + msalConfig: Configuration, + cacheClient: RedisClientType, + partitionKey: string + ) { this.msalConfig = msalConfig; this.cacheClientWrapper = new RedisClientWrapper(cacheClient); this.partitionKey = partitionKey; @@ -43,7 +47,10 @@ export class AuthProvider { * @param cacheClient * @returns */ - static async initialize(appConfig: AppConfig, cacheClient: RedisClientType): Promise { + static async initialize( + appConfig: AppConfig, + cacheClient: RedisClientType + ): Promise { const msalConfig = { auth: { clientId: appConfig.clientId, @@ -56,37 +63,51 @@ export class AuthProvider { console.log(message); }, logLevel: LogLevel.Trace, - piiLoggingEnabled: false + piiLoggingEnabled: false, }, // proxyUrl: "http://localhost:8888" // uncomment to capture traffic with Fiddler - } + }, } as Configuration; const partitionKey = `${appConfig.clientId}.${appConfig.tenantId}`; - const msalConfigWithMetadata = await AuthProvider.getMetadata(msalConfig, cacheClient, partitionKey); - - return new AuthProvider(msalConfigWithMetadata, cacheClient, partitionKey); + const msalConfigWithMetadata = await AuthProvider.getMetadata( + msalConfig, + cacheClient, + partitionKey + ); + + return new AuthProvider( + msalConfigWithMetadata, + cacheClient, + partitionKey + ); } - async getToken(tokenRequest: ClientCredentialRequest): Promise { + async getToken( + tokenRequest: ClientCredentialRequest + ): Promise { const cca = new ConfidentialClientApplication({ ...this.msalConfig, cache: { cachePlugin: new CustomCachePlugin( this.cacheClientWrapper, this.partitionKey // . - ) - } + ), + }, }); let tokenResponse = null; try { performance.mark("acquireTokenByClientCredential-start"); - tokenResponse = await cca.acquireTokenByClientCredential(tokenRequest); + tokenResponse = await cca.acquireTokenByClientCredential( + tokenRequest + ); performance.mark("acquireTokenByClientCredential-end"); performance.measure( - tokenResponse?.fromCache ? "acquireTokenByClientCredential-fromCache" : "acquireTokenByClientCredential-fromNetwork", + tokenResponse?.fromCache + ? "acquireTokenByClientCredential-fromCache" + : "acquireTokenByClientCredential-fromNetwork", "acquireTokenByClientCredential-start", "acquireTokenByClientCredential-end" ); @@ -97,29 +118,53 @@ export class AuthProvider { return tokenResponse; } - private static async getMetadata(msalConfig: Configuration, cacheClient: RedisClientType, partitionKey: string): Promise { + private static async getMetadata( + msalConfig: Configuration, + cacheClient: RedisClientType, + partitionKey: string + ): Promise { const msalConfigWithMetadata = msalConfig; try { - let [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([ - cacheClient.get(`${partitionKey}.discovery-metadata`), - cacheClient.get(`${partitionKey}.authority-metadata`) - ]); + let [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all( + [ + cacheClient.get(`${partitionKey}.discovery-metadata`), + cacheClient.get(`${partitionKey}.authority-metadata`), + ] + ); if (!cloudDiscoveryMetadata || !authorityMetadata) { - [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([ - AuthProvider.fetchCloudDiscoveryMetadata(partitionKey.split('.')[1]), - AuthProvider.fetchOIDCMetadata(partitionKey.split('.')[1]) - ]); + [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all( + [ + AuthProvider.fetchCloudDiscoveryMetadata( + partitionKey.split(".")[1] + ), + AuthProvider.fetchOIDCMetadata( + partitionKey.split(".")[1] + ), + ] + ); if (cloudDiscoveryMetadata && authorityMetadata) { - await cacheClient.set(`${partitionKey}.discovery-metadata`, JSON.stringify(cloudDiscoveryMetadata)); - await cacheClient.set(`${partitionKey}.authority-metadata`, JSON.stringify(authorityMetadata)); + await cacheClient.set( + `${partitionKey}.discovery-metadata`, + JSON.stringify(cloudDiscoveryMetadata) + ); + await cacheClient.set( + `${partitionKey}.authority-metadata`, + JSON.stringify(authorityMetadata) + ); } } - msalConfigWithMetadata.auth.cloudDiscoveryMetadata = typeof cloudDiscoveryMetadata === 'string' ? cloudDiscoveryMetadata : JSON.stringify(cloudDiscoveryMetadata); - msalConfigWithMetadata.auth.authorityMetadata = typeof authorityMetadata === 'string' ? authorityMetadata : JSON.stringify(authorityMetadata); + msalConfigWithMetadata.auth.cloudDiscoveryMetadata = + typeof cloudDiscoveryMetadata === "string" + ? cloudDiscoveryMetadata + : JSON.stringify(cloudDiscoveryMetadata); + msalConfigWithMetadata.auth.authorityMetadata = + typeof authorityMetadata === "string" + ? authorityMetadata + : JSON.stringify(authorityMetadata); } catch (error) { console.log(error); } @@ -127,14 +172,21 @@ export class AuthProvider { return msalConfigWithMetadata; } - private static async fetchCloudDiscoveryMetadata(tenantId: string): Promise { - const endpoint = 'https://login.microsoftonline.com/common/discovery/instance'; + private static async fetchCloudDiscoveryMetadata( + tenantId: string + ): Promise { + const endpoint = + "https://login.microsoftonline.com/common/discovery/instance"; try { - const response = await AxiosHelper.callDownstreamApi(endpoint, undefined, { - 'api-version': '1.1', - 'authorization_endpoint': `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize` - }); + const response = await AxiosHelper.callDownstreamApi( + endpoint, + undefined, + { + "api-version": "1.1", + authorization_endpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`, + } + ); return response; } catch (error) { @@ -146,7 +198,7 @@ export class AuthProvider { const endpoint = `https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`; try { - const response = await AxiosHelper.callDownstreamApi(endpoint) + const response = await AxiosHelper.callDownstreamApi(endpoint); return response; } catch (error) { console.log(error); diff --git a/samples/msal-node-samples/client-credentials-distributed-cache/src/AxiosHelper.ts b/samples/msal-node-samples/client-credentials-distributed-cache/src/AxiosHelper.ts index 328c546ebf..fb65c27b20 100644 --- a/samples/msal-node-samples/client-credentials-distributed-cache/src/AxiosHelper.ts +++ b/samples/msal-node-samples/client-credentials-distributed-cache/src/AxiosHelper.ts @@ -6,25 +6,30 @@ import axios from "axios"; class AxiosHelper { - /** * Makes an Authorization "Bearer" request with the given accessToken to the given endpoint. * @param endpoint * @param accessToken */ - static async callDownstreamApi(endpoint: string, accessToken?: string, params?: Record): Promise { + static async callDownstreamApi( + endpoint: string, + accessToken?: string, + params?: Record + ): Promise { console.log(`Request to ${endpoint} made at: ${new Date().toString()}`); const response = await axios.get(endpoint, { - headers: (accessToken && { Authorization: `Bearer ${accessToken}`}) || undefined, - params: params || undefined + headers: + (accessToken && { Authorization: `Bearer ${accessToken}` }) || + undefined, + params: params || undefined, }); if (response.status !== 200) { throw new Error(`Response: ${response.status}`); } - return (await response.data); + return await response.data; } } diff --git a/samples/msal-node-samples/client-credentials-distributed-cache/src/CustomCachePlugin.ts b/samples/msal-node-samples/client-credentials-distributed-cache/src/CustomCachePlugin.ts index 34b3544830..1e26c06aad 100644 --- a/samples/msal-node-samples/client-credentials-distributed-cache/src/CustomCachePlugin.ts +++ b/samples/msal-node-samples/client-credentials-distributed-cache/src/CustomCachePlugin.ts @@ -3,7 +3,11 @@ * Licensed under the MIT License. */ -import { ICacheClient, ICachePlugin, TokenCacheContext } from "@azure/msal-node"; +import { + ICacheClient, + ICachePlugin, + TokenCacheContext, +} from "@azure/msal-node"; import { performance } from "perf_hooks"; /** @@ -20,20 +24,35 @@ class CustomCachePlugin implements ICachePlugin { this.partitionKey = partitionKey; } - public async beforeCacheAccess(cacheContext: TokenCacheContext): Promise { + public async beforeCacheAccess( + cacheContext: TokenCacheContext + ): Promise { performance.mark("beforeCacheAccess-start"); const cacheData = await this.client.get(this.partitionKey); cacheContext.tokenCache.deserialize(cacheData); performance.mark("beforeCacheAccess-end"); - performance.measure("beforeCacheAccess", "beforeCacheAccess-start", "beforeCacheAccess-end"); + performance.measure( + "beforeCacheAccess", + "beforeCacheAccess-start", + "beforeCacheAccess-end" + ); } - public async afterCacheAccess(cacheContext: TokenCacheContext): Promise { + public async afterCacheAccess( + cacheContext: TokenCacheContext + ): Promise { if (cacheContext.cacheHasChanged) { performance.mark("afterCacheAccess-start"); - await this.client.set(this.partitionKey, cacheContext.tokenCache.serialize()); + await this.client.set( + this.partitionKey, + cacheContext.tokenCache.serialize() + ); performance.mark("afterCacheAccess-end"); - performance.measure("afterCacheAccess", "afterCacheAccess-start", "afterCacheAccess-end"); + performance.measure( + "afterCacheAccess", + "afterCacheAccess-start", + "afterCacheAccess-end" + ); } } } diff --git a/samples/msal-node-samples/client-credentials-distributed-cache/src/ProvisionHandler.ts b/samples/msal-node-samples/client-credentials-distributed-cache/src/ProvisionHandler.ts index e64746fcf8..7c93679644 100644 --- a/samples/msal-node-samples/client-credentials-distributed-cache/src/ProvisionHandler.ts +++ b/samples/msal-node-samples/client-credentials-distributed-cache/src/ProvisionHandler.ts @@ -30,7 +30,12 @@ class ProvisionHandler { * @param clientId * @param scope */ - async grantAdminConsent(instance: string, tenantId: string, clientId: string, scope: string): Promise { + async grantAdminConsent( + instance: string, + tenantId: string, + clientId: string, + scope: string + ): Promise { const adminConsentListener = this.listenForAdminConsentResponse(); const redirectUri = this.getRedirectUri(); @@ -44,10 +49,10 @@ class ProvisionHandler { const searchParams = new URLSearchParams({}); - searchParams.append('client_id', clientId); - searchParams.append('state', this.state); - searchParams.append('redirect_uri', redirectUri); - searchParams.append('scope', scope); + searchParams.append("client_id", clientId); + searchParams.append("state", this.state); + searchParams.append("redirect_uri", redirectUri); + searchParams.append("scope", scope); await open(`${adminConsentUri}?${searchParams.toString()}`); @@ -60,38 +65,51 @@ class ProvisionHandler { private async listenForAdminConsentResponse(): Promise { if (!!this.server) { - throw new Error('Server already exists. Cannot create another.') + throw new Error("Server already exists. Cannot create another."); } const adminConsentListener = new Promise((resolve, reject) => { - this.server = http.createServer(async (req: http.IncomingMessage, res: http.ServerResponse) => { - const url = req.url; - - if (!url) { - res.end("Error occurred loading redirectUrl"); - reject(new Error('Server callback was invoked without a url. This is unexpected.')); - return; - } - - const redirectUri = await this.getRedirectUri(); - const responseUri = new URL(`${redirectUri}${url}`); - - /** - * Retrieve the response parameters. For more information, visit: - * https://docs.microsoft.com/azure/active-directory/develop/v2-admin-consent#successful-response - */ - const isGranted = responseUri.searchParams.get('admin_consent') === "True"; - const doesStateMatch = responseUri.searchParams.get('state') === this.state; - const hasAnyErrors = responseUri.searchParams.has('error'); - - if (isGranted && doesStateMatch && !hasAnyErrors) { - res.end("Admin consent was successfully acquired. You can close this window now."); - } else { - res.end("Admin consent was not acquired. Make sure the account has admin privileges in the tenant and try again."); + this.server = http.createServer( + async (req: http.IncomingMessage, res: http.ServerResponse) => { + const url = req.url; + + if (!url) { + res.end("Error occurred loading redirectUrl"); + reject( + new Error( + "Server callback was invoked without a url. This is unexpected." + ) + ); + return; + } + + const redirectUri = await this.getRedirectUri(); + const responseUri = new URL(`${redirectUri}${url}`); + + /** + * Retrieve the response parameters. For more information, visit: + * https://docs.microsoft.com/azure/active-directory/develop/v2-admin-consent#successful-response + */ + const isGranted = + responseUri.searchParams.get("admin_consent") === + "True"; + const doesStateMatch = + responseUri.searchParams.get("state") === this.state; + const hasAnyErrors = responseUri.searchParams.has("error"); + + if (isGranted && doesStateMatch && !hasAnyErrors) { + res.end( + "Admin consent was successfully acquired. You can close this window now." + ); + } else { + res.end( + "Admin consent was not acquired. Make sure the account has admin privileges in the tenant and try again." + ); + } + + resolve(isGranted && doesStateMatch && !hasAnyErrors); } - - resolve(isGranted && doesStateMatch && !hasAnyErrors); - }); + ); this.server.listen(0); }); @@ -101,8 +119,10 @@ class ProvisionHandler { let ticks = 0; const id = setInterval(() => { - if ((5000 / 100) < ticks) { - throw new Error('Timed out waiting for auth code listener to be registered.'); + if (5000 / 100 < ticks) { + throw new Error( + "Timed out waiting for auth code listener to be registered." + ); } if (this.server.listening) { @@ -119,14 +139,16 @@ class ProvisionHandler { private getRedirectUri(): string { if (!this.server) { - throw new Error('No loopback server exists yet.') + throw new Error("No loopback server exists yet."); } const address = this.server.address(); if (!address || typeof address === "string" || !address.port) { this.closeServer(); - throw new Error('Loopback server address is not type string. This is unexpected.') + throw new Error( + "Loopback server address is not type string. This is unexpected." + ); } const port = address && address.port; diff --git a/samples/msal-node-samples/client-credentials-distributed-cache/src/RedisClientWrapper.ts b/samples/msal-node-samples/client-credentials-distributed-cache/src/RedisClientWrapper.ts index c122b61a16..862da01d2b 100644 --- a/samples/msal-node-samples/client-credentials-distributed-cache/src/RedisClientWrapper.ts +++ b/samples/msal-node-samples/client-credentials-distributed-cache/src/RedisClientWrapper.ts @@ -13,11 +13,11 @@ const EVICTION_POLICY = "volatile-lru"; const EMPTY_STRING = ""; /** -* Simple persistence client helper, using Redis (node-redis). You must have redis installed -* on your machine and have redis server listening. Note that this is only for illustration, -* and you'll need to consider cache eviction policies and handle cache server connection -* issues. For more information, visit: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md -*/ + * Simple persistence client helper, using Redis (node-redis). You must have redis installed + * on your machine and have redis server listening. Note that this is only for illustration, + * and you'll need to consider cache eviction policies and handle cache server connection + * issues. For more information, visit: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md + */ class RedisClientWrapper implements ICacheClient { cacheClient: RedisClientType; @@ -39,7 +39,7 @@ class RedisClientWrapper implements ICacheClient { */ public async get(key: string): Promise { try { - return await this.cacheClient.get(key) || EMPTY_STRING; + return (await this.cacheClient.get(key)) || EMPTY_STRING; } catch (error) { console.log(error); } @@ -55,9 +55,11 @@ class RedisClientWrapper implements ICacheClient { */ public async set(key: string, value: string): Promise { try { - return await this.cacheClient.set(key, value, { - EX: CACHE_TTL - }) || EMPTY_STRING; + return ( + (await this.cacheClient.set(key, value, { + EX: CACHE_TTL, + })) || EMPTY_STRING + ); } catch (error) { console.log(error); } diff --git a/samples/msal-node-samples/client-credentials-distributed-cache/src/index.ts b/samples/msal-node-samples/client-credentials-distributed-cache/src/index.ts index 6054658221..0abb7ba4e2 100644 --- a/samples/msal-node-samples/client-credentials-distributed-cache/src/index.ts +++ b/samples/msal-node-samples/client-credentials-distributed-cache/src/index.ts @@ -7,7 +7,11 @@ import fs from "fs"; import dotenv from "dotenv"; import yargs from "yargs"; import { createClient, RedisClientType } from "redis"; -import { PerformanceObserver, PerformanceObserverEntryList, PerformanceEntry } from "perf_hooks"; +import { + PerformanceObserver, + PerformanceObserverEntryList, + PerformanceEntry, +} from "perf_hooks"; import { AuthProvider, AppConfig } from "./AuthProvider"; import AxiosHelper from "./AxiosHelper"; @@ -16,14 +20,29 @@ import ProvisionHandler from "./ProvisionHandler"; dotenv.config(); const options: { [key: string]: any } = yargs - .usage('Usage: --tenant --operation ') - .option('i', { alias: 'instance', describe: 'cloud instance', type: 'string', demandOption: false }) - .option('t', { alias: 'tenant', describe: 'tenant id', type: 'string', demandOption: true }) - .option('o', { alias: 'operation', describe: 'operation name', type: 'string', demandOption: true }) - .argv; + .usage("Usage: --tenant --operation ") + .option("i", { + alias: "instance", + describe: "cloud instance", + type: "string", + demandOption: false, + }) + .option("t", { + alias: "tenant", + describe: "tenant id", + type: "string", + demandOption: true, + }) + .option("o", { + alias: "operation", + describe: "operation name", + type: "string", + demandOption: true, + }).argv; const appConfig: AppConfig = { - instance: options.instance || process.env.INSTANCE || "ENTER_CLOUD_INSTANCE_HERE", // if instance is provided as a command line arg, use that first + instance: + options.instance || process.env.INSTANCE || "ENTER_CLOUD_INSTANCE_HERE", // if instance is provided as a command line arg, use that first tenantId: options.tenant || process.env.TENANT_ID || "ENTER_TENANT_ID_HERE", // if tenantId is provided as a command line arg, use that first clientId: process.env.CLIENT_ID || "ENTER_CLIENT_ID_HERE", clientSecret: process.env.CLIENT_SECRET || "ENTER_CLIENT_SECRET_HERE", // in production, use a certificate instead -see the README for more @@ -34,7 +53,10 @@ async function main() { try { const cacheClient = await initializeRedisClient(); - const authProvider = await AuthProvider.initialize(appConfig, cacheClient); + const authProvider = await AuthProvider.initialize( + appConfig, + cacheClient + ); switch (options.operation) { case "provisionApp": @@ -67,7 +89,7 @@ async function main() { console.log(graphResponse); process.exit(0); default: - console.log('Selected operation is not found'); + console.log("Selected operation is not found"); process.exit(1); } } catch (error) { @@ -77,65 +99,77 @@ async function main() { } function initializePerformanceObserver(): void { - const perfObserver = new PerformanceObserver((items: PerformanceObserverEntryList) => { - let durationInCacheInMs = 0; - let durationTotalInMs = 0; - let tokenSource; - - items.getEntriesByName("beforeCacheAccess").forEach((entry: PerformanceEntry) => { - durationInCacheInMs += entry.duration; - }); - - items.getEntriesByName("afterCacheAccess").forEach((entry: PerformanceEntry) => { - durationInCacheInMs += entry.duration; - }); - - items.getEntriesByName("acquireTokenByClientCredential-fromNetwork").forEach((entry: PerformanceEntry) => { - durationTotalInMs = entry.duration; - tokenSource = "network"; - }); - - items.getEntriesByName("acquireTokenByClientCredential-fromCache").forEach((entry: PerformanceEntry) => { - durationTotalInMs = entry.duration; - tokenSource = "cache"; - }); - - if (tokenSource) { - const results = { - tokenSource, - durationTotalInMs, - durationInCacheInMs, - durationInHttpInMs: tokenSource === "network" ? durationTotalInMs - durationInCacheInMs : 0, - }; - - fs.appendFile( - "benchmarks.json", - `${JSON.stringify(results)}\n`, - function (err) { - if (err) { - throw err; + const perfObserver = new PerformanceObserver( + (items: PerformanceObserverEntryList) => { + let durationInCacheInMs = 0; + let durationTotalInMs = 0; + let tokenSource; + + items + .getEntriesByName("beforeCacheAccess") + .forEach((entry: PerformanceEntry) => { + durationInCacheInMs += entry.duration; + }); + + items + .getEntriesByName("afterCacheAccess") + .forEach((entry: PerformanceEntry) => { + durationInCacheInMs += entry.duration; + }); + + items + .getEntriesByName("acquireTokenByClientCredential-fromNetwork") + .forEach((entry: PerformanceEntry) => { + durationTotalInMs = entry.duration; + tokenSource = "network"; + }); + + items + .getEntriesByName("acquireTokenByClientCredential-fromCache") + .forEach((entry: PerformanceEntry) => { + durationTotalInMs = entry.duration; + tokenSource = "cache"; + }); + + if (tokenSource) { + const results = { + tokenSource, + durationTotalInMs, + durationInCacheInMs, + durationInHttpInMs: + tokenSource === "network" + ? durationTotalInMs - durationInCacheInMs + : 0, + }; + + fs.appendFile( + "benchmarks.json", + `${JSON.stringify(results)}\n`, + function (err) { + if (err) { + throw err; + } } - } - ); + ); + } } - }); + ); perfObserver.observe({ entryTypes: ["measure"], buffered: true }); } async function initializeRedisClient(): Promise { - /** * Configure connection strategy and other settings. See: * https://github.com/redis/node-redis/blob/master/docs/client-configuration.md */ const redis = createClient({ socket: { - reconnectStrategy: false - } + reconnectStrategy: false, + }, }); - redis.on('error', (err: any) => console.log('Redis Client Error', err)); + redis.on("error", (err: any) => console.log("Redis Client Error", err)); await redis.connect(); return redis as RedisClientType; diff --git a/samples/msal-node-samples/client-credentials/README.md b/samples/msal-node-samples/client-credentials/README.md index a773ea22c2..effbb4884a 100644 --- a/samples/msal-node-samples/client-credentials/README.md +++ b/samples/msal-node-samples/client-credentials/README.md @@ -19,22 +19,22 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-console`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-console`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. 1. Select **Register** to create the application. 1. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later. 1. In the app's registration screen, select the **Certificates & secrets** blade in the left. - - In the **Client secrets** section, select **New client secret**. - - Type a key description (for instance `app secret`), - - Select one of the available key durations (6 months, 12 months or Custom) as per your security posture. - - The generated key value will be displayed when you select the **Add** button. Copy and save the generated value for use in later steps. + - In the **Client secrets** section, select **New client secret**. + - Type a key description (for instance `app secret`), + - Select one of the available key durations (6 months, 12 months or Custom) as per your security posture. + - The generated key value will be displayed when you select the **Add** button. Copy and save the generated value for use in later steps. 1. In the app's registration screen, select the API permissions blade in the left to open the page where we add access to the APIs that your application needs. - - Select the **Add a permission** button and then, - - Ensure that the **Microsoft APIs** tab is selected. - - In the **Commonly used Microsoft APIs** section, select **Microsoft Graph** - - In the **Application permissions** section, select the **User.Read.All** in the list. Use the search box if necessary. - - Select the **Add permissions** button at the bottom. - - Finally, grant **admin consent** for this scope. + - Select the **Add a permission** button and then, + - Ensure that the **Microsoft APIs** tab is selected. + - In the **Commonly used Microsoft APIs** section, select **Microsoft Graph** + - In the **Application permissions** section, select the **User.Read.All** in the list. Use the search box if necessary. + - 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 the configuration object: @@ -44,7 +44,7 @@ const config = { clientId: "ENTER_CLIENT_ID", authority: "https://login.microsoftonline.com/ENTER_TENANT_INFO", clientSecret: "ENTER_CLIENT_SECRET", - } + }, }; ``` @@ -60,4 +60,4 @@ After that, you should see the response from Microsoft Entra ID in your terminal ## More information -- [Tutorial: Call the Microsoft Graph API in a Node.js console app](https://docs.microsoft.com/azure/active-directory/develop/tutorial-v2-nodejs-console) +- [Tutorial: Call the Microsoft Graph API in a Node.js console app](https://docs.microsoft.com/azure/active-directory/develop/tutorial-v2-nodejs-console) diff --git a/samples/msal-node-samples/client-credentials/index.js b/samples/msal-node-samples/client-credentials/index.js index 62f34495a9..080139b213 100644 --- a/samples/msal-node-samples/client-credentials/index.js +++ b/samples/msal-node-samples/client-credentials/index.js @@ -53,12 +53,12 @@ function getClientCredentialsToken(cca, clientCredentialRequestScopes, ro) { * and execute the sample client credentials application. */ if(argv.$0 === "index.js") { - const loggerOptions = { - loggerCallback(loglevel, message, containsPii) { -     console.log(message); + const loggerOptions = { + loggerCallback(loglevel, message, containsPii) { + console.log(message); }, -     piiLoggingEnabled: false, - logLevel: msal.LogLevel.Verbose, + piiLoggingEnabled: false, + logLevel: msal.LogLevel.Verbose, } // Build MSAL ClientApplication Configuration object @@ -69,8 +69,8 @@ if(argv.$0 === "index.js") { }, // Uncomment or comment the code below to enable or disable the MSAL logger respectively // system: { -     // loggerOptions, -     // } + // loggerOptions, + // } }; // Create msal application object diff --git a/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/HttpClientAxios.ts b/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/HttpClientAxios.ts index 193d49ebd8..185b073e36 100644 --- a/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/HttpClientAxios.ts +++ b/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/HttpClientAxios.ts @@ -19,7 +19,6 @@ enum HttpMethod { * This class implements the API for network requests. */ export class HttpClientAxios implements INetworkModule { - /** * Http Get request * @param url @@ -35,7 +34,7 @@ export class HttpClientAxios implements INetworkModule { /* istanbul ignore next */ headers: options && options.headers, /* istanbul ignore next */ - validateStatus: () => true + validateStatus: () => true, }; const response = await axios(request); @@ -54,7 +53,7 @@ export class HttpClientAxios implements INetworkModule { async sendPostRequestAsync( url: string, options?: NetworkRequestOptions, - cancellationToken?: number + cancellationToken?: number ): Promise> { const request: AxiosRequestConfig = { method: HttpMethod.POST, @@ -65,7 +64,7 @@ export class HttpClientAxios implements INetworkModule { /* istanbul ignore next */ headers: options && options.headers, /* istanbul ignore next */ - validateStatus: () => true + validateStatus: () => true, }; const response = await axios(request); @@ -75,4 +74,4 @@ export class HttpClientAxios implements INetworkModule { status: response.status, }; } -} \ No newline at end of file +} diff --git a/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/HttpClientCurrent.ts b/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/HttpClientCurrent.ts index bc1f491afa..820a2ae500 100644 --- a/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/HttpClientCurrent.ts +++ b/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/HttpClientCurrent.ts @@ -6,7 +6,7 @@ import { INetworkModule, NetworkRequestOptions, - NetworkResponse + NetworkResponse, } from "@azure/msal-node"; import http from "http"; @@ -24,13 +24,13 @@ enum HttpStatus { CLIENT_ERROR_RANGE_START = 400, CLIENT_ERROR_RANGE_END = 499, SERVER_ERROR_RANGE_START = 500, - SERVER_ERROR_RANGE_END = 599 + SERVER_ERROR_RANGE_END = 599, } enum ProxyStatus { SUCCESS_RANGE_START = 200, SUCCESS_RANGE_END = 299, - SERVER_ERROR = 500 + SERVER_ERROR = 500, } /** @@ -41,7 +41,11 @@ const Constants = { }; class NetworkUtils { - static getNetworkResponse(headers: Record, body: T, statusCode: number): NetworkResponse { + static getNetworkResponse( + headers: Record, + body: T, + statusCode: number + ): NetworkResponse { return { headers: headers, body: body, @@ -56,9 +60,10 @@ class NetworkUtils { static urlToHttpOptions(url: URL): https.RequestOptions { const options: https.RequestOptions & Partial> = { protocol: url.protocol, - hostname: url.hostname && url.hostname.startsWith("[") ? - url.hostname.slice(1, -1) : - url.hostname, + hostname: + url.hostname && url.hostname.startsWith("[") + ? url.hostname.slice(1, -1) + : url.hostname, hash: url.hash, search: url.search, pathname: url.pathname, @@ -69,7 +74,9 @@ class NetworkUtils { options.port = Number(url.port); } if (url.username || url.password) { - options.auth = `${decodeURIComponent(url.username)}:${decodeURIComponent(url.password)}`; + options.auth = `${decodeURIComponent( + url.username + )}:${decodeURIComponent(url.password)}`; } return options; } @@ -84,7 +91,7 @@ export class HttpClientCurrent implements INetworkModule { constructor( proxyUrl?: string, - customAgentOptions?: http.AgentOptions | https.AgentOptions, + customAgentOptions?: http.AgentOptions | https.AgentOptions ) { this.proxyUrl = proxyUrl || ""; this.customAgentOptions = customAgentOptions || {}; @@ -97,12 +104,23 @@ export class HttpClientCurrent implements INetworkModule { */ async sendGetRequestAsync( url: string, - options?: NetworkRequestOptions, + options?: NetworkRequestOptions ): Promise> { if (this.proxyUrl) { - return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.GET, options, this.customAgentOptions as http.AgentOptions); + return networkRequestViaProxy( + url, + this.proxyUrl, + HttpMethod.GET, + options, + this.customAgentOptions as http.AgentOptions + ); } else { - return networkRequestViaHttps(url, HttpMethod.GET, options, this.customAgentOptions as https.AgentOptions); + return networkRequestViaHttps( + url, + HttpMethod.GET, + options, + this.customAgentOptions as https.AgentOptions + ); } } @@ -114,12 +132,25 @@ export class HttpClientCurrent implements INetworkModule { async sendPostRequestAsync( url: string, options?: NetworkRequestOptions, - cancellationToken?: number, + cancellationToken?: number ): Promise> { if (this.proxyUrl) { - return networkRequestViaProxy(url, this.proxyUrl, HttpMethod.POST, options, this.customAgentOptions as http.AgentOptions, cancellationToken); + return networkRequestViaProxy( + url, + this.proxyUrl, + HttpMethod.POST, + options, + this.customAgentOptions as http.AgentOptions, + cancellationToken + ); } else { - return networkRequestViaHttps(url, HttpMethod.POST, options, this.customAgentOptions as https.AgentOptions, cancellationToken); + return networkRequestViaHttps( + url, + HttpMethod.POST, + options, + this.customAgentOptions as https.AgentOptions, + cancellationToken + ); } } } @@ -130,13 +161,13 @@ const networkRequestViaProxy = ( httpMethod: string, options: NetworkRequestOptions, agentOptions?: http.AgentOptions, - timeout?: number, + timeout?: number ): Promise> => { const destinationUrl = new URL(destinationUrlString); const proxyUrl = new URL(proxyUrlString); // "method: connect" must be used to establish a connection to the proxy - const headers = options?.headers || {} as Record; + const headers = options?.headers || ({} as Record); const tunnelRequestOptions: https.RequestOptions = { host: proxyUrl.hostname, port: proxyUrl.port, @@ -162,13 +193,14 @@ const networkRequestViaProxy = ( `Content-Length: ${body.length}\r\n` + `\r\n${body}`; } - const outgoingRequestString = `${httpMethod.toUpperCase()} ${destinationUrl.href} HTTP/1.1\r\n` + + const outgoingRequestString = + `${httpMethod.toUpperCase()} ${destinationUrl.href} HTTP/1.1\r\n` + `Host: ${destinationUrl.host}\r\n` + "Connection: close\r\n" + postRequestStringContent + "\r\n"; - return new Promise>(((resolve, reject) => { + return new Promise>((resolve, reject) => { const request = http.request(tunnelRequestOptions); if (tunnelRequestOptions.timeout) { @@ -182,11 +214,23 @@ const networkRequestViaProxy = ( // establish connection to the proxy request.on("connect", (response, socket) => { - const proxyStatusCode = response?.statusCode || ProxyStatus.SERVER_ERROR; - if ((proxyStatusCode < ProxyStatus.SUCCESS_RANGE_START) || (proxyStatusCode > ProxyStatus.SUCCESS_RANGE_END)) { + const proxyStatusCode = + response?.statusCode || ProxyStatus.SERVER_ERROR; + if ( + proxyStatusCode < ProxyStatus.SUCCESS_RANGE_START || + proxyStatusCode > ProxyStatus.SUCCESS_RANGE_END + ) { request.destroy(); socket.destroy(); - reject(new Error(`Error connecting to proxy. Http status code: ${response.statusCode}. Http status message: ${response?.statusMessage || "Unknown"}`)); + reject( + new Error( + `Error connecting to proxy. Http status code: ${ + response.statusCode + }. Http status message: ${ + response?.statusMessage || "Unknown" + }` + ) + ); } if (tunnelRequestOptions.timeout) { socket.setTimeout(tunnelRequestOptions.timeout); @@ -212,14 +256,22 @@ const networkRequestViaProxy = ( // separate each line into it's own entry in an arry const dataStringArray = dataString.split("\r\n"); // the first entry will contain the statusCode and statusMessage - const httpStatusCode = parseInt(dataStringArray[0].split(" ")[1]); + const httpStatusCode = parseInt( + dataStringArray[0].split(" ")[1] + ); // remove "HTTP/1.1" and the status code to get the status message - const statusMessage = dataStringArray[0].split(" ").slice(2).join(" "); + const statusMessage = dataStringArray[0] + .split(" ") + .slice(2) + .join(" "); // the last entry will contain the body const body = dataStringArray[dataStringArray.length - 1]; // everything in between the first and last entries are the headers - const headersArray = dataStringArray.slice(1, dataStringArray.length - 2); + const headersArray = dataStringArray.slice( + 1, + dataStringArray.length - 2 + ); // build an object out of all the headers const entries = new Map(); @@ -239,7 +291,7 @@ const networkRequestViaProxy = ( const object = JSON.parse(headerValue); // if it is, then convert it from a string to a JSON object - if (object && (typeof object === "object")) { + if (object && typeof object === "object") { headerValue = object; } } catch (e) { @@ -253,13 +305,22 @@ const networkRequestViaProxy = ( const parsedHeaders = headers as Record; const networkResponse = NetworkUtils.getNetworkResponse( parsedHeaders, - parseBody(httpStatusCode, statusMessage, parsedHeaders, body) as T, + parseBody( + httpStatusCode, + statusMessage, + parsedHeaders, + body + ) as T, httpStatusCode ); - if (((httpStatusCode < HttpStatus.SUCCESS_RANGE_START) || (httpStatusCode > HttpStatus.SUCCESS_RANGE_END)) && + if ( + (httpStatusCode < HttpStatus.SUCCESS_RANGE_START || + httpStatusCode > HttpStatus.SUCCESS_RANGE_END) && // do not destroy the request for the device code flow - networkResponse.body["error"] !== Constants.AUTHORIZATION_PENDING) { + networkResponse.body["error"] !== + Constants.AUTHORIZATION_PENDING + ) { request.destroy(); } resolve(networkResponse); @@ -276,7 +337,7 @@ const networkRequestViaProxy = ( request.destroy(); reject(new Error(chunk.toString())); }); - })); + }); }; const networkRequestViaHttps = ( @@ -284,19 +345,19 @@ const networkRequestViaHttps = ( httpMethod: string, options?: NetworkRequestOptions, agentOptions?: https.AgentOptions, - timeout?: number, + timeout?: number ): Promise> => { const isPostRequest = httpMethod === HttpMethod.POST; const body: string = options?.body || ""; const url = new URL(urlString); - const headers = options?.headers || {} as Record; + const headers = options?.headers || ({} as Record); let customOptions: https.RequestOptions = { method: httpMethod, headers: headers, ...NetworkUtils.urlToHttpOptions(url), }; - + if (timeout) { customOptions.timeout = timeout; } @@ -346,13 +407,22 @@ const networkRequestViaHttps = ( const parsedHeaders = headers as Record; const networkResponse = NetworkUtils.getNetworkResponse( parsedHeaders, - parseBody(statusCode, statusMessage, parsedHeaders, body) as T, + parseBody( + statusCode, + statusMessage, + parsedHeaders, + body + ) as T, statusCode ); - if (((statusCode < HttpStatus.SUCCESS_RANGE_START) || (statusCode > HttpStatus.SUCCESS_RANGE_END)) && + if ( + (statusCode < HttpStatus.SUCCESS_RANGE_START || + statusCode > HttpStatus.SUCCESS_RANGE_END) && // do not destroy the request for the device code flow - networkResponse.body["error"] !== Constants.AUTHORIZATION_PENDING) { + networkResponse.body["error"] !== + Constants.AUTHORIZATION_PENDING + ) { request.destroy(); } resolve(networkResponse); @@ -374,7 +444,12 @@ const networkRequestViaHttps = ( * @param body {string} the body from the response of the server * @returns {Object} JSON parsed body or error object */ -const parseBody = (statusCode: number, statusMessage: string | undefined, headers: Record, body: string) => { +const parseBody = ( + statusCode: number, + statusMessage: string | undefined, + headers: Record, + body: string +) => { /* * Informational responses (100 – 199) * Successful responses (200 – 299) @@ -382,17 +457,23 @@ const parseBody = (statusCode: number, statusMessage: string | undefined, header * Client error responses (400 – 499) * Server error responses (500 – 599) */ - + let parsedBody; try { parsedBody = JSON.parse(body); } catch (error) { let errorType; let errorDescriptionHelper; - if ((statusCode >= HttpStatus.CLIENT_ERROR_RANGE_START) && (statusCode <= HttpStatus.CLIENT_ERROR_RANGE_END)) { + if ( + statusCode >= HttpStatus.CLIENT_ERROR_RANGE_START && + statusCode <= HttpStatus.CLIENT_ERROR_RANGE_END + ) { errorType = "client_error"; errorDescriptionHelper = "A client"; - } else if ((statusCode >= HttpStatus.SERVER_ERROR_RANGE_START) && (statusCode <= HttpStatus.SERVER_ERROR_RANGE_END)) { + } else if ( + statusCode >= HttpStatus.SERVER_ERROR_RANGE_START && + statusCode <= HttpStatus.SERVER_ERROR_RANGE_END + ) { errorType = "server_error"; errorDescriptionHelper = "A server"; } else { @@ -402,7 +483,9 @@ const parseBody = (statusCode: number, statusMessage: string | undefined, header parsedBody = { error: errorType, - error_description: `${errorDescriptionHelper} error occured.\nHttp status code: ${statusCode}\nHttp status message: ${statusMessage || "Unknown"}\nHeaders: ${JSON.stringify(headers)}` + error_description: `${errorDescriptionHelper} error occured.\nHttp status code: ${statusCode}\nHttp status message: ${ + statusMessage || "Unknown" + }\nHeaders: ${JSON.stringify(headers)}`, }; } diff --git a/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/README.md b/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/README.md index 621956d53a..150d12cfbd 100644 --- a/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/README.md +++ b/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/README.md @@ -5,6 +5,7 @@ This sample demonstrates how to implement a custom [INetworkModule](https://azur Fiddler Everywhere is not supported on all operating systems. [Fiddler Classic](https://www.telerik.com/fiddler/fiddler-classic) is a free Windows-only version of Fiddler Everywhere. It's important to note that Microsoft Entra ID no longer supports TLS 1.0, which is the default TLS version in Fiddler Classic. The TLS version can be configured via navigating to Tools > Options > HTTPS, then setting TLS to 1.2. ## Note + This sample is written in TypeScript and was developed with Node version 16.14.0. ## Setup @@ -20,15 +21,15 @@ In a terminal, navigate to the directory where `package.json` resides. Then type 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 2. Select the **App Registrations** blade on the left, then select **New registration**. 3. In the **Register an application page** that appears, enter registration information: - - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `Confidential Client Application`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `Confidential Client Application`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. 4. Select **Register** to create the application. 5. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. These values will be used in the app's configuration file(s) later. 6. In the app's registration screen, select the **Certificates & secrets** blade in the left. - - In the **Client secrets** section, select **New client secret**. - - Type a key description (for instance `app_secret`), - - Select one of the available key durations (6 months, 12 months or Custom). - - The generated key value will be displayed when the **Add** button is selected. Copy and save the generated value for use in later steps. + - In the **Client secrets** section, select **New client secret**. + - Type a key description (for instance `app_secret`), + - Select one of the available key durations (6 months, 12 months or Custom). + - The generated key value will be displayed when the **Add** button is selected. Copy and save the generated value for use in later steps. Before running the sample, the values in the configuration object in app.ts or express.ts will need to be replaced: @@ -46,11 +47,13 @@ const config = { ## Implement a custom INetworkModule There are three approaches to implementing a custom INetworkModule in this sample. + 1. The default msal-node (as of v1.15.0) INetworkModule has been copied to HttpClientCurrent.ts and can be imported to app.ts or express.ts to be used as a custom INetworkModule. HttpClientCurrent.ts can be edited to include console.log()'s to see how network traffic is processed. 2. The pre-proxy-support msal-node INetworkModule has been copied to HttpClientAxios.ts and can be imported to app.ts or express.ts to be used as a custom INetworkModule. HttpClientAxios.ts can be edited to include console.log()'s to see how network traffic is processed. `NOTE: Axios does not support proxy functionality. Therefore, neither does HttpClientAxios.` 3. A custom INetworkModule can be implemeted inline in the system configuration. Stubs to mock the default implementation of INetworkModule have been provided. ## Use Fiddler Everywhere to perform a network trace + Fiddler acts as a proxy and monitors all traffic on a local network 1. Download and install [Fiddler Everywhere](https://www.telerik.com/download/fiddler-everywhere) @@ -63,22 +66,29 @@ Before running the sample (and everytime changes are made to the sample), the Ty ```console npx tsc ``` + This will compile the TypeScript into JavaScript, and put the compiled files in the /dist folder. The sample can now be run by typing: + ```console node dist/app.js ``` + or + ```console node dist/express.js ``` Two different npm scripts, which will run the above npx and node commands, has been configured in package.json. To compile and start either sample, type: + ```console npm run start:app ``` + or + ```console npm run start:express ``` diff --git a/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/app.ts b/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/app.ts index a98b4e3c45..7cf3970503 100644 --- a/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/app.ts +++ b/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/app.ts @@ -23,7 +23,6 @@ const clientConfig: msal.Configuration = { */ // proxyUrl: "http://localhost:8866", // Fiddler Everywhere default port // proxyUrl: "http://localhost:8888", // Fiddler Classic default port - /** * Uncomment either of the HttpClient import statement to use a custom INetworkModule * The contents of ./HttpClientCurrent.ts are the default msal-node network functionality (msal-node v1.15.0), copied from: @@ -36,7 +35,6 @@ const clientConfig: msal.Configuration = { // networkClient: new HttpClientCurrent, // networkClient: new HttpClientCurrent(, ), // networkClient: new HttpClientAxios, - /** * This is the same functionality as the networkClient lines above. Instead of importing a custom INetworkModule, one can be implemented here. * Uncomment the INetworkModule import statement to implement the custom INetworkModule below @@ -61,10 +59,14 @@ const request: msal.ClientCredentialRequest = { // self-executing anonymous asyc function that's needed to use "await" for acquireTokenByClientCredential (async () => { try { - const confidentialClientApplication = new msal.ConfidentialClientApplication(clientConfig); - const response = await confidentialClientApplication.acquireTokenByClientCredential(request); + const confidentialClientApplication = + new msal.ConfidentialClientApplication(clientConfig); + const response = + await confidentialClientApplication.acquireTokenByClientCredential( + request + ); console.log(response); } catch (error) { console.log(error); } -})(); \ No newline at end of file +})(); diff --git a/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/express.ts b/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/express.ts index 73da7c75ca..e24f0a4e96 100644 --- a/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/express.ts +++ b/samples/msal-node-samples/custom-INetworkModule-and-network-tracing/express.ts @@ -28,7 +28,6 @@ const clientConfig: msal.Configuration = { */ // networkClient: new HttpClientCurrent, // networkClient: new HttpClientAxios, - /** * This is the same functionality as the networkClient lines above. Instead of importing a custom INetworkModule, one can be implemented here. * Uncomment the INetworkModule import statement to implement the custom INetworkModule below @@ -57,12 +56,18 @@ app.get("/", async (req, res) => { console.log(`Request received - ${new Date()}`); try { - const confidentialClientApplication = new msal.ConfidentialClientApplication(clientConfig); - const response = await confidentialClientApplication.acquireTokenByClientCredential(request); + const confidentialClientApplication = + new msal.ConfidentialClientApplication(clientConfig); + const response = + await confidentialClientApplication.acquireTokenByClientCredential( + request + ); console.log(response); } catch (error) { console.log(error); } }); -app.listen(SERVER_PORT, () => console.log(`Msal Node web app listening on port ${SERVER_PORT}!`)) \ No newline at end of file +app.listen(SERVER_PORT, () => + console.log(`Msal Node web app listening on port ${SERVER_PORT}!`) +); diff --git a/samples/msal-node-samples/device-code/README.md b/samples/msal-node-samples/device-code/README.md index 9b53b47194..7f956b3f77 100644 --- a/samples/msal-node-samples/device-code/README.md +++ b/samples/msal-node-samples/device-code/README.md @@ -1,4 +1,4 @@ -# MSAL Node Sample: Device Code +# MSAL Node Sample: Device Code This sample application demonstrates how to use the Device Code Grant APIs provided by MSAL Node.js in a Node application. @@ -6,49 +6,53 @@ Once MSAL Node is installed, and you have the right files, come here to learn ab ### How is this scenario used? -The Device Code flow is most commonly used a device that can display text, but not a web ux, and should support user interaction. General information about this scenario is available [here](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-device-code). +The Device Code flow is most commonly used a device that can display text, but not a web ux, and should support user interaction. General information about this scenario is available [here](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-device-code). It can be used for devices with a small display, or connected devices over SSH. ## Test the Sample ### Configure the application + Open the `config/customConfig.json` file. We will change this to add details about our app registration and deployment. -By default, this configuration is set to support all Microsoft accounts. This includes Microsoft Entra accounts used by organizations, and MSA accounts typically used by consumers. +By default, this configuration is set to support all Microsoft accounts. This includes Microsoft Entra accounts used by organizations, and MSA accounts typically used by consumers. Before proceeding, go to the Microsoft Entra admin center, and open the app registration for this app. #### **Client ID** -Within the "Overview" you will see a GUID labeled **Application (client) ID**. Copy this GUID to the clientId field in the config. + +Within the "Overview" you will see a GUID labeled **Application (client) ID**. Copy this GUID to the clientId field in the config. Click the **Authentication** link in the left nav. #### **Authority** + Check that supported account types are: **Accounts in any organizational directory (Any Microsoft Entra ID directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)** If so, then set the authority attribute in the JSON configuraiton file to `https://login.microsoftonline.com/common` -For other supported account types, review the other [Authority options](https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration). Unless there is a specific need to restrict users of your app to an organization, we strongly suggest that everyone use the default authority. User restrictions can be placed later in the application flow if needed. +For other supported account types, review the other [Authority options](https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration). Unless there is a specific need to restrict users of your app to an organization, we strongly suggest that everyone use the default authority. User restrictions can be placed later in the application flow if needed. 🎉You have finished the basic configuration!🎉 ### Executing the application -From the command line, let npm install any needed dependencies. This only needs to be done once. +From the command line, let npm install any needed dependencies. This only needs to be done once. ```bash $ npm install ``` + Once the dependencies are installed, you can run the sample application by using the following command: ```bash $ npm start ``` -By default, the sample will display a code and the web address to visit to validate the code. That should be visited by a web capable device. On completion, the sample will automatically receive the response and complete authentication. +By default, the sample will display a code and the web address to visit to validate the code. That should be visited by a web capable device. On completion, the sample will automatically receive the response and complete authentication. ### Customizing the application @@ -58,14 +62,14 @@ To customize the start script, review the `package.json` file. ### Import the Configuration Object -If you set up the sample with your app registration, you may be able to copy this object directly into your application. +If you set up the sample with your app registration, you may be able to copy this object directly into your application. ```javascript const config = { auth: { clientId: "YOUR_CLIENT_ID", - authority: "YOUR_AUTHORITY" -    } + authority: "YOUR_AUTHORITY", + }, }; ``` @@ -74,7 +78,7 @@ const config = { Add the dependency on MSAL Node to your Node app. ```javascript -const msal = require('@azure/msal-node'); +const msal = require("@azure/msal-node"); ``` ### Initialize MSAL Node at runtime @@ -87,11 +91,12 @@ const pca = new msal.PublicClientApplication(msalConfig); ### Configure Sign In Request -The device code sample immediately initiates the authentication. If you want to gate the authentication behind other logic, then move this next configuration and request. +The device code sample immediately initiates the authentication. If you want to gate the authentication behind other logic, then move this next configuration and request. -Next we have to pick the `scopes` related to the user. If we are logging in a user, then we must at least request access to basic user information. The default scope of `user.read` grants that basic access. To learn more see the [Microsoft Graph permissions reference](https://docs.microsoft.com/graph/permissions-reference). +Next we have to pick the `scopes` related to the user. If we are logging in a user, then we must at least request access to basic user information. The default scope of `user.read` grants that basic access. To learn more see the [Microsoft Graph permissions reference](https://docs.microsoft.com/graph/permissions-reference). **device-code/config/customConfig.json:** + ```json { ..., @@ -104,18 +109,21 @@ Next we have to pick the `scopes` related to the user. If we are logging in a u ... ``` -Next we send the authentication request. The following code block handles the request and the response. +Next we send the authentication request. The following code block handles the request and the response. ```javascript -return clientApplication.acquireTokenByDeviceCode(deviceCodeRequest).then((response) => { +return clientApplication + .acquireTokenByDeviceCode(deviceCodeRequest) + .then((response) => { return response; - }).catch((error) => { + }) + .catch((error) => { return error; }); ``` ### The User Experience -What happens if the user logs in, closes the window, returns to the site, and logs in again? Microsoft supports many, many complex scenarios with many, many forms of authentication: certificates, hardware keys, federated experiences, and even biometrics in some cases. Let our library handle the complexity of deciding the simplest way of logging in the user. +What happens if the user logs in, closes the window, returns to the site, and logs in again? Microsoft supports many, many complex scenarios with many, many forms of authentication: certificates, hardware keys, federated experiences, and even biometrics in some cases. Let our library handle the complexity of deciding the simplest way of logging in the user. -For this flow, a new code is generated, and a new user interaction is required with each request. +For this flow, a new code is generated, and a new user interaction is required with each request. diff --git a/samples/msal-node-samples/device-code/index.js b/samples/msal-node-samples/device-code/index.js index f37943cf4a..bfe5c79371 100644 --- a/samples/msal-node-samples/device-code/index.js +++ b/samples/msal-node-samples/device-code/index.js @@ -77,12 +77,12 @@ const getTokenDeviceCode = function (scenarioConfig, clientApplication, runtimeO * and execute the sample application. */ if(argv.$0 === "index.js") { - const loggerOptions = { - loggerCallback(loglevel, message, containsPii) { -     console.log(message); + const loggerOptions = { + loggerCallback(loglevel, message, containsPii) { + console.log(message); }, -         piiLoggingEnabled: false, - logLevel: msal.LogLevel.Verbose, + piiLoggingEnabled: false, + logLevel: msal.LogLevel.Verbose, } // Build MSAL Client Configuration from scenario configuration file diff --git a/samples/msal-node-samples/on-behalf-of-distributed-cache/README.md b/samples/msal-node-samples/on-behalf-of-distributed-cache/README.md index eb761fed09..be7328711a 100644 --- a/samples/msal-node-samples/on-behalf-of-distributed-cache/README.md +++ b/samples/msal-node-samples/on-behalf-of-distributed-cache/README.md @@ -1,6 +1,6 @@ # MSAL Node Standalone Sample: Web API using On-Behalf-Of Flow -This sample demonstrates how to implement an MSAL Node [confidential client application](../../../lib/msal-node/docs/initialize-confidential-client-application.md) calling a protected web API (aka *middle-tier*) which in turn calls Microsoft Graph using the [OAuth 2.0 on-behalf-of flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow). +This sample demonstrates how to implement an MSAL Node [confidential client application](../../../lib/msal-node/docs/initialize-confidential-client-application.md) calling a protected web API (aka _middle-tier_) which in turn calls Microsoft Graph using the [OAuth 2.0 on-behalf-of flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow). In addition, this sample uses a custom cache plugin to implement the [distributed token caching](../../../lib/msal-node/docs/caching.md#performance-and-security) pattern. Here, the cache is persisted via [Redis](https://redis.io/) and [node-redis](https://github.com/NodeRedis/node-redis). @@ -19,23 +19,23 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-webapi`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-webapi`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. 1. Select **Register** to create the application. 1. 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. 1. Select **Save** to save your changes. 1. In the app's registration screen, select the **Certificates & secrets** blade in the left to open the page where we can generate secrets and upload certificates. 1. In the **Client secrets** section, select **New client secret**: - - Type a key description (for instance `app secret`), - - Select one of the available key durations (**In 1 year**, **In 2 years**, or **Never Expires**) as per your security posture. - - The generated key value will be displayed when you select the **Add** button. Copy the generated value for use in the steps later. - > :warning: In production, use certificates with Azure Key Vault instead of secrets. See [certificate-credentials.md](../../../lib/msal-node/docs/certificate-credentials.md) and [key-vault.md](../../../lib/msal-node/docs/key-vault-managed-identity.md) for more information and examples. + - Type a key description (for instance `app secret`), + - Select one of the available key durations (**In 1 year**, **In 2 years**, or **Never Expires**) as per your security posture. + - The generated key value will be displayed when you select the **Add** button. Copy the generated value for use in the steps later. + > :warning: In production, use certificates with Azure Key Vault instead of secrets. See [certificate-credentials.md](../../../lib/msal-node/docs/certificate-credentials.md) and [key-vault.md](../../../lib/msal-node/docs/key-vault-managed-identity.md) for more information and examples. 1. In the app's registration screen, select the **Expose an API** blade to the left to open the page where you can declare the parameters to expose this app as an API for which client applications can obtain [access tokens](https://docs.microsoft.com/azure/active-directory/develop/access-tokens) for. -The first thing that we need to do is to declare the unique [resource](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) URI that the clients will be using to obtain access tokens for this API. To declare an resource URI, follow the following steps: - - Select `Set` next to the **Application ID URI** to generate a URI that is unique for this app. - - For this sample, accept the proposed Application ID URI (`api://{clientId}`) by selecting **Save**. + The first thing that we need to do is to declare the unique [resource](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) URI that the clients will be using to obtain access tokens for this API. To declare an resource URI, follow the following steps: + - Select `Set` next to the **Application ID URI** to generate a URI that is unique for this app. + - For this sample, accept the proposed Application ID URI (`api://{clientId}`) by selecting **Save**. 1. All APIs have to publish a minimum of one [scope](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code) for the client's to obtain an access token successfully. To publish a scope, follow the following steps: - - Select **Add a scope** button open the **Add a scope** screen and Enter the values as indicated below: + - Select **Add a scope** button open the **Add a scope** screen and Enter the values as indicated below: - For **Scope name**, use `access_as_user`. - Select **Admins and users** options for **Who can consent?**. - For **Admin consent display name** type `Access msal-node-webapi`. @@ -45,14 +45,14 @@ The first thing that we need to do is to declare the unique [resource](https://d - Keep **State** as **Enabled**. - Select the **Add scope** button on the bottom to save this scope. 1. On the left hand side menu, select the `Manifest` blade. - - Set `accessTokenAcceptedVersion` property to **2**. - - Modify the `"knownClientApplications": []` array to contain the client ID/app ID of the client application(S) that will call this web API (for example `knownClientApplications": ["APP_ID_OF_THE_CLIENT_APP"]`) - - Click on **Save**. + - Set `accessTokenAcceptedVersion` property to **2**. + - Modify the `"knownClientApplications": []` array to contain the client ID/app ID of the client application(S) that will call this web API (for example `knownClientApplications": ["APP_ID_OF_THE_CLIENT_APP"]`) + - Click on **Save**. Before running the sample, you'll need to: -- make sure the Redis service is running -- replace the values in the configuration object (see [app.ts](./src/app.ts)): +- make sure the Redis service is running +- replace the values in the configuration object (see [app.ts](./src/app.ts)): ```typescript const appConfig: AppConfig = { @@ -60,7 +60,7 @@ const appConfig: AppConfig = { tenantId: process.env.TENANT_ID || "ENTER_TENANT_ID_HERE", clientId: process.env.CLIENT_ID || "ENTER_CLIENT_ID_HERE", clientSecret: process.env.CLIENT_SECRET || "ENTER_CLIENT_SECRET_HERE", - permissions: process.env.PERMISSIONS || "ENTER_REQUIRED_PERMISSIONS_HERE" // e.g. "access_as_user" + permissions: process.env.PERMISSIONS || "ENTER_REQUIRED_PERMISSIONS_HERE", // e.g. "access_as_user" }; ``` @@ -88,6 +88,6 @@ For persisting tokens using a distributed cache, web APIs using OBO flow should ## More information -- [Scenario: A web API that calls web APIs](https://docs.microsoft.com/azure/active-directory/develop/scenario-web-api-call-api-overview) -- [The knownClientApplications attribute](https://docs.microsoft.com/azure/active-directory/develop/reference-app-manifest#knownclientapplications-attribute) -- [The /.default scope](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope) +- [Scenario: A web API that calls web APIs](https://docs.microsoft.com/azure/active-directory/develop/scenario-web-api-call-api-overview) +- [The knownClientApplications attribute](https://docs.microsoft.com/azure/active-directory/develop/reference-app-manifest#knownclientapplications-attribute) +- [The /.default scope](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope) diff --git a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/AuthProvider.ts b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/AuthProvider.ts index 0c47954fbd..421b025df1 100644 --- a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/AuthProvider.ts +++ b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/AuthProvider.ts @@ -11,7 +11,7 @@ import { ConfidentialClientApplication, AuthenticationResult, CryptoProvider, - OnBehalfOfRequest + OnBehalfOfRequest, } from "@azure/msal-node"; import CustomCachePlugin from "./CustomCachePlugin"; @@ -31,13 +31,19 @@ export class AuthProvider { private cryptoProvider: CryptoProvider; private cacheClientWrapper: RedisClientWrapper; - private constructor(msalConfig: Configuration, cacheClient: RedisClientType) { + private constructor( + msalConfig: Configuration, + cacheClient: RedisClientType + ) { this.msalConfig = msalConfig; this.cryptoProvider = new CryptoProvider(); this.cacheClientWrapper = new RedisClientWrapper(cacheClient); } - static async initialize(appConfig: AppConfig, cacheClient: RedisClientType): Promise { + static async initialize( + appConfig: AppConfig, + cacheClient: RedisClientType + ): Promise { const msalConfig = { auth: { clientId: appConfig.clientId, @@ -50,19 +56,26 @@ export class AuthProvider { console.log(message); }, logLevel: LogLevel.Trace, - piiLoggingEnabled: false + piiLoggingEnabled: false, }, // proxyUrl: "http://localhost:8888" // uncomment to capture traffic with Fiddler - } + }, } as Configuration; - const msalConfigWithMetadata = await AuthProvider.getMetadata(msalConfig, cacheClient); + const msalConfigWithMetadata = await AuthProvider.getMetadata( + msalConfig, + cacheClient + ); return new AuthProvider(msalConfigWithMetadata, cacheClient); } - async getToken(tokenRequest: OnBehalfOfRequest): Promise { - const partitionKey = await this.cryptoProvider.hashString(tokenRequest.oboAssertion); + async getToken( + tokenRequest: OnBehalfOfRequest + ): Promise { + const partitionKey = await this.cryptoProvider.hashString( + tokenRequest.oboAssertion + ); const cca = new ConfidentialClientApplication({ ...this.msalConfig, @@ -70,8 +83,8 @@ export class AuthProvider { cachePlugin: new CustomCachePlugin( this.cacheClientWrapper, partitionKey // partitionKey hash(oboAssertion) - ) - } + ), + }, }); performance.mark("acquireTokenOnBehalfOf-start"); @@ -81,7 +94,9 @@ export class AuthProvider { performance.mark("acquireTokenOnBehalfOf-end"); performance.measure( - tokenResponse?.fromCache ? "acquireTokenOnBehalfOf-fromCache" : "acquireTokenOnBehalfOf-fromNetwork", + tokenResponse?.fromCache + ? "acquireTokenOnBehalfOf-fromCache" + : "acquireTokenOnBehalfOf-fromNetwork", "acquireTokenOnBehalfOf-start", "acquireTokenOnBehalfOf-end" ); @@ -89,32 +104,57 @@ export class AuthProvider { return tokenResponse; } - private static async getMetadata(msalConfig: Configuration, cacheClient: RedisClientType): Promise { + private static async getMetadata( + msalConfig: Configuration, + cacheClient: RedisClientType + ): Promise { const msalConfigWithMetadata = msalConfig; const clientId = msalConfigWithMetadata.auth.clientId; - const tenantId = msalConfigWithMetadata.auth.authority!.split("/").pop()!; + const tenantId = msalConfigWithMetadata.auth + .authority!.split("/") + .pop()!; try { - let [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([ - cacheClient.get(`${clientId}.${tenantId}.discovery-metadata`), - cacheClient.get(`${clientId}.${tenantId}.authority-metadata`) - ]); + let [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all( + [ + cacheClient.get( + `${clientId}.${tenantId}.discovery-metadata` + ), + cacheClient.get( + `${clientId}.${tenantId}.authority-metadata` + ), + ] + ); if (!cloudDiscoveryMetadata || !authorityMetadata) { - [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all([ - AuthProvider.fetchCloudDiscoveryMetadata(tenantId), - AuthProvider.fetchOIDCMetadata(tenantId) - ]); + [cloudDiscoveryMetadata, authorityMetadata] = await Promise.all( + [ + AuthProvider.fetchCloudDiscoveryMetadata(tenantId), + AuthProvider.fetchOIDCMetadata(tenantId), + ] + ); if (cloudDiscoveryMetadata && authorityMetadata) { - await cacheClient.set(`${clientId}.${tenantId}.discovery-metadata`, JSON.stringify(cloudDiscoveryMetadata)); - await cacheClient.set(`${clientId}.${tenantId}.authority-metadata`, JSON.stringify(authorityMetadata)); + await cacheClient.set( + `${clientId}.${tenantId}.discovery-metadata`, + JSON.stringify(cloudDiscoveryMetadata) + ); + await cacheClient.set( + `${clientId}.${tenantId}.authority-metadata`, + JSON.stringify(authorityMetadata) + ); } } - msalConfigWithMetadata.auth.cloudDiscoveryMetadata = typeof cloudDiscoveryMetadata === "string" ? cloudDiscoveryMetadata : JSON.stringify(cloudDiscoveryMetadata); - msalConfigWithMetadata.auth.authorityMetadata = typeof authorityMetadata === "string" ? authorityMetadata : JSON.stringify(authorityMetadata); + msalConfigWithMetadata.auth.cloudDiscoveryMetadata = + typeof cloudDiscoveryMetadata === "string" + ? cloudDiscoveryMetadata + : JSON.stringify(cloudDiscoveryMetadata); + msalConfigWithMetadata.auth.authorityMetadata = + typeof authorityMetadata === "string" + ? authorityMetadata + : JSON.stringify(authorityMetadata); } catch (error) { console.log(error); } @@ -122,14 +162,21 @@ export class AuthProvider { return msalConfigWithMetadata; } - private static async fetchCloudDiscoveryMetadata(tenantId: string): Promise { - const endpoint = "https://login.microsoftonline.com/common/discovery/instance"; + private static async fetchCloudDiscoveryMetadata( + tenantId: string + ): Promise { + const endpoint = + "https://login.microsoftonline.com/common/discovery/instance"; try { - const response = await AxiosHelper.callDownstreamApi(endpoint, undefined, { - "api-version": "1.1", - "authorization_endpoint": `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize` - }); + const response = await AxiosHelper.callDownstreamApi( + endpoint, + undefined, + { + "api-version": "1.1", + authorization_endpoint: `https://login.microsoftonline.com/${tenantId}/oauth2/v2.0/authorize`, + } + ); return response; } catch (error) { @@ -141,7 +188,7 @@ export class AuthProvider { const endpoint = `https://login.microsoftonline.com/${tenantId}/v2.0/.well-known/openid-configuration`; try { - const response = await AxiosHelper.callDownstreamApi(endpoint) + const response = await AxiosHelper.callDownstreamApi(endpoint); return response; } catch (error) { console.log(error); diff --git a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/AxiosHelper.ts b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/AxiosHelper.ts index 46d3046d4f..3a9358b617 100644 --- a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/AxiosHelper.ts +++ b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/AxiosHelper.ts @@ -6,7 +6,6 @@ import axios from "axios"; class AxiosHelper { - /** * Makes an HTTP GET to the endpoint uri. If an access token exists, it includes * an Authorization header in the request. The header contains the bearer token. @@ -14,19 +13,25 @@ class AxiosHelper { * @param accessToken - raw access token * @param params - parameters object for the request in the form of key-value pairs */ - static async callDownstreamApi(endpoint: string, accessToken?: string, params?: Record): Promise { + static async callDownstreamApi( + endpoint: string, + accessToken?: string, + params?: Record + ): Promise { console.log(`Request to ${endpoint} made at: ${new Date().toString()}`); const response = await axios.get(endpoint, { - headers: (accessToken && { Authorization: `Bearer ${accessToken}`}) || undefined, - params: params || undefined + headers: + (accessToken && { Authorization: `Bearer ${accessToken}` }) || + undefined, + params: params || undefined, }); if (response.status !== 200) { throw new Error(`Response: ${response.status}`); } - return (await response.data); + return await response.data; } } diff --git a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/CustomCachePlugin.ts b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/CustomCachePlugin.ts index 34b3544830..1e26c06aad 100644 --- a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/CustomCachePlugin.ts +++ b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/CustomCachePlugin.ts @@ -3,7 +3,11 @@ * Licensed under the MIT License. */ -import { ICacheClient, ICachePlugin, TokenCacheContext } from "@azure/msal-node"; +import { + ICacheClient, + ICachePlugin, + TokenCacheContext, +} from "@azure/msal-node"; import { performance } from "perf_hooks"; /** @@ -20,20 +24,35 @@ class CustomCachePlugin implements ICachePlugin { this.partitionKey = partitionKey; } - public async beforeCacheAccess(cacheContext: TokenCacheContext): Promise { + public async beforeCacheAccess( + cacheContext: TokenCacheContext + ): Promise { performance.mark("beforeCacheAccess-start"); const cacheData = await this.client.get(this.partitionKey); cacheContext.tokenCache.deserialize(cacheData); performance.mark("beforeCacheAccess-end"); - performance.measure("beforeCacheAccess", "beforeCacheAccess-start", "beforeCacheAccess-end"); + performance.measure( + "beforeCacheAccess", + "beforeCacheAccess-start", + "beforeCacheAccess-end" + ); } - public async afterCacheAccess(cacheContext: TokenCacheContext): Promise { + public async afterCacheAccess( + cacheContext: TokenCacheContext + ): Promise { if (cacheContext.cacheHasChanged) { performance.mark("afterCacheAccess-start"); - await this.client.set(this.partitionKey, cacheContext.tokenCache.serialize()); + await this.client.set( + this.partitionKey, + cacheContext.tokenCache.serialize() + ); performance.mark("afterCacheAccess-end"); - performance.measure("afterCacheAccess", "afterCacheAccess-start", "afterCacheAccess-end"); + performance.measure( + "afterCacheAccess", + "afterCacheAccess-start", + "afterCacheAccess-end" + ); } } } diff --git a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/RedisClientWrapper.ts b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/RedisClientWrapper.ts index e718bff8e1..66e1207b4a 100644 --- a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/RedisClientWrapper.ts +++ b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/RedisClientWrapper.ts @@ -14,11 +14,11 @@ const EVICTION_POLICY = "volatile-lru"; const EMPTY_STRING = ""; /** -* Simple persistence client helper, using Redis (node-redis). You must have redis installed -* on your machine and have redis server listening. Note that this is only for illustration, -* and you'll need to consider cache eviction policies and handle cache server connection -* issues. For more information, visit: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md -*/ + * Simple persistence client helper, using Redis (node-redis). You must have redis installed + * on your machine and have redis server listening. Note that this is only for illustration, + * and you'll need to consider cache eviction policies and handle cache server connection + * issues. For more information, visit: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-node/docs/caching.md + */ class RedisClientWrapper implements ICacheClient { cacheClient: RedisClientType; @@ -40,7 +40,7 @@ class RedisClientWrapper implements ICacheClient { */ public async get(key: string): Promise { try { - return await this.cacheClient.get(key) || EMPTY_STRING; + return (await this.cacheClient.get(key)) || EMPTY_STRING; } catch (error) { console.log(error); } @@ -56,9 +56,11 @@ class RedisClientWrapper implements ICacheClient { */ public async set(key: string, value: string): Promise { try { - return await this.cacheClient.set(key, value, { - EX: CACHE_TTL - }) || EMPTY_STRING; + return ( + (await this.cacheClient.set(key, value, { + EX: CACHE_TTL, + })) || EMPTY_STRING + ); } catch (error) { console.log(error); } diff --git a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/TokenValidator.ts b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/TokenValidator.ts index c263a1e428..a9aab1516f 100644 --- a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/TokenValidator.ts +++ b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/TokenValidator.ts @@ -41,8 +41,10 @@ class TokenValidator { * @param {string} rawAccessToken * @returns {Promise} */ - async validateAccessToken(rawAccessToken: string, idTokenClaims: IdTokenClaims): Promise { - + async validateAccessToken( + rawAccessToken: string, + idTokenClaims: IdTokenClaims + ): Promise { /** * A JWT token validation is a 2-step process comprising of: * (1) signature validation @@ -52,7 +54,9 @@ class TokenValidator { * https://learn.microsoft.com/azure/active-directory/develop/access-tokens#validate-tokens */ try { - const isTokenSignatureValid = await this.validateTokenSignature(rawAccessToken); + const isTokenSignatureValid = await this.validateTokenSignature( + rawAccessToken + ); if (!isTokenSignatureValid) { return false; @@ -63,14 +67,16 @@ class TokenValidator { console.log(error); return false; } - }; + } /** * Validates the access token for a set of claims * @param {AccessTokenClaims} accessTokenClaims: decoded access token claims * @returns {boolean} */ - private validateAccessTokenClaims(accessTokenClaims: AccessTokenClaims): boolean { + private validateAccessTokenClaims( + accessTokenClaims: AccessTokenClaims + ): boolean { const now = Math.round(new Date().getTime() / 1000.0); // current time in seconds /** @@ -81,27 +87,52 @@ class TokenValidator { * then it can ignore the issuer value altogether. For more information, visit: * https://learn.microsoft.com/azure/active-directory/develop/howto-convert-app-to-be-multi-tenant#update-your-code-to-handle-multiple-issuer-values */ - const isMultiTenant = ["common", "organizations", "consumers"] - .some((val) => this.appConfig.tenantId === val) ? true : false; + const isMultiTenant = ["common", "organizations", "consumers"].some( + (val) => this.appConfig.tenantId === val + ) + ? true + : false; /** * At the very least, check for issuer, audience and expiry dates. For more information * on validating access tokens claims, visit: * https://docs.microsoft.com/azure/active-directory/develop/access-tokens#validate-tokens */ - const checkIssuer = isMultiTenant ? true : accessTokenClaims.iss?.endsWith(`${this.appConfig.tenantId}/`) ? true : false; - const checkAudience = (accessTokenClaims.aud === this.appConfig.clientId || accessTokenClaims.aud === `api://${this.appConfig.clientId}`) ? true : false; - const checkTimestamp = (accessTokenClaims.iat && accessTokenClaims.iat <= now) && (accessTokenClaims.exp && accessTokenClaims.exp >= now) ? true : false; - const checkPermissions = accessTokenClaims.scp && accessTokenClaims.scp.includes(this.appConfig.permissions) ? true : false; - return checkIssuer && checkAudience && checkTimestamp && checkPermissions; - }; + const checkIssuer = isMultiTenant + ? true + : accessTokenClaims.iss?.endsWith(`${this.appConfig.tenantId}/`) + ? true + : false; + const checkAudience = + accessTokenClaims.aud === this.appConfig.clientId || + accessTokenClaims.aud === `api://${this.appConfig.clientId}` + ? true + : false; + const checkTimestamp = + accessTokenClaims.iat && + accessTokenClaims.iat <= now && + accessTokenClaims.exp && + accessTokenClaims.exp >= now + ? true + : false; + const checkPermissions = + accessTokenClaims.scp && + accessTokenClaims.scp.includes(this.appConfig.permissions) + ? true + : false; + return ( + checkIssuer && checkAudience && checkTimestamp && checkPermissions + ); + } /** * Verifies a given tokens signature * @param {string} authToken * @returns {Promise} */ - private async validateTokenSignature(rawAuthToken: string): Promise { + private async validateTokenSignature( + rawAuthToken: string + ): Promise { if (!rawAuthToken) { return false; } @@ -118,7 +149,9 @@ class TokenValidator { let keys; // obtain signing keys from discovery endpoint try { - keys = decodedToken && await this.getSigningKeys(decodedToken.header); + keys = + decodedToken && + (await this.getSigningKeys(decodedToken.header)); } catch (error) { console.log(error); return false; @@ -132,7 +165,7 @@ class TokenValidator { console.log(error); return false; } - }; + } /** * Fetches signing keys from the openid-configuration endpoint @@ -141,7 +174,7 @@ class TokenValidator { */ private async getSigningKeys(header: any): Promise { return (await this.keyClient.getSigningKey(header.kid)).getPublicKey(); - }; + } } export default TokenValidator; diff --git a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/app.ts b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/app.ts index 50b80d5ffd..d1fedaccf9 100644 --- a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/app.ts +++ b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/app.ts @@ -1,12 +1,16 @@ import fs from "fs"; import dotenv from "dotenv"; -import express, { Express, Request, Response, NextFunction } from 'express'; +import express, { Express, Request, Response, NextFunction } from "express"; import { createClient, RedisClientType } from "redis"; -import { PerformanceObserver, PerformanceObserverEntryList, PerformanceEntry } from "perf_hooks"; +import { + PerformanceObserver, + PerformanceObserverEntryList, + PerformanceEntry, +} from "perf_hooks"; -import { AuthProvider, AppConfig } from './AuthProvider'; -import { isAuthorized } from './middleware'; -import AxiosHelper from './AxiosHelper'; +import { AuthProvider, AppConfig } from "./AuthProvider"; +import { isAuthorized } from "./middleware"; +import AxiosHelper from "./AxiosHelper"; export const port = process.env.PORT || 5000; export const app: Express = express(); @@ -18,7 +22,7 @@ const appConfig: AppConfig = { tenantId: process.env.TENANT_ID || "ENTER_TENANT_ID_HERE", clientId: process.env.CLIENT_ID || "ENTER_CLIENT_ID_HERE", clientSecret: process.env.CLIENT_SECRET || "ENTER_CLIENT_SECRET_HERE", - permissions: process.env.PERMISSIONS || "ENTER_REQUIRED_PERMISSIONS_HERE" // e.g. "access_as_user" + permissions: process.env.PERMISSIONS || "ENTER_REQUIRED_PERMISSIONS_HERE", // e.g. "access_as_user" }; async function main() { @@ -27,13 +31,13 @@ async function main() { const authProvider = await AuthProvider.initialize(appConfig, cacheClient); app.get( - '/obo', + "/obo", isAuthorized(appConfig), // check if the access token is valid async (req: Request, res: Response, next: NextFunction) => { try { const tokenResponse = await authProvider.getToken({ - oboAssertion: req.headers.authorization?.split(' ')[1]!, - scopes: ['User.Read'], + oboAssertion: req.headers.authorization?.split(" ")[1]!, + scopes: ["User.Read"], }); const graphResponse = await AxiosHelper.callDownstreamApi( @@ -54,7 +58,7 @@ async function main() { // set locals, only providing error in development res.locals.message = err.message; - res.locals.error = req.app.get('env') === 'development' ? err : {}; + res.locals.error = req.app.get("env") === "development" ? err : {}; // send error response res.status(err.status || 500).send(err); @@ -67,50 +71,63 @@ async function main() { } function initializePerformanceObserver(): void { - const perfObserver = new PerformanceObserver((items: PerformanceObserverEntryList) => { - let durationInCacheInMs = 0; - let durationTotalInMs = 0; - let tokenSource; - - items.getEntriesByName("beforeCacheAccess").forEach((entry: PerformanceEntry) => { - durationInCacheInMs += entry.duration; - }); - - items.getEntriesByName("afterCacheAccess").forEach((entry: PerformanceEntry) => { - durationInCacheInMs += entry.duration; - }); - - items.getEntriesByName("acquireTokenOnBehalfOf-fromNetwork").forEach((entry: PerformanceEntry) => { - durationTotalInMs = entry.duration; - tokenSource = "network"; - }); - - items.getEntriesByName("acquireTokenOnBehalfOf-fromCache").forEach((entry: PerformanceEntry) => { - durationTotalInMs = entry.duration; - tokenSource = "cache"; - }); - - if (tokenSource) { - const results = { - tokenSource, - durationTotalInMs, - durationInCacheInMs, - durationInHttpInMs: tokenSource === "network" ? durationTotalInMs - durationInCacheInMs : 0, - }; - - console.log(results); - - fs.appendFile( - "benchmarks.json", - `${JSON.stringify(results)}\n`, - function (err) { - if (err) { - throw err; + const perfObserver = new PerformanceObserver( + (items: PerformanceObserverEntryList) => { + let durationInCacheInMs = 0; + let durationTotalInMs = 0; + let tokenSource; + + items + .getEntriesByName("beforeCacheAccess") + .forEach((entry: PerformanceEntry) => { + durationInCacheInMs += entry.duration; + }); + + items + .getEntriesByName("afterCacheAccess") + .forEach((entry: PerformanceEntry) => { + durationInCacheInMs += entry.duration; + }); + + items + .getEntriesByName("acquireTokenOnBehalfOf-fromNetwork") + .forEach((entry: PerformanceEntry) => { + durationTotalInMs = entry.duration; + tokenSource = "network"; + }); + + items + .getEntriesByName("acquireTokenOnBehalfOf-fromCache") + .forEach((entry: PerformanceEntry) => { + durationTotalInMs = entry.duration; + tokenSource = "cache"; + }); + + if (tokenSource) { + const results = { + tokenSource, + durationTotalInMs, + durationInCacheInMs, + durationInHttpInMs: + tokenSource === "network" + ? durationTotalInMs - durationInCacheInMs + : 0, + }; + + console.log(results); + + fs.appendFile( + "benchmarks.json", + `${JSON.stringify(results)}\n`, + function (err) { + if (err) { + throw err; + } } - } - ); + ); + } } - }); + ); perfObserver.observe({ entryTypes: ["measure"], buffered: true }); } @@ -122,11 +139,11 @@ async function initializeRedisClient(): Promise { */ const redis = createClient({ socket: { - reconnectStrategy: false - } + reconnectStrategy: false, + }, }); - redis.on('error', (err: any) => console.log('Redis Client Error', err)); + redis.on("error", (err: any) => console.log("Redis Client Error", err)); await redis.connect(); return redis as RedisClientType; diff --git a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/middleware.ts b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/middleware.ts index e42a996b59..a58ffb3885 100644 --- a/samples/msal-node-samples/on-behalf-of-distributed-cache/src/middleware.ts +++ b/samples/msal-node-samples/on-behalf-of-distributed-cache/src/middleware.ts @@ -18,15 +18,17 @@ export const isAuthorized = (appConfig: AppConfig): RequestHandler => { try { const tokenValidator = new TokenValidator(appConfig); - const isTokenValid = await tokenValidator.validateAccessToken(accessToken); + const isTokenValid = await tokenValidator.validateAccessToken( + accessToken + ); if (!isTokenValid) { return res.status(401).json({ message: "Unauthorized" }); - }; + } next(); } catch (error) { next(error); } - } + }; }; diff --git a/samples/msal-node-samples/on-behalf-of/README.md b/samples/msal-node-samples/on-behalf-of/README.md index 00d4c939b5..15bbe99b02 100644 --- a/samples/msal-node-samples/on-behalf-of/README.md +++ b/samples/msal-node-samples/on-behalf-of/README.md @@ -1,8 +1,8 @@ # MSAL Node Standalone Sample: On-Behalf-Of Flow -This sample demonstrates how to implement an MSAL Node [confidential client application](../../../lib/msal-node/docs/initialize-confidential-client-application.md) calling a protected web API (aka *middle-tier*) which in turn calls Microsoft Graph using the [OAuth 2.0 on-behalf-of flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow). +This sample demonstrates how to implement an MSAL Node [confidential client application](../../../lib/msal-node/docs/initialize-confidential-client-application.md) calling a protected web API (aka _middle-tier_) which in turn calls Microsoft Graph using the [OAuth 2.0 on-behalf-of flow](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-on-behalf-of-flow). -The on-behalf-of is most commonly used for a web app calling a web API. That web API can also use the same flow to call subsequent web APIs, thereby establishing an *OBO chain*. +The on-behalf-of is most commonly used for a web app calling a web API. That web API can also use the same flow to call subsequent web APIs, thereby establishing an _OBO chain_. Since the on behalf of flow relies on a web app calling a web API, we rely on two separate app registrations, and two running processes. The `web-app` sample works in tandem with the sample in the `web-api` folder. Both apps must be registered and `index.js` files must be configured. @@ -28,23 +28,23 @@ Then perform the same step for `web-api`: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-webapi`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-webapi`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. 1. Select **Register** to create the application. 1. 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. 1. Select **Save** to save your changes. 1. In the app's registration screen, select the **Certificates & secrets** blade in the left to open the page where we can generate secrets and upload certificates. 1. In the **Client secrets** section, select **New client secret**: - - Type a key description (for instance `app secret`), - - Select one of the available key durations (**In 1 year**, **In 2 years**, or **Never Expires**) as per your security posture. - - The generated key value will be displayed when you select the **Add** button. Copy the generated value for use in the steps later. - - You'll need this key later in your code's configuration files. This key value will not be displayed again, and is not retrievable by any other means, so make sure to note it from the Microsoft Entra admin center before navigating to any other screen or blade. + - Type a key description (for instance `app secret`), + - Select one of the available key durations (**In 1 year**, **In 2 years**, or **Never Expires**) as per your security posture. + - The generated key value will be displayed when you select the **Add** button. Copy the generated value for use in the steps later. + - You'll need this key later in your code's configuration files. This key value will not be displayed again, and is not retrievable by any other means, so make sure to note it from the Microsoft Entra admin center before navigating to any other screen or blade. 1. In the app's registration screen, select the **Expose an API** blade to the left to open the page where you can declare the parameters to expose this app as an API for which client applications can obtain [access tokens](https://docs.microsoft.com/azure/active-directory/develop/access-tokens) for. -The first thing that we need to do is to declare the unique [resource](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) URI that the clients will be using to obtain access tokens for this API. To declare an resource URI, follow the following steps: - - Select `Set` next to the **Application ID URI** to generate a URI that is unique for this app. - - For this sample, accept the proposed Application ID URI (`api://{clientId}`) by selecting **Save**. + The first thing that we need to do is to declare the unique [resource](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow) URI that the clients will be using to obtain access tokens for this API. To declare an resource URI, follow the following steps: + - Select `Set` next to the **Application ID URI** to generate a URI that is unique for this app. + - For this sample, accept the proposed Application ID URI (`api://{clientId}`) by selecting **Save**. 1. All APIs have to publish a minimum of one [scope](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow#request-an-authorization-code) for the client's to obtain an access token successfully. To publish a scope, follow the following steps: - - Select **Add a scope** button open the **Add a scope** screen and Enter the values as indicated below: + - Select **Add a scope** button open the **Add a scope** screen and Enter the values as indicated below: - For **Scope name**, use `access_as_user`. - Select **Admins and users** options for **Who can consent?**. - For **Admin consent display name** type `Access msal-node-webapi`. @@ -54,20 +54,21 @@ The first thing that we need to do is to declare the unique [resource](https://d - Keep **State** as **Enabled**. - Select the **Add scope** button on the bottom to save this scope. 1. On the left hand side menu, select the `Manifest` blade. - - Set `accessTokenAcceptedVersion` property to **2**. - - Click on **Save**. + - Set `accessTokenAcceptedVersion` property to **2**. + - Click on **Save**. Before running the sample, you will need to replace the values in the configuration object: ```javascript -const DISCOVERY_KEYS_ENDPOINT = "https://login.microsoftonline.com/ENTER_TENANT_INFO/discovery/v2.0/keys"; +const DISCOVERY_KEYS_ENDPOINT = + "https://login.microsoftonline.com/ENTER_TENANT_INFO/discovery/v2.0/keys"; const config = { auth: { clientId: "ENTER_CLIENT_ID", authority: "https://login.microsoftonline.com/ENTER_TENANT_INFO", clientSecret: "ENTER_CLIENT_SECRET", - } + }, }; ``` @@ -76,24 +77,24 @@ const config = { 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-webapp`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. - - Under **Redirect URI (optional)**, select **Web** and set the redirect URI to **http://localhost:3000/redirect** + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-webapp`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. + - Under **Redirect URI (optional)**, select **Web** and set the redirect URI to **http://localhost:3000/redirect** 1. Select **Register** to create the application. 1. 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. 1. Select **Save** to save your changes. 1. In the app's registration screen, select the **Certificates & secrets** blade in the left to open the page where we can generate secrets and upload certificates. 1. In the **Client secrets** section, select **New client secret**: - - Type a key description (for instance `app secret`), - - Select one of the available key durations (**In 1 year**, **In 2 years**, or **Never Expires**) as per your security posture. - - The generated key value will be displayed when you select the **Add** button. Copy the generated value for use in the steps later. - - You'll need this key later in your code's configuration files. This key value will not be displayed again, and is not retrievable by any other means, so make sure to note it from the Microsoft Entra admin center before navigating to any other screen or blade. + - Type a key description (for instance `app secret`), + - Select one of the available key durations (**In 1 year**, **In 2 years**, or **Never Expires**) as per your security posture. + - The generated key value will be displayed when you select the **Add** button. Copy the generated value for use in the steps later. + - You'll need this key later in your code's configuration files. This key value will not be displayed again, and is not retrievable by any other means, so make sure to note it from the Microsoft Entra admin center before navigating to any other screen or blade. 1. In the app's registration screen, select the **API permissions** blade in the left to open the page where we add access to the APIs that your application needs. - - Select the **Add a permission** button and then, - - Ensure that the **My APIs** tab is selected. - - In the list of APIs, select the API `msal-node-webapi`. - - In the **Delegated permissions** section, select the **access_as_user** in the list. Use the search box if necessary. - - Select the **Add permissions** button at the bottom. + - Select the **Add a permission** button and then, + - Ensure that the **My APIs** tab is selected. + - In the list of APIs, select the API `msal-node-webapi`. + - In the **Delegated permissions** section, select the **access_as_user** in the list. Use the search box if necessary. + - Select the **Add permissions** button at the bottom. #### Configure Known Client Applications for the middle-tier web API (msal-node-webapi) @@ -139,6 +140,6 @@ Open your browser and navigate to `http://localhost:3000`. This will trigger the ## More information -- [Scenario: A web API that calls web APIs](https://docs.microsoft.com/azure/active-directory/develop/scenario-web-api-call-api-overview) -- [The /.default scope](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope) -- [The knownClientApplications attribute](https://docs.microsoft.com/azure/active-directory/develop/reference-app-manifest#knownclientapplications-attribute) +- [Scenario: A web API that calls web APIs](https://docs.microsoft.com/azure/active-directory/develop/scenario-web-api-call-api-overview) +- [The /.default scope](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent#the-default-scope) +- [The knownClientApplications attribute](https://docs.microsoft.com/azure/active-directory/develop/reference-app-manifest#knownclientapplications-attribute) diff --git a/samples/msal-node-samples/refresh-token/README.md b/samples/msal-node-samples/refresh-token/README.md index bca31e3d67..9c13a1e973 100644 --- a/samples/msal-node-samples/refresh-token/README.md +++ b/samples/msal-node-samples/refresh-token/README.md @@ -19,16 +19,16 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-app`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. - - In the **Redirect URI (optional)** section, select **Web** in the combo-box and enter the following redirect URI: `http://localhost:3000/redirect`. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-app`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Redirect URI (optional)** section, select **Web** in the combo-box and enter the following redirect URI: `http://localhost:3000/redirect`. 1. Select **Register** to create the application. 1. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later. 1. In the app's registration screen, select the **Certificates & secrets** blade in the left. - - In the **Client secrets** section, select **New client secret**. - - Type a key description (for instance `app secret`), - - Select one of the available key durations (6 months, 12 months or Custom) as per your security posture. - - The generated key value will be displayed when you select the **Add** button. Copy and add this client secret to the `.env` file as `CLIENT_SECRET`. + - In the **Client secrets** section, select **New client secret**. + - Type a key description (for instance `app secret`), + - Select one of the available key durations (6 months, 12 months or Custom) as per your security posture. + - The generated key value will be displayed when you select the **Add** button. Copy and add this client secret to the `.env` file as `CLIENT_SECRET`. Before running the sample, you will need to replace the values in the [customConfig.json](./config/customConfig.json): @@ -55,7 +55,7 @@ CLIENT_SECRET= npm run start:adal ``` -1. Open a browser and navigate to `http://localhost:3000`. The app will attempt to interactively acquire tokens for the user. Enter your credentials and consent to the permissions required by the app. Once you do, you should be redirected back to the app, and see the token acquisition response printed on the page. At this point, your tokens should be cached in a file named [cache.json](./data/cache.json) under the *data* folder. +1. Open a browser and navigate to `http://localhost:3000`. The app will attempt to interactively acquire tokens for the user. Enter your credentials and consent to the permissions required by the app. Once you do, you should be redirected back to the app, and see the token acquisition response printed on the page. At this point, your tokens should be cached in a file named [cache.json](./data/cache.json) under the _data_ folder. 1. Stop the ADAL Node app in your terminal. 1. Now start the MSAL Node app. In your terminal, type: @@ -146,5 +146,5 @@ app.get('/acquireToken', async (req, res, next) => { ## More information -- [Microsoft identity platform refresh tokens](https://docs.microsoft.com/azure/active-directory/develop/refresh-tokens) -- [How to migrate a Node.js app from ADAL to MSAL](https://docs.microsoft.com/azure/active-directory/develop/msal-node-migration) +- [Microsoft identity platform refresh tokens](https://docs.microsoft.com/azure/active-directory/develop/refresh-tokens) +- [How to migrate a Node.js app from ADAL to MSAL](https://docs.microsoft.com/azure/active-directory/develop/msal-node-migration) diff --git a/samples/msal-node-samples/silent-flow/README.md b/samples/msal-node-samples/silent-flow/README.md index ecc3f1d8cc..69d1909e4e 100644 --- a/samples/msal-node-samples/silent-flow/README.md +++ b/samples/msal-node-samples/silent-flow/README.md @@ -5,6 +5,7 @@ This sample application demonstrates how to use the **acquireTokenSilent** API p Once MSAL Node is installed, and you have the right files, come here to learn about this scenario. ### How is this scenario used? + The silent flow is most commonly used for a web app that signs in users. General information about this scenario is available [here](https://docs.microsoft.com/azure/active-directory/develop/scenario-web-app-sign-user-overview?tabs=aspnetcore). ## Test the Sample @@ -21,36 +22,37 @@ Before proceeding, go to the Microsoft Entra admin center, and open the app regi #### **Client ID** -Within the "Overview" you will see a GUID labeled **Application (client) ID**. Copy this GUID to the clientId field in the config. +Within the "Overview" you will see a GUID labeled **Application (client) ID**. Copy this GUID to the clientId field in the config. Click the **Authentication** link in the left nav. #### **Authority** + Check that supported account types are: **Accounts in any organizational directory (Any Microsoft Entra ID directory - Multitenant) and personal Microsoft accounts (e.g. Skype, Xbox)** If so, then set the authority attribute in the JSON configuraiton file to `https://login.microsoftonline.com/common` -For other supported account types, review the other [Authority options](https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration). Unless there is a specific need to restrict users of your app to an organization, we strongly suggest that everyone use the default authority. User restrictions can be placed later in the application flow if needed. +For other supported account types, review the other [Authority options](https://docs.microsoft.com/azure/active-directory/develop/msal-client-application-configuration). Unless there is a specific need to restrict users of your app to an organization, we strongly suggest that everyone use the default authority. User restrictions can be placed later in the application flow if needed. #### **Client Secret** If your AzureAD app registration is configured as a Confidential Client Application, you'll have to add a `clientSecret` attribute to a `.env` file and change the `PublicClientApplication` object in the sample's `index.js` file into a `ConfidentialClientApplication` object. This secret helps prevent third parties from using your app registration. + 1. Click on `Certificates and Secrets` in the left nav. 1. Click `New Client Secret` and pick an expiry. 1. Click the `Copy to Clipboard` icon, add this client secret to the `.env` file as `CLIENT_SECRET`. **silent-flow/config/customConfig.json** + ```json { - "authOptions": - { - "clientId": "YOUR_CLIENT_ID", - "authority": "YOUR_AUTHORITY", - }, - "request": - { + "authOptions": { + "clientId": "YOUR_CLIENT_ID", + "authority": "YOUR_AUTHORITY" + }, + "request": { "authCodeUrlParameters": { "scopes": ["user.read"], "redirectUri": "http://localhost:3000/redirect" @@ -63,8 +65,7 @@ This secret helps prevent third parties from using your app registration. "scopes": ["user.read"] } }, - "resourceApi": - { + "resourceApi": { "endpoint": "https://graph.microsoft.com/v1.0/me" } } @@ -79,25 +80,34 @@ CLIENT_SECRET= **silent-flow/index.js** ```javascript - // Change this - const publicClientApplication = new msal.PublicClientApplication(clientConfig); - const msalTokenCache = publicClientApplication.getTokenCache(); - return getTokenSilent(config, publicClientApplication, null, msalTokenCache); - - // To this - const confidentialClientApplication = new msal.ConfidentialClientApplication(clientConfig); - const msalTokenCache = confidentialClientApplication.getTokenCache(); - return getTokenSilent(config, confidentialClientApplication, null, msalTokenCache); +// Change this +const publicClientApplication = new msal.PublicClientApplication(clientConfig); +const msalTokenCache = publicClientApplication.getTokenCache(); +return getTokenSilent(config, publicClientApplication, null, msalTokenCache); + +// To this +const confidentialClientApplication = new msal.ConfidentialClientApplication( + clientConfig +); +const msalTokenCache = confidentialClientApplication.getTokenCache(); +return getTokenSilent( + config, + confidentialClientApplication, + null, + msalTokenCache +); ``` + 🎉You have finished the basic configuration!🎉 ### Executing the application -From the command line, let npm install any needed dependencies. This only needs to be done once. +From the command line, let npm install any needed dependencies. This only needs to be done once. ```bash $ npm install ``` + 1. Once the dependencies are installed, you can run the sample application by using the following command: ```bash @@ -108,4 +118,4 @@ $ npm start ### The User Experience -What happens if the user logs in, closes the window, returns to the site, and logs in again? Microsoft supports many, many complex scenarios with many, many forms of authentication: certificates, hardware keys, federated experiences, and even biometrics in some cases. Let our library handle the complexity of deciding the simplest way of logging in the user. +What happens if the user logs in, closes the window, returns to the site, and logs in again? Microsoft supports many, many complex scenarios with many, many forms of authentication: certificates, hardware keys, federated experiences, and even biometrics in some cases. Let our library handle the complexity of deciding the simplest way of logging in the user. diff --git a/samples/msal-node-samples/username-password-cca/README.md b/samples/msal-node-samples/username-password-cca/README.md index 2ece04a31d..efa6cc8cda 100644 --- a/samples/msal-node-samples/username-password-cca/README.md +++ b/samples/msal-node-samples/username-password-cca/README.md @@ -4,9 +4,9 @@ This sample demonstrates how to implement an MSAL Node [confidential client appl ## Prerequisites -- ROPC grant cannot be used with personal Microsoft accounts (MSA) -- ROPC grant cannot be used if the user needs to perform multi-factor authentication -- MSAL Node supports ROPC only when the authorization server is OpenID-compliant (e.g. ADFS 2019 is supported, but WS-Federation is not). +- ROPC grant cannot be used with personal Microsoft accounts (MSA) +- ROPC grant cannot be used if the user needs to perform multi-factor authentication +- MSAL Node supports ROPC only when the authorization server is OpenID-compliant (e.g. ADFS 2019 is supported, but WS-Federation is not). ## Setup @@ -21,15 +21,15 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-app`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-app`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. 1. Select **Register** to create the application. 1. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later. 1. In the app's registration screen, select the **Certificates & secrets** blade in the left. - - In the **Client secrets** section, select **New client secret**. - - Type a key description (for instance `app secret`), - - Select one of the available key durations (6 months, 12 months or Custom) as per your security posture. - - The generated key value will be displayed when you select the **Add** button. Copy and add this client secret to the `.env` file as `CLIENT_SECRET`. + - In the **Client secrets** section, select **New client secret**. + - Type a key description (for instance `app secret`), + - Select one of the available key durations (6 months, 12 months or Custom) as per your security posture. + - The generated key value will be displayed when you select the **Add** button. Copy and add this client secret to the `.env` file as `CLIENT_SECRET`. Before running the sample, you will need to replace the values in the configuration object: @@ -40,7 +40,7 @@ const msalConfig = { auth: { clientId: "ENTER_CLIENT_ID", authority: "https://login.microsoftonline.com/ENTER_TENANT_INFO", - clientSecret: process.env.CLIENT_SECRET + clientSecret: process.env.CLIENT_SECRET, }, }; ``` @@ -63,4 +63,4 @@ After that, you should see the response from Microsoft Entra ID in your terminal ## More information -- [Microsoft identity platform and OAuth 2.0 Resource Owner Password Credentials](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth-ropc) +- [Microsoft identity platform and OAuth 2.0 Resource Owner Password Credentials](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth-ropc) diff --git a/samples/msal-node-samples/username-password/README.md b/samples/msal-node-samples/username-password/README.md index 37fe342e9d..96cba88684 100644 --- a/samples/msal-node-samples/username-password/README.md +++ b/samples/msal-node-samples/username-password/README.md @@ -6,9 +6,9 @@ See this flow at work: [MSAL.js Jest/Puppeteer Testing Example](../../msal-brows ## Prerequisites -- ROPC grant cannot be used with personal Microsoft accounts -- ROPC grant cannot be used if the user needs to perform multi-factor authentication -- MSAL Node supports ROPC only when the authorization server is OpenID-compliant (e.g. ADFS 2019 is supported, but WS-Federation is not). +- ROPC grant cannot be used with personal Microsoft accounts +- ROPC grant cannot be used if the user needs to perform multi-factor authentication +- MSAL Node supports ROPC only when the authorization server is OpenID-compliant (e.g. ADFS 2019 is supported, but WS-Federation is not). ## Setup @@ -23,12 +23,12 @@ Locate the folder where `package.json` resides in your terminal. Then type: 1. Navigate to the [Microsoft Entra admin center](https://entra.microsoft.com) and select the **Microsoft Entra ID** service. 1. Select the **App Registrations** blade on the left, then select **New registration**. 1. 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-node-app`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. + - In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `msal-node-app`. + - Under **Supported account types**, select **Accounts in this organizational directory only**. 1. Select **Register** to create the application. 1. In the app's registration screen, find and note the **Application (client) ID** and **Directory (Tenant) ID**. You use these values in your app's configuration file(s) later. 1. In the app's registration screen, select the **Authentication** blade to the left. - - In the **Advanced settings** section, flip the switch for **Treat application as a public client** to **Yes**. + - In the **Advanced settings** section, flip the switch for **Treat application as a public client** to **Yes**. Before running the sample, you will need to replace the values in the configuration object: @@ -37,7 +37,7 @@ const msalConfig = { auth: { clientId: "ENTER_CLIENT_ID", authority: "https://login.microsoftonline.com/ENTER_TENANT_INFO", - } + }, }; ``` @@ -53,4 +53,4 @@ After that, you should see the response from Microsoft Entra ID in your terminal ## More information -- [Microsoft identity platform and OAuth 2.0 Resource Owner Password Credentials](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth-ropc) +- [Microsoft identity platform and OAuth 2.0 Resource Owner Password Credentials](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth-ropc)