From 4b7b713326b0060e4ba05c869a87e887b8cf3da2 Mon Sep 17 00:00:00 2001 From: salman90 Date: Fri, 2 Dec 2022 12:58:55 -0800 Subject: [PATCH 1/8] update sample to follow basher and zero trust --- .../1-call-graph/App/authConfig.js | 23 +- .../1-call-graph/App/authPopup.js | 120 +- .../1-call-graph/App/authRedirect.js | 108 +- 2-Authorization-I/1-call-graph/App/fetch.js | 63 + 2-Authorization-I/1-call-graph/App/graph.js | 33 +- 2-Authorization-I/1-call-graph/App/index.html | 40 +- .../1-call-graph/App/redirect.html | 7 + .../1-call-graph/App/signout.html | 19 + 2-Authorization-I/1-call-graph/App/styles.css | 3 + 2-Authorization-I/1-call-graph/App/ui.js | 37 +- .../1-call-graph/App/utils/storageUtils.js | 32 + .../AppCreationScripts/AppCreationScripts.md | 103 +- .../AppCreationScripts/Cleanup.ps1 | 137 +- .../AppCreationScripts/Configure.ps1 | 376 +- .../AppCreationScripts/sample.json | 52 +- .../1-call-graph/README-incremental.md | 276 +- 2-Authorization-I/1-call-graph/README.md | 385 +- .../1-call-graph/package-lock.json | 5097 ++++++++++++++++- 2-Authorization-I/1-call-graph/package.json | 1 + 2-Authorization-I/1-call-graph/server.js | 29 + 20 files changed, 6401 insertions(+), 540 deletions(-) create mode 100644 2-Authorization-I/1-call-graph/App/fetch.js create mode 100644 2-Authorization-I/1-call-graph/App/redirect.html create mode 100644 2-Authorization-I/1-call-graph/App/signout.html create mode 100644 2-Authorization-I/1-call-graph/App/styles.css create mode 100644 2-Authorization-I/1-call-graph/App/utils/storageUtils.js diff --git a/2-Authorization-I/1-call-graph/App/authConfig.js b/2-Authorization-I/1-call-graph/App/authConfig.js index 85ca99f..ebfab5f 100644 --- a/2-Authorization-I/1-call-graph/App/authConfig.js +++ b/2-Authorization-I/1-call-graph/App/authConfig.js @@ -5,12 +5,14 @@ */ const msalConfig = { auth: { - clientId: "Enter_the_Application_Id_Here", // This is the ONLY mandatory field that you need to supply. - authority: "https://login.microsoftonline.com/Enter_the_Tenant_Info_Here", // Defaults to "https://login.microsoftonline.com/common" - redirectUri: "/", // You must register this URI on Azure Portal/App Registration. Defaults to window.location.href + clientId: 'Enter_the_Application_Id_Here', // This is the ONLY mandatory field that you need to supply. + authority: 'https://login.microsoftonline.com/Enter_the_Tenant_Info_Here', // Defaults to "https://login.microsoftonline.com/common" + redirectUri: '/', // You must register this URI on Azure Portal/App Registration. Defaults to window.location.href + postLogoutRedirectUri: '/', //Indicates the page to navigate after logout. + clientCapabilities: ['CP1'], // this lets the resource owner know that this client is capable of handling claims challenge. }, cache: { - cacheLocation: "localStorage", // This configures where your cache will be stored + cacheLocation: 'localStorage', // This configures where your cache will be stored storeAuthStateInCookie: false, // Set this to "true" if you are having issues on IE11 or Edge }, }; @@ -18,13 +20,13 @@ const msalConfig = { // Add here the endpoints for MS Graph API services you would like to use. const graphConfig = { graphMeEndpoint: { - uri: "https://graph.microsoft.com/v1.0/me", - scopes: ["User.Read"] + uri: 'https://graph.microsoft.com/v1.0/me', + scopes: ['User.Read'], + }, + graphContactsEndpoint: { + uri: 'https://graph.microsoft.com/v1.0/me/contacts', + scopes: ['Contacts.Read'], }, - graphMailEndpoint: { - uri: "https://graph.microsoft.com/v1.0/me/messages", - scopes: ["Mail.Read"] - } }; /** @@ -41,5 +43,6 @@ const loginRequest = { if (typeof exports !== 'undefined') { module.exports = { msalConfig: msalConfig, + graphConfig: graphConfig }; } diff --git a/2-Authorization-I/1-call-graph/App/authPopup.js b/2-Authorization-I/1-call-graph/App/authPopup.js index db9039f..6750fa8 100644 --- a/2-Authorization-I/1-call-graph/App/authPopup.js +++ b/2-Authorization-I/1-call-graph/App/authPopup.js @@ -2,12 +2,11 @@ // configuration parameters are located at authConfig.js const myMSALObj = new msal.PublicClientApplication(msalConfig); -let username = ""; +let username = ''; function selectAccount() { - /** - * See here for more info on account retrieval: + * See here for more info on account retrieval: * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md */ @@ -17,7 +16,7 @@ function selectAccount() { return; } else if (currentAccounts.length > 1) { // Add choose account code here - console.warn("Multiple accounts detected."); + console.warn('Multiple accounts detected.'); } else if (currentAccounts.length === 1) { username = currentAccounts[0].username; showWelcomeMessage(username); @@ -40,57 +39,126 @@ function handleResponse(response) { } function signIn() { - + /** * You can pass a custom request object below. This will override the initial configuration. For more information, visit: * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request */ - myMSALObj.loginPopup(loginRequest) + myMSALObj + .loginPopup(loginRequest) .then(handleResponse) - .catch(error => { + .catch((error) => { console.error(error); }); } function signOut() { - /** * You can pass a custom request object below. This will override the initial configuration. For more information, visit: * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request */ + const account = myMSALObj.getAccountByUsername(username); const logoutRequest = { - account: myMSALObj.getAccountByUsername(username) + account: account, + redirectUri: '/redirect', + mainWindowRedirectUri: '/signout', }; - - myMSALObj.logout(logoutRequest); + clearStorage(account); + myMSALObj.logoutPopup(logoutRequest).catch((error) => { + console.log(error); + }); } function seeProfile() { - + const account = myMSALObj.getAccountByUsername(username); getGraphClient({ - account: myMSALObj.getAccountByUsername(username), + account: account, scopes: graphConfig.graphMeEndpoint.scopes, - interactionType: msal.InteractionType.Popup - }).api('/me').get() + interactionType: msal.InteractionType.Popup, + }) + .api('/me') + .responseType('raw') + .get() + .then((response) => { + return handleClaimsChallenge(account, response, graphConfig.graphMeEndpoint.uri); + }) .then((response) => { + if (response && response.error === 'claims_challenge_occurred') throw response.error; return updateUI(response, graphConfig.graphMeEndpoint.uri); - }).catch((error) => { - console.log(error); + }) + .catch((error) => { + if (error === 'claims_challenge_occurred') { + const resource = new URL(graphConfig.graphMeEndpoint.uri).hostname; + const claims = + account && + getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}`) + ? window.atob( + getClaimsFromStorage( + `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}` + ) + ) + : undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}} + let request = { + account: account, + scopes: graphConfig.graphMeEndpoint.scopes, + claims: claims, + redirectUri: '/redirect', + }; + + myMSALObj.acquireTokenPopup(request).catch((error) => { + console.log(error); + }); + } else { + console.log(error) + } }); } -function readMail() { - +function readContacts() { + const account = myMSALObj.getAccountByUsername(username); getGraphClient({ - account: myMSALObj.getAccountByUsername(username), - scopes: graphConfig.graphMailEndpoint.scopes, - interactionType: msal.InteractionType.Popup - }).api('/me/messages').get() + account: account, + scopes: graphConfig.graphContactsEndpoint.scopes, + interactionType: msal.InteractionType.Popup, + }) + .api('/me/contacts') + .responseType('raw') + .get() + .then((response) => { + return handleClaimsChallenge(account, response, graphConfig.graphContactsEndpoint.uri); + }) .then((response) => { - return updateUI(response, graphConfig.graphMailEndpoint.uri); - }).catch((error) => { - console.log(error); + if (response && response.error === 'claims_challenge_occurred') throw response.error; + return updateUI(response, graphConfig.graphContactsEndpoint.uri); + }) + .catch((error) => { + if (error === 'claims_challenge_occurred') { + const resource = new URL(graphConfig.graphContactsEndpoint.uri).hostname; + const claims = + account && + getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}`) + ? window.atob( + getClaimsFromStorage( + `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}` + ) + ) + : undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}} + let request = { + account: account, + scopes: graphConfig.graphContactsEndpoint.scopes, + claims: claims, + redirectUri: '/redirect', + }; + + myMSALObj.acquireTokenPopup(request).catch((error) => { + console.log(error); + }); + } else if (error.toString().includes('404')) { + return updateUI(null, graphConfig.graphContactsEndpoint.uri); + } else { + console.log(error); + } }); } diff --git a/2-Authorization-I/1-call-graph/App/authRedirect.js b/2-Authorization-I/1-call-graph/App/authRedirect.js index cba0234..233a930 100644 --- a/2-Authorization-I/1-call-graph/App/authRedirect.js +++ b/2-Authorization-I/1-call-graph/App/authRedirect.js @@ -2,23 +2,23 @@ // configuration parameters are located at authConfig.js const myMSALObj = new msal.PublicClientApplication(msalConfig); -let username = ""; +let username = ''; /** * A promise handler needs to be registered for handling the * response returned from redirect flow. For more information, visit: * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/acquire-token.md */ -myMSALObj.handleRedirectPromise() +myMSALObj + .handleRedirectPromise() .then(handleResponse) .catch((error) => { console.error(error); }); -function selectAccount () { - +function selectAccount() { /** - * See here for more info on account retrieval: + * See here for more info on account retrieval: * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-common/docs/Accounts.md */ @@ -28,7 +28,7 @@ function selectAccount () { return; } else if (currentAccounts.length > 1) { // Add your account choosing logic here - console.warn("Multiple accounts detected."); + console.warn('Multiple accounts detected.'); } else if (currentAccounts.length === 1) { username = currentAccounts[0].username; showWelcomeMessage(username); @@ -55,42 +55,106 @@ function signIn() { } function signOut() { - + /** * You can pass a custom request object below. This will override the initial configuration. For more information, visit: * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#request */ // Choose which account to logout from by passing a username. + const account = myMSALObj.getAccountByUsername(username); const logoutRequest = { - account: myMSALObj.getAccountByUsername(username) + account: account, }; - myMSALObj.logout(logoutRequest); + clearStorage(account); + myMSALObj.logoutRedirect(logoutRequest); } function seeProfile() { + const account = myMSALObj.getAccountByUsername(username); + getGraphClient({ - account: myMSALObj.getAccountByUsername(username), + account: account, scopes: graphConfig.graphMeEndpoint.scopes, - interactionType: msal.InteractionType.Redirect - }).api('/me').get() + interactionType: msal.InteractionType.Redirect, + }) + .api('/me') + .responseType('raw') + .get() .then((response) => { + return handleClaimsChallenge(account, response, graphConfig.graphMeEndpoint.uri); + }) + .then((response) => { + if (response && response.error === 'claims_challenge_occurred') throw response.error; return updateUI(response, graphConfig.graphMeEndpoint.uri); - }).catch((error) => { - console.log(error); + }) + .catch((error) => { + if (error === 'claims_challenge_occurred') { + const resource = new URL(graphConfig.graphMeEndpoint.uri).hostname; + const claims = + account && + getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}`) + ? window.atob( + getClaimsFromStorage( + `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}` + ) + ) + : undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}} + let request = { + account: account, + scopes: graphConfig.graphMeEndpoint.scopes, + claims: claims, + }; + + myMSALObj.acquireTokenRedirect(request); + } else { + console.log(error); + } }); } -function readMail() { +function readContacts() { + const account = myMSALObj.getAccountByUsername(username); getGraphClient({ - account: myMSALObj.getAccountByUsername(username), - scopes: graphConfig.graphMailEndpoint.scopes, - interactionType: msal.InteractionType.Redirect - }).api('/me/messages').get() + account: account, + scopes: graphConfig.graphContactsEndpoint.scopes, + interactionType: msal.InteractionType.Redirect, + }) + .api('/me/contacts') + .responseType('raw') + .get() + .then((response) => { + return handleClaimsChallenge(account, response, graphConfig.graphContactsEndpoint.uri); + }) .then((response) => { - return updateUI(response, graphConfig.graphMailEndpoint.uri); - }).catch((error) => { - console.log(error); + if (response && response.error === 'claims_challenge_occurred') throw response.error; + return updateUI(response, graphConfig.graphContactsEndpoint.uri); + }) + .catch((error) => { + if (error === 'claims_challenge_occurred') { + const resource = new URL(graphConfig.graphContactsEndpoint.uri).hostname; + const claims = + account && + getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}`) + ? window.atob( + getClaimsFromStorage( + `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}` + ) + ) + : undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}} + + let request = { + account: account, + scopes: graphConfig.graphContactsEndpoint.scopes, + claims: claims, + }; + + myMSALObj.acquireTokenRedirect(request); + } else if (error.toString().includes('404')) { + return updateUI(null, graphConfig.graphContactsEndpoint.uri); + } else { + console.log(error); + } }); } diff --git a/2-Authorization-I/1-call-graph/App/fetch.js b/2-Authorization-I/1-call-graph/App/fetch.js new file mode 100644 index 0000000..dd53592 --- /dev/null +++ b/2-Authorization-I/1-call-graph/App/fetch.js @@ -0,0 +1,63 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * This method inspects the HTTPS response from a fetch call for the "www-authenticate header" + * If present, it grabs the claims challenge from the header and store it in the localStorage + * For more information, visit: https://docs.microsoft.com/en-us/azure/active-directory/develop/claims-challenge#claims-challenge-header-format + * @param {object} response + * @returns response + */ +const handleClaimsChallenge = async (account,response, apiEndpoint) => { + if (response.status === 200) { + return response.json(); + } else if (response.status === 401) { + if (response.headers.get('www-authenticate')) { + const authenticateHeader = response.headers.get('www-authenticate'); + const claimsChallenge = parseChallenges(authenticateHeader); + /** + * This method stores the claim challenge to the session storage in the browser to be used when acquiring a token. + * To ensure that we are fetching the correct claim from the storage, we are using the clientId + * of the application and oid (user’s object id) as the key identifier of the claim with schema + * cc... + */ + addClaimsToStorage( + claimsChallenge.claims, + `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${new URL(apiEndpoint).hostname}` + ); + return { error: 'claims_challenge_occurred', payload: claimsChallenge.claims }; + } + + throw new Error(`Unauthorized: ${response.status}`); + } else { + throw new Error(`Something went wrong with the request: ${response.status}`); + } +}; + +/** +* This method parses WWW-Authenticate authentication headers +* @param header +* @return {Object} challengeMap +*/ +const parseChallenges = (header) => { + const schemeSeparator = header.indexOf(' '); + const challenges = header.substring(schemeSeparator + 1).split(','); + const challengeMap = {}; + + challenges.forEach((challenge) => { + const [key, value] = challenge.split('='); + challengeMap[key.trim()] = window.decodeURI(value.replace(/['"]+/g, '')); + }); + + return challengeMap; +} + + +// exporting config object for jest +if (typeof exports !== 'undefined') { + module.exports = { + handleClaimsChallenge: handleClaimsChallenge, + }; +} \ No newline at end of file diff --git a/2-Authorization-I/1-call-graph/App/graph.js b/2-Authorization-I/1-call-graph/App/graph.js index 863ae59..0386cb6 100644 --- a/2-Authorization-I/1-call-graph/App/graph.js +++ b/2-Authorization-I/1-call-graph/App/graph.js @@ -1,7 +1,7 @@ /** - * The code below demonstrates how you can use MSAL as a custom authentication provider for the Microsoft Graph JavaScript SDK. - * You do NOT need to implement a custom provider. Microsoft Graph JavaScript SDK v3.0 (preview) offers AuthCodeMSALBrowserAuthenticationProvider - * which handles token acquisition and renewal for you automatically. For more information on how to use it, visit: + * The code below demonstrates how you can use MSAL as a custom authentication provider for the Microsoft Graph JavaScript SDK. + * You do NOT need to implement a custom provider. Microsoft Graph JavaScript SDK v3.0 (preview) offers AuthCodeMSALBrowserAuthenticationProvider + * which handles token acquisition and renewal for you automatically. For more information on how to use it, visit: * https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/AuthCodeMSALBrowserAuthenticationProvider.md */ @@ -22,22 +22,30 @@ const getGraphClient = (providerOptions) => { const graphClient = MicrosoftGraph.Client.initWithMiddleware(clientOptions); return graphClient; -} +}; /** * This class implements the IAuthenticationProvider interface, which allows a custom authentication provider to be * used with the Graph client. See: https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/src/IAuthenticationProvider.ts */ class MsalAuthenticationProvider { - + account; // user account object to be used when attempting silent token acquisition scopes; // array of scopes required for this resource endpoint interactionType; // type of interaction to fallback to when silent token acquisition fails + claims; constructor(providerOptions) { this.account = providerOptions.account; this.scopes = providerOptions.scopes; this.interactionType = providerOptions.interactionType; + const resource = new URL(graphConfig.graphMeEndpoint.uri).hostname; + this.claims = + this.account && getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${this.account.idTokenClaims.oid}.${resource}`) + ? window.atob( + getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${this.account.idTokenClaims.oid}.${resource}`) + ) + : undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}} } /** @@ -52,7 +60,8 @@ class MsalAuthenticationProvider { try { response = await myMSALObj.acquireTokenSilent({ account: this.account, - scopes: this.scopes + scopes: this.scopes, + claims: this.claims }); if (response.accessToken) { @@ -65,9 +74,10 @@ class MsalAuthenticationProvider { if (error instanceof msal.InteractionRequiredAuthError) { switch (this.interactionType) { case msal.InteractionType.Popup: - response = await myMSALObj.acquireTokenPopup({ - scopes: this.scopes + scopes: this.scopes, + claims: this.claims, + redirectUri: '/redirect', }); if (response.accessToken) { @@ -80,11 +90,12 @@ class MsalAuthenticationProvider { case msal.InteractionType.Redirect: /** * This will cause the app to leave the current page and redirect to the consent screen. - * Once consent is provided, the app will return back to the current page and then the - * silent token acquisition will succeed. + * Once consent is provided, the app will return back to the current page and then the + * silent token acquisition will succeed. */ myMSALObj.acquireTokenRedirect({ - scopes: this.scopes + scopes: this.scopes, + claims: this.claims, }); break; diff --git a/2-Authorization-I/1-call-graph/App/index.html b/2-Authorization-I/1-call-graph/App/index.html index 98d73e2..8dd166e 100644 --- a/2-Authorization-I/1-call-graph/App/index.html +++ b/2-Authorization-I/1-call-graph/App/index.html @@ -6,34 +6,34 @@ Microsoft identity platform + - - - - + + - - + + + -