diff --git a/2-Authorization-I/1-call-graph/App/authConfig.js b/2-Authorization-I/1-call-graph/App/authConfig.js index 85ca99f..3d7d439 100644 --- a/2-Authorization-I/1-call-graph/App/authConfig.js +++ b/2-Authorization-I/1-call-graph/App/authConfig.js @@ -5,26 +5,57 @@ */ 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_Id_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 }, + system: { + /** + * Below you can configure MSAL.js logs. For more information, visit: + * https://docs.microsoft.com/azure/active-directory/develop/msal-logging-js + */ + loggerOptions: { + loggerCallback: (level, message, containsPii) => { + if (containsPii) { + return; + } + switch (level) { + case msal.LogLevel.Error: + console.error(message); + return; + case msal.LogLevel.Info: + console.info(message); + return; + case msal.LogLevel.Verbose: + console.debug(message); + return; + case msal.LogLevel.Warning: + console.warn(message); + return; + default: + return; + } + }, + }, + }, }; // 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 +72,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..eadb916 100644 --- a/2-Authorization-I/1-call-graph/App/authPopup.js +++ b/2-Authorization-I/1-call-graph/App/authPopup.js @@ -2,96 +2,152 @@ // configuration parameters are located at authConfig.js const myMSALObj = new msal.PublicClientApplication(msalConfig); -let username = ""; +let username = ''; + +/** + * This method adds an event callback function to the MSAL object + * to handle the response from redirect flow. For more information, visit: + * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/events.md + */ +myMSALObj.addEventCallback((event) => { + if ( + (event.eventType === msal.EventType.LOGIN_SUCCESS || + event.eventType === msal.EventType.ACQUIRE_TOKEN_SUCCESS) && + event.payload.account + ) { + const account = event.payload.account; + myMSALObj.setActiveAccount(account); + } -function selectAccount() { + if (event.eventType === msal.EventType.LOGOUT_SUCCESS) { + if (myMSALObj.getAllAccounts().length > 0) { + myMSALObj.setActiveAccount(myMSALObj.getAllAccounts()[0]); + } + } +}); +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 */ - const currentAccounts = myMSALObj.getAllAccounts(); - if (currentAccounts === null) { return; - } else if (currentAccounts.length > 1) { + } else if (currentAccounts.length >= 1) { // Add choose account code here - console.warn("Multiple accounts detected."); - } else if (currentAccounts.length === 1) { - username = currentAccounts[0].username; - showWelcomeMessage(username); + username = myMSALObj.getActiveAccount().username; + showWelcomeMessage(username, currentAccounts); } } -function handleResponse(response) { +async function addAnotherAccount(event) { + if (event.target.innerHTML.includes('@')) { + const username = event.target.innerHTML; + const account = myMSALObj.getAllAccounts().find((account) => account.username === username); + const activeAccount = myMSALObj.getActiveAccount(); + if (account && activeAccount.homeAccountId != account.homeAccountId) { + try { + myMSALObj.setActiveAccount(account); + let res = await myMSALObj.ssoSilent({ + ...loginRequest, + account: account, + }); + closeModal(); + handleResponse(res); + window.location.reload(); + } catch (error) { + if (error instanceof msal.InteractionRequiredAuthError) { + let res = await myMSALObj.loginPopup({ + ...loginRequest, + prompt: 'login', + }); + handleResponse(res); + window.location.reload(); + } + } + } else { + closeModal(); + } + } else { + try { + myMSALObj.setActiveAccount(null); + const res = await myMSALObj.loginPopup({ + ...loginRequest, + prompt: 'login', + }); + handleResponse(res); + closeModal(); + window.location.reload(); + } catch (error) { + console.log(error); + } + } +} +function handleResponse(response) { /** * To see the full list of response object properties, visit: * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/request-response-object.md#response */ if (response !== null) { + const accounts = myMSALObj.getAllAccounts(); username = response.account.username; - showWelcomeMessage(username); + showWelcomeMessage(username, accounts); } else { selectAccount(); } } 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, + mainWindowRedirectUri: '/', }; - - myMSALObj.logout(logoutRequest); + clearStorage(account); + myMSALObj.logoutPopup(logoutRequest).catch((error) => { + console.log(error); + }); } function seeProfile() { - - getGraphClient({ - account: myMSALObj.getAccountByUsername(username), - scopes: graphConfig.graphMeEndpoint.scopes, - interactionType: msal.InteractionType.Popup - }).api('/me').get() - .then((response) => { - return updateUI(response, graphConfig.graphMeEndpoint.uri); - }).catch((error) => { - console.log(error); - }); + callGraph( + username, + graphConfig.graphMeEndpoint.scopes, + graphConfig.graphMeEndpoint.uri, + msal.InteractionType.Popup, + myMSALObj + ); } -function readMail() { - - getGraphClient({ - account: myMSALObj.getAccountByUsername(username), - scopes: graphConfig.graphMailEndpoint.scopes, - interactionType: msal.InteractionType.Popup - }).api('/me/messages').get() - .then((response) => { - return updateUI(response, graphConfig.graphMailEndpoint.uri); - }).catch((error) => { - console.log(error); - }); +function readContacts() { + callGraph( + username, + graphConfig.graphContactsEndpoint.scopes, + graphConfig.graphContactsEndpoint.uri, + msal.InteractionType.Popup, + myMSALObj + ); } selectAccount(); diff --git a/2-Authorization-I/1-call-graph/App/authRedirect.js b/2-Authorization-I/1-call-graph/App/authRedirect.js index cba0234..79ce8de 100644 --- a/2-Authorization-I/1-call-graph/App/authRedirect.js +++ b/2-Authorization-I/1-call-graph/App/authRedirect.js @@ -2,23 +2,45 @@ // configuration parameters are located at authConfig.js const myMSALObj = new msal.PublicClientApplication(msalConfig); -let username = ""; +let username = ''; + +/** + * This method adds an event callback function to the MSAL object + * to handle the response from redirect flow. For more information, visit: + * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/events.md + */ +myMSALObj.addEventCallback((event) => { + if ( + (event.eventType === msal.EventType.LOGIN_SUCCESS || + event.eventType === msal.EventType.ACQUIRE_TOKEN_SUCCESS) && + event.payload.account + ) { + const account = event.payload.account; + myMSALObj.setActiveAccount(account); + } + + if (event.eventType === msal.EventType.LOGOUT_SUCCESS) { + if (myMSALObj.getAllAccounts().length > 0) { + myMSALObj.setActiveAccount(myMSALObj.getAllAccounts()[0]); + } + } +}); /** * 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 */ @@ -26,19 +48,57 @@ function selectAccount () { if (!currentAccounts) { return; - } else if (currentAccounts.length > 1) { + } else if (currentAccounts.length >= 1) { // Add your account choosing logic here - console.warn("Multiple accounts detected."); - } else if (currentAccounts.length === 1) { - username = currentAccounts[0].username; - showWelcomeMessage(username); + username = myMSALObj.getActiveAccount().username; + showWelcomeMessage(username, currentAccounts); + } +} + +async function addAnotherAccount(event) { + if (event.target.innerHTML.includes("@")) { + const username = event.target.innerHTML; + const account = myMSALObj.getAllAccounts().find((account) => account.username === username); + const activeAccount = myMSALObj.getActiveAccount(); + if (account && activeAccount.homeAccountId != account.homeAccountId) { + try { + myMSALObj.setActiveAccount(account); + let res = await myMSALObj.ssoSilent({ + ...loginRequest, + account: account, + }); + handleResponse(res); + closeModal(); + window.location.reload(); + } catch (error) { + if (error instanceof msal.InteractionRequiredAuthError) { + await myMSALObj.loginRedirect({ + ...loginRequest, + prompt: 'login', + }); + } + } + } else { + closeModal(); + } + } else { + try { + myMSALObj.setActiveAccount(null); + await myMSALObj.loginRedirect({ + ...loginRequest, + prompt: 'login', + }); + } catch (error) { + console.log(error); + } } } function handleResponse(response) { if (response !== null) { + const accounts = myMSALObj.getAllAccounts(); username = response.account.username; - showWelcomeMessage(username); + showWelcomeMessage(username, accounts); } else { selectAccount(); } @@ -62,35 +122,32 @@ function signOut() { */ // Choose which account to logout from by passing a username. + const account = myMSALObj.getAccountByUsername(username); const logoutRequest = { - account: myMSALObj.getAccountByUsername(username) + account: account, + loginHint: account.idTokenClaims.login_hint, }; - myMSALObj.logout(logoutRequest); + clearStorage(account); + myMSALObj.logoutRedirect(logoutRequest); } function seeProfile() { - getGraphClient({ - account: myMSALObj.getAccountByUsername(username), - scopes: graphConfig.graphMeEndpoint.scopes, - interactionType: msal.InteractionType.Redirect - }).api('/me').get() - .then((response) => { - return updateUI(response, graphConfig.graphMeEndpoint.uri); - }).catch((error) => { - console.log(error); - }); + callGraph( + username, + graphConfig.graphMeEndpoint.scopes, + graphConfig.graphMeEndpoint.uri, + msal.InteractionType.Redirect, + myMSALObj + ); } -function readMail() { - getGraphClient({ - account: myMSALObj.getAccountByUsername(username), - scopes: graphConfig.graphMailEndpoint.scopes, - interactionType: msal.InteractionType.Redirect - }).api('/me/messages').get() - .then((response) => { - return updateUI(response, graphConfig.graphMailEndpoint.uri); - }).catch((error) => { - console.log(error); - }); +function readContacts() { + callGraph( + username, + graphConfig.graphContactsEndpoint.scopes, + graphConfig.graphContactsEndpoint.uri, + msal.InteractionType.Redirect, + myMSALObj + ); } 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..d498687 --- /dev/null +++ b/2-Authorization-I/1-call-graph/App/fetch.js @@ -0,0 +1,119 @@ +/* + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. + */ + +/** + * This method calls the Graph API by utilizing the graph client instance. + * @param {String} username + * @param {Array} scopes + * @param {String} uri + * @param {String} interactionType + * @param {Object} myMSALObj + * @returns + */ +const callGraph = async (username, scopes, uri, interactionType, myMSALObj) => { + const account = myMSALObj.getAccountByUsername(username); + try { + let response = await getGraphClient({ + account: account, + scopes: scopes, + interactionType: interactionType, + }) + .api(uri) + .responseType('raw') + .get(); + + response = await handleClaimsChallenge(account, response, uri); + if (response && response.error === 'claims_challenge_occurred') throw response.error; + updateUI(response, uri); + } catch (error) { + if (error === 'claims_challenge_occurred') { + const resource = new URL(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: scopes, + claims: claims, + }; + switch (interactionType) { + case msal.InteractionType.Popup: + + await myMSALObj.acquireTokenPopup({ + ...request, + redirectUri: '/redirect', + }); + break; + case msal.InteractionType.Redirect: + await myMSALObj.acquireTokenRedirect(request); + break; + default: + await myMSALObj.acquireTokenRedirect(request); + break; + } + } else if (error.toString().includes('404')) { + return updateUI(null, uri); + } else { + console.log(error); + } + } +} + +/** + * 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; +} \ 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..3ed7982 100644 --- a/2-Authorization-I/1-call-graph/App/index.html +++ b/2-Authorization-I/1-call-graph/App/index.html @@ -1,93 +1,143 @@ + + + + Microsoft identity platform + + - - - - Microsoft identity platform - + + - - + + - - + + - - + + - - - - + + +
+
Vanilla JavaScript SPA calling MS Graph API with MSAL.js
+
+
+ +
+
+
+
+
+
+ +
+
+
+
- - -
-
Vanilla JavaScript SPA calling MS Graph API with MSAL.js
-
-
- -
-
- - - - + + + + - - - + - - - - - + + + - \ No newline at end of file + + + + + + + + diff --git a/2-Authorization-I/1-call-graph/App/redirect.html b/2-Authorization-I/1-call-graph/App/redirect.html new file mode 100644 index 0000000..dcc8b0e --- /dev/null +++ b/2-Authorization-I/1-call-graph/App/redirect.html @@ -0,0 +1,6 @@ + \ No newline at end of file diff --git a/2-Authorization-I/1-call-graph/App/styles.css b/2-Authorization-I/1-call-graph/App/styles.css new file mode 100644 index 0000000..166ab61 --- /dev/null +++ b/2-Authorization-I/1-call-graph/App/styles.css @@ -0,0 +1,13 @@ +.navbarStyle { + padding: .5rem 1rem !important; +} + +.dropdown-toggle { + visibility: hidden; + display: none !important; +} + +.list-group-item { + cursor: pointer; +} + diff --git a/2-Authorization-I/1-call-graph/App/ui.js b/2-Authorization-I/1-call-graph/App/ui.js index 9021a2a..7f99f10 100644 --- a/2-Authorization-I/1-call-graph/App/ui.js +++ b/2-Authorization-I/1-call-graph/App/ui.js @@ -1,18 +1,46 @@ // Select DOM elements to work with const welcomeDiv = document.getElementById("WelcomeMessage"); const signInButton = document.getElementById("SignIn"); +const dropdownButton = document.getElementById('dropdownMenuButton1'); const cardDiv = document.getElementById("card-div"); const mailButton = document.getElementById("readMail"); const profileButton = document.getElementById("seeProfile"); const profileDiv = document.getElementById("profile-div"); +const listGroup = document.getElementById('list-group'); -function showWelcomeMessage(username) { +function showWelcomeMessage(username, accounts) { // Reconfiguring DOM elements cardDiv.style.display = 'initial'; + signInButton.style.visibility = 'hidden'; welcomeDiv.innerHTML = `Welcome ${username}`; - signInButton.setAttribute("onclick", "signOut();"); - signInButton.setAttribute('class', "btn btn-success") - signInButton.innerHTML = "Sign Out"; + dropdownButton.setAttribute('style', 'display:inline !important; visibility:visible'); + dropdownButton.innerHTML = username; + accounts.forEach(account => { + let item = document.getElementById(account.username); + if (!item) { + const listItem = document.createElement('li'); + listItem.setAttribute('onclick', 'addAnotherAccount(event)'); + listItem.setAttribute('id', account.username); + listItem.innerHTML = account.username; + if (account.username === username) { + listItem.setAttribute('class', 'list-group-item active'); + } else { + listItem.setAttribute('class', 'list-group-item'); + } + listGroup.appendChild(listItem); + } else { + if (account.username === username) { + item.setAttribute('class', 'list-group-item active'); + } else { + item.setAttribute('active', 'list-group-item'); + } + } + }); +} + +function closeModal() { + const element = document.getElementById("closeModal"); + element.click(); } function updateUI(data, endpoint) { @@ -33,34 +61,25 @@ function updateUI(data, endpoint) { profileDiv.appendChild(phone); profileDiv.appendChild(address); - } else if (endpoint === graphConfig.graphMailEndpoint.uri) { - if (data.value.length < 1) { - alert("Your mailbox is empty!") + } else if (endpoint === graphConfig.graphContactsEndpoint.uri) { + if (!data || data.value.length < 1) { + alert('Your contacts is empty!'); } else { - const tabContent = document.getElementById("nav-tabContent"); - const tabList = document.getElementById("list-tab"); + const tabList = document.getElementById('list-tab'); tabList.innerHTML = ''; // clear tabList at each readMail call data.value.map((d, i) => { - // Keeping it simple if (i < 10) { - const listItem = document.createElement("a"); - listItem.setAttribute("class", "list-group-item list-group-item-action") - listItem.setAttribute("id", "list" + i + "list") - listItem.setAttribute("data-toggle", "list") - listItem.setAttribute("href", "#list" + i) - listItem.setAttribute("role", "tab") - listItem.setAttribute("aria-controls", i) - listItem.innerHTML = d.subject; - tabList.appendChild(listItem) - - const contentItem = document.createElement("div"); - contentItem.setAttribute("class", "tab-pane fade") - contentItem.setAttribute("id", "list" + i) - contentItem.setAttribute("role", "tabpanel") - contentItem.setAttribute("aria-labelledby", "list" + i + "list") - contentItem.innerHTML = " from: " + d.from.emailAddress.address + "

" + d.bodyPreview + "..."; - tabContent.appendChild(contentItem); + const listItem = document.createElement('a'); + listItem.setAttribute('class', 'list-group-item list-group-item-action'); + listItem.setAttribute('id', 'list' + i + 'list'); + listItem.setAttribute('data-toggle', 'list'); + listItem.setAttribute('href', '#list' + i); + listItem.setAttribute('role', 'tab'); + listItem.setAttribute('aria-controls', i); + listItem.innerHTML = + ' Name: ' + d.displayName + '

' + 'Note: ' + d.personalNotes + '...'; + tabList.appendChild(listItem); } }); } diff --git a/2-Authorization-I/1-call-graph/App/utils/storageUtils.js b/2-Authorization-I/1-call-graph/App/utils/storageUtils.js new file mode 100644 index 0000000..fc9184d --- /dev/null +++ b/2-Authorization-I/1-call-graph/App/utils/storageUtils.js @@ -0,0 +1,27 @@ +/** + * This method stores the claim challenge to the localStorage in the browser to be used when acquiring a token + * @param {String} claimsChallenge + */ +const addClaimsToStorage = (claimsChallenge, claimsChallengeId) => { + sessionStorage.setItem(claimsChallengeId, claimsChallenge); +}; + +/** + * This method retrieves the claims challenge from the localStorage + * @param {string} claimsChallengeId + * @returns + */ +const getClaimsFromStorage = (claimsChallengeId) => { + return sessionStorage.getItem(claimsChallengeId); +}; + +/** + * This method clears localStorage of any claims challenge entry + * @param {Object} account + */ +const clearStorage = (account) => { + for (var key in sessionStorage) { + if (key.startsWith(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}`)) + sessionStorage.removeItem(key); + } +}; diff --git a/2-Authorization-I/1-call-graph/AppCreationScripts/AppCreationScripts.md b/2-Authorization-I/1-call-graph/AppCreationScripts/AppCreationScripts.md index da7ede5..6e062df 100644 --- a/2-Authorization-I/1-call-graph/AppCreationScripts/AppCreationScripts.md +++ b/2-Authorization-I/1-call-graph/AppCreationScripts/AppCreationScripts.md @@ -1,45 +1,35 @@ -# Registering the sample apps with the Microsoft identity platform and updating the configuration files using PowerShell +# Registering sample apps with the Microsoft identity platform and updating configuration files using PowerShell ## Overview ### Quick summary -1. On Windows run PowerShell as **Administrator** and navigate to the root of the cloned directory +1. On Windows, run PowerShell as **Administrator** and navigate to the root of the cloned directory 1. In PowerShell run: ```PowerShell Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force ``` -1. Run the script to create your Azure AD application and configure the code of the sample application accordingly. (Other ways of running the scripts are described below) +1. Run the script to create your Azure AD application and configure the code of the sample application accordingly. ```PowerShell cd .\AppCreationScripts\ - .\Configure.ps1 + .\Configure.ps1 -TenantId "your test tenant's id" -AzureEnvironmentName "[Optional] - Azure environment, defaults to 'Global'" ``` -1. Open the Visual Studio solution and click start - ### More details -The following paragraphs: - -- [Registering the sample apps with the Microsoft identity platform and updating the configuration files using PowerShell](#registering-the-sample-apps-with-the-Microsoft-identity-platform-and-updating-the-configuration-files-using-PowerShell) - - [Overview](#overview) - - [Quick summary](#quick-summary) - - [More details](#more-details) - - [Goal of the provided scripts](#goal-of-the-provided-scripts) - - [Presentation of the scripts](#presentation-of-the-scripts) - - [Usage pattern for tests and DevOps scenarios](#usage-pattern-for-tests-and-DevOps-scenarios) - - [How to use the app creation scripts?](#how-to-use-the-app-creation-scripts) - - [Pre-requisites](#pre-requisites) - - [Run the script and start running](#run-the-script-and-start-running) - - [Four ways to run the script](#four-ways-to-run-the-script) - - [Option 1 (interactive)](#option-1-interactive) - - [Option 2 (non-interactive)](#option-2-non-interactive) - - [Option 3 (Interactive, but create apps in a specified tenant)](#option-3-Interactive-but-create-apps-in-a-specified-tenant) - - [Option 4 (non-interactive, and create apps in a specified tenant)](#option-4-non-interactive-and-create-apps-in-a-specified-tenant) - - [Running the script on Azure Sovereign clouds](#running-the-script-on-Azure-Sovereign-clouds) +- [Goal of the provided scripts](#goal-of-the-provided-scripts) + - [Presentation of the scripts](#presentation-of-the-scripts) + - [Usage pattern for tests and DevOps scenarios](#usage-pattern-for-tests-and-DevOps-scenarios) +- [How to use the app creation scripts?](#how-to-use-the-app-creation-scripts) + - [Pre-requisites](#pre-requisites) + - [Run the script and start running](#run-the-script-and-start-running) + - [Four ways to run the script](#four-ways-to-run-the-script) + - [Option 1 (interactive)](#option-1-interactive) + - [Option 2 (Interactive, but create apps in a specified tenant)](#option-3-Interactive-but-create-apps-in-a-specified-tenant) + - [Running the script on Azure Sovereign clouds](#running-the-script-on-Azure-Sovereign-clouds) ## Goal of the provided scripts @@ -50,14 +40,14 @@ This sample comes with two PowerShell scripts, which automate the creation of th These scripts are: - `Configure.ps1` which: - - creates Azure AD applications and their related objects (permissions, dependencies, secrets), - - changes the configuration files in the C# and JavaScript projects. + - creates Azure AD applications and their related objects (permissions, dependencies, secrets, app roles), + - changes the configuration files in the sample projects. - creates a summary file named `createdApps.html` in the folder from which you ran the script, and containing, for each Azure AD application it created: - the identifier of the application - the AppId of the application - the url of its registration in the [Azure portal](https://portal.azure.com). -- `Cleanup.ps1` which cleans-up the Azure AD objects created by `Configure.ps1`. Note that this script does not revert the changes done in the configuration files, though. You will need to undo the change from source control (from Visual Studio, or from the command line using, for instance, git reset). +- `Cleanup.ps1` which cleans-up the Azure AD objects created by `Configure.ps1`. Note that this script does not revert the changes done in the configuration files, though. You will need to undo the change from source control (from Visual Studio, or from the command line using, for instance, `git reset`). ### Usage pattern for tests and DevOps scenarios @@ -75,22 +65,23 @@ The `Configure.ps1` will stop if it tries to create an Azure AD application whic Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process ``` -1. ### (Optionally) install AzureAD PowerShell modules +### (Optionally) install Microsoft.Graph.Applications PowerShell modules + +The scripts install the required PowerShell module (Microsoft.Graph.Applications) for the current user if needed. However, if you want to install if for all users on the machine, you can follow the following steps: -The scripts install the required PowerShell module (AzureAD) for the current user if needed. However, if you want to install if for all users on the machine, you can follow the following steps: +1. If you have never done it already, in the PowerShell window, install the Microsoft.Graph.Applications PowerShell modules. For this: -1. If you have never done it already, in the PowerShell window, install the AzureAD PowerShell modules. For this: - 1. Open PowerShell as admin (On Windows, Search Powershell in the search bar, right click on it and select Run as administrator). + 1. Open PowerShell as admin (On Windows, Search Powershell in the search bar, right click on it and select **Run as administrator**). 2. Type: ```PowerShell - Install-Module AzureAD + Install-Module Microsoft.Graph.Applications ``` or if you cannot be administrator on your machine, run: ```PowerShell - Install-Module AzureAD -Scope CurrentUser + Install-Module Microsoft.Graph.Applications -Scope CurrentUser ``` ### Run the script and start running @@ -105,44 +96,29 @@ The scripts install the required PowerShell module (AzureAD) for the current use 1. Open the Visual Studio solution, and in the solution's context menu, choose **Set Startup Projects**. 1. select **Start** for the projects -You're done. this just works! +You're done! -### Four ways to run the script +### Two ways to run the script We advise four ways of running the script: - Interactive: you will be prompted for credentials, and the scripts decide in which tenant to create the objects, -- non-interactive: you will provide credentials, and the scripts decide in which tenant to create the objects, -- Interactive in specific tenant: you will provide the tenant in which you want to create the objects and then you will be prompted for credentials, and the scripts will create the objects, -- non-interactive in specific tenant: you will provide tenant in which you want to create the objects and credentials, and the scripts will create the objects. +- Interactive in specific tenant: you will provide the tenant in which you want to create the objects and then you will be prompted for credentials, and the scripts will create the objects, Here are the details on how to do this. #### Option 1 (interactive) -- Just run ``. .\Configure.ps1``, and you will be prompted to sign-in (email address, password, and if needed MFA). +- Just run ``.\Configure.ps1``, and you will be prompted to sign-in (email address, password, and if needed MFA). - The script will be run as the signed-in user and will use the tenant in which the user is defined. Note that the script will choose the tenant in which to create the applications, based on the user. Also to run the `Cleanup.ps1` script, you will need to re-sign-in. -#### Option 2 (non-interactive) - -When you know the identity and credentials of the user in the name of whom you want to create the applications, you can use the non-interactive approach. It's more adapted to DevOps. Here is an example of script you'd want to run in a PowerShell Window - -```PowerShell -$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force -$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) -. .\Cleanup.ps1 -Credential $mycreds -. .\Configure.ps1 -Credential $mycreds -``` - -Of course, in real life, you might already get the password as a `SecureString`. You might also want to get the password from KeyVault. - -#### Option 3 (Interactive, but create apps in a specified tenant) +#### Option 2 (Interactive, but create apps in a specified tenant) if you want to create the apps in a particular tenant, you can use the following option: -- open the [Azure portal](https://portal.azure.com) +- Open the [Azure portal](https://portal.azure.com) - Select the Azure Active directory you are interested in (in the combo-box below your name on the top right of the browser window) - Find the "Active Directory" object in this tenant - Go to **Properties** and copy the content of the **Directory Id** property @@ -154,32 +130,19 @@ $tenantId = "yourTenantIdGuid" . .\Configure.ps1 -TenantId $tenantId ``` -#### Option 4 (non-interactive, and create apps in a specified tenant) - -This option combines option 2 and option 3: it creates the application in a specific tenant. See option 3 for the way to get the tenant Id. Then run: - -```PowerShell -$secpasswd = ConvertTo-SecureString "[Password here]" -AsPlainText -Force -$mycreds = New-Object System.Management.Automation.PSCredential ("[login@tenantName here]", $secpasswd) -$tenantId = "yourTenantIdGuid" -. .\Cleanup.ps1 -Credential $mycreds -TenantId $tenantId -. .\Configure.ps1 -Credential $mycreds -TenantId $tenantId -``` - ### Running the script on Azure Sovereign clouds -All the four options listed above, can be used on any Azure Sovereign clouds. By default, the script targets `AzureCloud`, but it can be changed using the parameter `-AzureEnvironmentName`. +All the four options listed above can be used on any Azure Sovereign clouds. By default, the script targets `AzureCloud`, but it can be changed using the parameter `-AzureEnvironmentName`. The acceptable values for this parameter are: - AzureCloud - AzureChinaCloud - AzureUSGovernment -- AzureGermanyCloud Example: ```PowerShell - . .\Cleanup.ps1 -AzureEnvironmentName "AzureGermanyCloud" - . .\Configure.ps1 -AzureEnvironmentName "AzureGermanyCloud" + . .\Cleanup.ps1 -AzureEnvironmentName "AzureUSGovernment" + . .\Configure.ps1 -AzureEnvironmentName "AzureUSGovernment" ``` diff --git a/2-Authorization-I/1-call-graph/AppCreationScripts/Cleanup.ps1 b/2-Authorization-I/1-call-graph/AppCreationScripts/Cleanup.ps1 index 2c5f756..ead66b7 100644 --- a/2-Authorization-I/1-call-graph/AppCreationScripts/Cleanup.ps1 +++ b/2-Authorization-I/1-call-graph/AppCreationScripts/Cleanup.ps1 @@ -1,26 +1,18 @@ + [CmdletBinding()] param( - [PSCredential] $Credential, [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] [string] $tenantId, - [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script. Default = Global')] [string] $azureEnvironmentName ) -#Requires -Modules AzureAD -RunAsAdministrator - - -if ($null -eq (Get-Module -ListAvailable -Name "AzureAD")) { - Install-Module "AzureAD" -Scope CurrentUser -} -Import-Module AzureAD -$ErrorActionPreference = "Stop" Function Cleanup { if (!$azureEnvironmentName) { - $azureEnvironmentName = "AzureCloud" + $azureEnvironmentName = "Global" } <# @@ -31,64 +23,129 @@ Function Cleanup # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD. - # Login to Azure PowerShell (interactive if credentials are not already provided: - # you'll need to sign-in with creds enabling your to create apps in the tenant) - if (!$Credential -and $TenantId) + # Connect to the Microsoft Graph API + Write-Host "Connecting to Microsoft Graph" + + + if ($tenantId -eq "") { - $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + Connect-MgGraph -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName } - else + else { - if (!$TenantId) - { - $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName - } - else - { - $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName - } + Connect-MgGraph -TenantId $tenantId -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName } + + $context = Get-MgContext + $tenantId = $context.TenantId - if (!$tenantId) - { - $tenantId = $creds.Tenant.Id - } - $tenant = Get-AzureADTenantDetail - $tenantName = ($tenant.VerifiedDomains | Where-Object { $_._Default -eq $True }).Name + # Get the user running the script + $currentUserPrincipalName = $context.Account + $user = Get-MgUser -Filter "UserPrincipalName eq '$($context.Account)'" + + # get the tenant we signed in to + $Tenant = Get-MgOrganization + $tenantName = $Tenant.DisplayName + $verifiedDomain = $Tenant.VerifiedDomains | where {$_.Isdefault -eq $true} + $verifiedDomainName = $verifiedDomain.Name + $tenantId = $Tenant.Id + + Write-Host ("Connected to Tenant {0} ({1}) as account '{2}'. Domain is '{3}'" -f $Tenant.DisplayName, $Tenant.Id, $currentUserPrincipalName, $verifiedDomainName) + # Removes the applications - Write-Host "Cleaning-up applications from tenant '$tenantName'" + Write-Host "Cleaning-up applications from tenant '$tenantId'" - Write-Host "Removing 'spa' (ms-identity-javascript-c2s1) if needed" + Write-Host "Removing 'client' (ms-identity-javascript-c2s1) if needed" try { - Get-AzureADApplication -Filter "DisplayName eq 'ms-identity-javascript-c2s1'" | ForEach-Object {Remove-AzureADApplication -ObjectId $_.ObjectId } + Get-MgApplication -Filter "DisplayName eq 'ms-identity-javascript-c2s1'" | ForEach-Object {Remove-MgApplication -ApplicationId $_.Id } } catch { - Write-Host "Unable to remove the 'ms-identity-javascript-c2s1' . Try deleting manually." -ForegroundColor White -BackgroundColor Red + $message = $_ + Write-Warning $Error[0] + Write-Host "Unable to remove the application 'ms-identity-javascript-c2s1'. Error is $message. Try deleting manually." -ForegroundColor White -BackgroundColor Red } - $apps = Get-AzureADApplication -Filter "DisplayName eq 'ms-identity-javascript-c2s1'" + + Write-Host "Making sure there are no more (ms-identity-javascript-c2s1) applications found, will remove if needed..." + $apps = Get-MgApplication -Filter "DisplayName eq 'ms-identity-javascript-c2s1'" | Format-List Id, DisplayName, AppId, SignInAudience, PublisherDomain + if ($apps) { - Remove-AzureADApplication -ObjectId $apps.ObjectId + Remove-MgApplication -ApplicationId $apps.Id } foreach ($app in $apps) { - Remove-AzureADApplication -ObjectId $app.ObjectId + Remove-MgApplication -ApplicationId $app.Id Write-Host "Removed ms-identity-javascript-c2s1.." } + # also remove service principals of this app try { - Get-AzureADServicePrincipal -filter "DisplayName eq 'ms-identity-javascript-c2s1'" | ForEach-Object {Remove-AzureADServicePrincipal -ObjectId $_.Id -Confirm:$false} + Get-MgServicePrincipal -filter "DisplayName eq 'ms-identity-javascript-c2s1'" | ForEach-Object {Remove-MgServicePrincipal -ServicePrincipalId $_.Id -Confirm:$false} } catch { - Write-Host "Unable to remove ServicePrincipal 'ms-identity-javascript-c2s1' . Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red + $message = $_ + Write-Warning $Error[0] + Write-Host "Unable to remove ServicePrincipal 'ms-identity-javascript-c2s1'. Error is $message. Try deleting manually from Enterprise applications." -ForegroundColor White -BackgroundColor Red } } -Cleanup -Credential $Credential -tenantId $TenantId +# Pre-requisites +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph")) { + Install-Module "Microsoft.Graph" -Scope CurrentUser +} + +#Import-Module Microsoft.Graph + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Authentication")) { + Install-Module "Microsoft.Graph.Authentication" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Authentication + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Identity.DirectoryManagement")) { + Install-Module "Microsoft.Graph.Identity.DirectoryManagement" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Identity.DirectoryManagement + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) { + Install-Module "Microsoft.Graph.Applications" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Applications + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Groups")) { + Install-Module "Microsoft.Graph.Groups" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Groups + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Users")) { + Install-Module "Microsoft.Graph.Users" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Users + +$ErrorActionPreference = "Stop" + + +try +{ + Cleanup -tenantId $tenantId -environment $azureEnvironmentName +} +catch +{ + $_.Exception.ToString() | out-host + $message = $_ + Write-Warning $Error[0] + Write-Host "Unable to register apps. Error is $message." -ForegroundColor White -BackgroundColor Red +} +Write-Host "Disconnecting from tenant" +Disconnect-MgGraph diff --git a/2-Authorization-I/1-call-graph/AppCreationScripts/Configure.ps1 b/2-Authorization-I/1-call-graph/AppCreationScripts/Configure.ps1 index 4e46fd9..8ce9b8c 100644 --- a/2-Authorization-I/1-call-graph/AppCreationScripts/Configure.ps1 +++ b/2-Authorization-I/1-call-graph/AppCreationScripts/Configure.ps1 @@ -1,24 +1,19 @@ + [CmdletBinding()] param( - [PSCredential] $Credential, [Parameter(Mandatory=$False, HelpMessage='Tenant ID (This is a GUID which represents the "Directory ID" of the AzureAD tenant into which you want to create the apps')] [string] $tenantId, - [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script (it defaults to AzureCloud)')] + [Parameter(Mandatory=$False, HelpMessage='Azure environment to use while running the script. Default = Global')] [string] $azureEnvironmentName ) -#Requires -Modules AzureAD -RunAsAdministrator - <# This script creates the Azure AD applications needed for this sample and updates the configuration files for the visual Studio projects from the data in the Azure AD applications. - Before running this script you need to install the AzureAD cmdlets as an administrator. - For this: - 1) Run Powershell as an administrator - 2) in the PowerShell window, type: Install-Module AzureAD - - There are four ways to run this script. For more information, read the AppCreationScripts.md file in the same folder as this script. + In case you don't have Microsoft.Graph.Applications already installed, the script will automatically install it for the current user + + There are two ways to run this script. For more information, read the AppCreationScripts.md file in the same folder as this script. #> # Adds the requiredAccesses (expressed as a pipe separated string) to the requiredAccess structure @@ -27,19 +22,19 @@ param( Function AddResourcePermission($requiredAccess, ` $exposedPermissions, [string]$requiredAccesses, [string]$permissionType) { - foreach($permission in $requiredAccesses.Trim().Split("|")) + foreach($permission in $requiredAccesses.Trim().Split("|")) + { + foreach($exposedPermission in $exposedPermissions) { - foreach($exposedPermission in $exposedPermissions) - { - if ($exposedPermission.Value -eq $permission) - { - $resourceAccess = New-Object Microsoft.Open.AzureAD.Model.ResourceAccess - $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions - $resourceAccess.Id = $exposedPermission.Id # Read directory data - $requiredAccess.ResourceAccess.Add($resourceAccess) - } - } + if ($exposedPermission.Value -eq $permission) + { + $resourceAccess = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphResourceAccess + $resourceAccess.Type = $permissionType # Scope = Delegated permissions | Role = Application permissions + $resourceAccess.Id = $exposedPermission.Id # Read directory data + $requiredAccess.ResourceAccess += $resourceAccess + } } + } } # @@ -54,17 +49,17 @@ Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requ } else { - $sp = Get-AzureADServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'" + $sp = Get-MgServicePrincipal -Filter "DisplayName eq '$applicationDisplayName'" } $appid = $sp.AppId - $requiredAccess = New-Object Microsoft.Open.AzureAD.Model.RequiredResourceAccess + $requiredAccess = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess $requiredAccess.ResourceAppId = $appid - $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.ResourceAccess] + $requiredAccess.ResourceAccess = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphResourceAccess] # $sp.Oauth2Permissions | Select Id,AdminConsentDisplayName,Value: To see the list of all the Delegated permissions for the application: if ($requiredDelegatedPermissions) { - AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2Permissions -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" + AddResourcePermission $requiredAccess -exposedPermissions $sp.Oauth2PermissionScopes -requiredAccesses $requiredDelegatedPermissions -permissionType "Scope" } # $sp.AppRoles | Select Id,AdminConsentDisplayName,Value: To see the list of all the Application permissions for the application @@ -76,6 +71,49 @@ Function GetRequiredPermissions([string] $applicationDisplayName, [string] $requ } +<#.Description + This function takes a string input as a single line, matches a key value and replaces with the replacement value +#> +Function UpdateLine([string] $line, [string] $value) +{ + $index = $line.IndexOf(':') + $lineEnd = '' + + if($line[$line.Length - 1] -eq ','){ $lineEnd = ',' } + + if ($index -ige 0) + { + $line = $line.Substring(0, $index+1) + " " + '"' + $value+ '"' + $lineEnd + } + return $line +} + +<#.Description + This function takes a dictionary of keys to search and their replacements and replaces the placeholders in a text file +#> +Function UpdateTextFile([string] $configFilePath, [System.Collections.HashTable] $dictionary) +{ + $lines = Get-Content $configFilePath + $index = 0 + while($index -lt $lines.Length) + { + $line = $lines[$index] + foreach($key in $dictionary.Keys) + { + if ($line.Contains($key)) + { + $lines[$index] = UpdateLine $line $dictionary[$key] + } + } + $index++ + } + + Set-Content -Path $configFilePath -Value $lines -Force +} + +<#.Description + This function takes a string input as a single line, matches a key value and replaces with the replacement value +#> Function ReplaceInLine([string] $line, [string] $key, [string] $value) { $index = $line.IndexOf($key) @@ -87,6 +125,9 @@ Function ReplaceInLine([string] $line, [string] $key, [string] $value) return $line } +<#.Description + This function takes a dictionary of keys to search and their replacements and replaces the placeholders in a text file +#> Function ReplaceInTextFile([string] $configFilePath, [System.Collections.HashTable] $dictionary) { $lines = Get-Content $configFilePath @@ -107,128 +148,215 @@ Function ReplaceInTextFile([string] $configFilePath, [System.Collections.HashTab Set-Content -Path $configFilePath -Value $lines -Force } -Set-Content -Value "" -Path createdApps.html -Add-Content -Value "" -Path createdApps.html -$ErrorActionPreference = "Stop" - -Function ConfigureApplications +<#.Description + This function takes a string as input and creates an instance of an Optional claim object +#> +Function CreateOptionalClaim([string] $name) { + <#.Description + This function creates a new Azure AD optional claims with default and provided values + #> + + $appClaim = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim + $appClaim.AdditionalProperties = New-Object System.Collections.Generic.List[string] + $appClaim.Source = $null + $appClaim.Essential = $false + $appClaim.Name = $name + return $appClaim +} + <#.Description - This function creates the Azure AD applications for the sample in the provided Azure AD tenant and updates the - configuration files in the client and service project of the visual studio solution (App.Config and Web.Config) - so that they are consistent with the Applications parameters + Primary entry method to create and configure app registrations #> - $commonendpoint = "common" +Function ConfigureApplications +{ + $isOpenSSl = 'N' #temporary disable open certificate creation + + <#.Description + This function creates the Azure AD applications for the sample in the provided Azure AD tenant and updates the + configuration files in the client and service project of the visual studio solution (App.Config and Web.Config) + so that they are consistent with the Applications parameters + #> if (!$azureEnvironmentName) { - $azureEnvironmentName = "AzureCloud" + $azureEnvironmentName = "Global" } - # $tenantId is the Active Directory Tenant. This is a GUID which represents the "Directory ID" of the AzureAD tenant - # into which you want to create the apps. Look it up in the Azure portal in the "Properties" of the Azure AD. - - # Login to Azure PowerShell (interactive if credentials are not already provided: - # you'll need to sign-in with creds enabling your to create apps in the tenant) - if (!$Credential -and $TenantId) - { - $creds = Connect-AzureAD -TenantId $tenantId -AzureEnvironmentName $azureEnvironmentName + # Connect to the Microsoft Graph API, non-interactive is not supported for the moment (Oct 2021) + Write-Host "Connecting to Microsoft Graph" + if ($tenantId -eq "") { + Connect-MgGraph -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName } - else - { - if (!$TenantId) - { - $creds = Connect-AzureAD -Credential $Credential -AzureEnvironmentName $azureEnvironmentName - } - else - { - $creds = Connect-AzureAD -TenantId $tenantId -Credential $Credential -AzureEnvironmentName $azureEnvironmentName - } + else { + Connect-MgGraph -TenantId $tenantId -Scopes "User.Read.All Organization.Read.All Application.ReadWrite.All" -Environment $azureEnvironmentName } + + $context = Get-MgContext + $tenantId = $context.TenantId - if (!$tenantId) - { - $tenantId = $creds.Tenant.Id - } + # Get the user running the script + $currentUserPrincipalName = $context.Account + $user = Get-MgUser -Filter "UserPrincipalName eq '$($context.Account)'" + # get the tenant we signed in to + $Tenant = Get-MgOrganization + $tenantName = $Tenant.DisplayName + $verifiedDomain = $Tenant.VerifiedDomains | where {$_.Isdefault -eq $true} + $verifiedDomainName = $verifiedDomain.Name + $tenantId = $Tenant.Id - $tenant = Get-AzureADTenantDetail - $tenantName = ($tenant.VerifiedDomains | Where { $_._Default -eq $True }).Name - - # Get the user running the script to add the user as the app owner - $user = Get-AzureADUser -ObjectId $creds.Account.Id + Write-Host ("Connected to Tenant {0} ({1}) as account '{2}'. Domain is '{3}'" -f $Tenant.DisplayName, $Tenant.Id, $currentUserPrincipalName, $verifiedDomainName) - # Create the spa AAD application + # Create the client AAD application Write-Host "Creating the AAD application (ms-identity-javascript-c2s1)" # create the application - $spaAadApplication = New-AzureADApplication -DisplayName "ms-identity-javascript-c2s1" ` - -HomePage "http://localhost:3000/" ` - -ReplyUrls "http://localhost:3000/" ` - -PublicClient $False - - # create the service principal of the newly created application - $currentAppId = $spaAadApplication.AppId - $spaServicePrincipal = New-AzureADServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp} - - # add the user running the script as an app owner if needed - $owner = Get-AzureADApplicationOwner -ObjectId $spaAadApplication.ObjectId - if ($owner -eq $null) - { - Add-AzureADApplicationOwner -ObjectId $spaAadApplication.ObjectId -RefObjectId $user.ObjectId - Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($spaServicePrincipal.DisplayName)'" - } - - - Write-Host "Done creating the spa application (ms-identity-javascript-c2s1)" - - # URL of the AAD application in the Azure portal - # Future? $spaPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$spaAadApplication.AppId+"/objectId/"+$spaAadApplication.ObjectId+"/isMSAApp/" - $spaPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$spaAadApplication.AppId+"/objectId/"+$spaAadApplication.ObjectId+"/isMSAApp/" - Add-Content -Value "" -Path createdApps.html - - $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Open.AzureAD.Model.RequiredResourceAccess] - - # Add Required Resources Access (from 'spa' to 'Microsoft Graph') - Write-Host "Getting access from 'spa' to 'Microsoft Graph'" - $requiredPermissions = GetRequiredPermissions -applicationDisplayName "Microsoft Graph" ` - -requiredDelegatedPermissions "User.Read" ` - - $requiredResourcesAccess.Add($requiredPermissions) - - - Set-AzureADApplication -ObjectId $spaAadApplication.ObjectId -RequiredResourceAccess $requiredResourcesAccess - Write-Host "Granted permissions." - - # Update config file for 'spa' - $configFile = $pwd.Path + "\..\App\authConfig.js" - Write-Host "Updating the sample code ($configFile)" - $dictionary = @{ "Enter_the_Application_Id_Here" = $spaAadApplication.AppId;"Enter_the_Tenant_Info_Here" = $tenantId }; - ReplaceInTextFile -configFilePath $configFile -dictionary $dictionary - Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" - Write-Host "IMPORTANT: Please follow the instructions below to complete a few manual step(s) in the Azure portal": - Write-Host "- For spa" - Write-Host " - Navigate to $spaPortalUrl" - Write-Host " - Navigate to Azure portal and set the 'replyUrlsWithType' to 'Spa' in the application manifest" -ForegroundColor Red - - Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" - if($isOpenSSL -eq 'Y') - { - Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" - Write-Host "You have generated certificate using OpenSSL so follow below steps: " - Write-Host "Install the certificate on your system from current folder." - Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" - } - Add-Content -Value "
ApplicationAppIdUrl in the Azure portal
spa$currentAppIdms-identity-javascript-c2s1
" -Path createdApps.html + $clientAadApplication = New-MgApplication -DisplayName "ms-identity-javascript-c2s1" ` + -Spa ` + @{ ` + RedirectUris = "http://localhost:3000", "http://localhost:3000/redirect"; ` + } ` + -SignInAudience AzureADMyOrg ` + #end of command + + $currentAppId = $clientAadApplication.AppId + $currentAppObjectId = $clientAadApplication.Id + + $tenantName = (Get-MgApplication -ApplicationId $currentAppObjectId).PublisherDomain + #Update-MgApplication -ApplicationId $currentAppObjectId -IdentifierUris @("https://$tenantName/ms-identity-javascript-c2s1") + + # create the service principal of the newly created application + $clientServicePrincipal = New-MgServicePrincipal -AppId $currentAppId -Tags {WindowsAzureActiveDirectoryIntegratedApp} + + # add the user running the script as an app owner if needed + $owner = Get-MgApplicationOwner -ApplicationId $currentAppObjectId + if ($owner -eq $null) + { + New-MgApplicationOwnerByRef -ApplicationId $currentAppObjectId -BodyParameter = @{"@odata.id" = "htps://graph.microsoft.com/v1.0/directoryObjects/$user.ObjectId"} + Write-Host "'$($user.UserPrincipalName)' added as an application owner to app '$($clientServicePrincipal.DisplayName)'" + } + + # Add Claims + + $optionalClaims = New-Object Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaims + $optionalClaims.AccessToken = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + $optionalClaims.IdToken = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + $optionalClaims.Saml2Token = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphOptionalClaim] + + # Add Optional Claims + + $newClaim = CreateOptionalClaim -name "acct" + $optionalClaims.IdToken += ($newClaim) + $newClaim = CreateOptionalClaim -name "login_hint" + $optionalClaims.IdToken += ($newClaim) + Update-MgApplication -ApplicationId $currentAppObjectId -OptionalClaims $optionalClaims + Write-Host "Done creating the client application (ms-identity-javascript-c2s1)" + + # URL of the AAD application in the Azure portal + # Future? $clientPortalUrl = "https://portal.azure.com/#@"+$tenantName+"/blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/Overview/appId/"+$currentAppId+"/objectId/"+$currentAppObjectId+"/isMSAApp/" + $clientPortalUrl = "https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationMenuBlade/CallAnAPI/appId/"+$currentAppId+"/objectId/"+$currentAppObjectId+"/isMSAApp/" + + Add-Content -Value "client$currentAppIdms-identity-javascript-c2s1" -Path createdApps.html + # Declare a list to hold RRA items + $requiredResourcesAccess = New-Object System.Collections.Generic.List[Microsoft.Graph.PowerShell.Models.MicrosoftGraphRequiredResourceAccess] + + # Add Required Resources Access (from 'client' to 'Microsoft Graph') + Write-Host "Getting access from 'client' to 'Microsoft Graph'" + $requiredPermission = GetRequiredPermissions -applicationDisplayName "Microsoft Graph"` + -requiredDelegatedPermissions "User.Read|Contacts.Read" + + $requiredResourcesAccess.Add($requiredPermission) + Write-Host "Added 'Microsoft Graph' to the RRA list." + # Useful for RRA additions troubleshooting + # $requiredResourcesAccess.Count + # $requiredResourcesAccess + + Update-MgApplication -ApplicationId $currentAppObjectId -RequiredResourceAccess $requiredResourcesAccess + Write-Host "Granted permissions." + + + # print the registered app portal URL for any further navigation + Write-Host "Successfully registered and configured that app registration for 'ms-identity-javascript-c2s1' at `n $clientPortalUrl" -ForegroundColor Green + + # Update config file for 'client' + # $configFile = $pwd.Path + "\..\App\authConfig.js" + $configFile = $(Resolve-Path ($pwd.Path + "\..\App\authConfig.js")) + + $dictionary = @{ "Enter_the_Application_Id_Here" = $clientAadApplication.AppId;"Enter_the_Tenant_Id_Here" = $tenantId }; + + Write-Host "Updating the sample config '$configFile' with the following config values:" -ForegroundColor Yellow + $dictionary + Write-Host "-----------------" + + ReplaceInTextFile -configFilePath $configFile -dictionary $dictionary + +if($isOpenSSL -eq 'Y') +{ + Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" + Write-Host "You have generated certificate using OpenSSL so follow below steps: " + Write-Host "Install the certificate on your system from current folder." + Write-Host -ForegroundColor Green "------------------------------------------------------------------------------------------------" } +Add-Content -Value "" -Path createdApps.html +} # end of ConfigureApplications function # Pre-requisites -if ((Get-Module -ListAvailable -Name "AzureAD") -eq $null) { - Install-Module "AzureAD" -Scope CurrentUser + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph")) { + Install-Module "Microsoft.Graph" -Scope CurrentUser +} + +#Import-Module Microsoft.Graph + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Authentication")) { + Install-Module "Microsoft.Graph.Authentication" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Authentication + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Identity.DirectoryManagement")) { + Install-Module "Microsoft.Graph.Identity.DirectoryManagement" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Identity.DirectoryManagement + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Applications")) { + Install-Module "Microsoft.Graph.Applications" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Applications + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Groups")) { + Install-Module "Microsoft.Graph.Groups" -Scope CurrentUser } -Import-Module AzureAD +Import-Module Microsoft.Graph.Groups + +if ($null -eq (Get-Module -ListAvailable -Name "Microsoft.Graph.Users")) { + Install-Module "Microsoft.Graph.Users" -Scope CurrentUser +} + +Import-Module Microsoft.Graph.Users + +Set-Content -Value "" -Path createdApps.html +Add-Content -Value "" -Path createdApps.html + +$ErrorActionPreference = "Stop" # Run interactively (will ask you for the tenant ID) -ConfigureApplications -Credential $Credential -tenantId $TenantId \ No newline at end of file + +try +{ + ConfigureApplications -tenantId $tenantId -environment $azureEnvironmentName +} +catch +{ + $_.Exception.ToString() | out-host + $message = $_ + Write-Warning $Error[0] + Write-Host "Unable to register apps. Error is $message." -ForegroundColor White -BackgroundColor Red +} +Write-Host "Disconnecting from tenant" +Disconnect-MgGraph \ No newline at end of file diff --git a/2-Authorization-I/1-call-graph/AppCreationScripts/sample.json b/2-Authorization-I/1-call-graph/AppCreationScripts/sample.json index 8933d9a..7a65d31 100644 --- a/2-Authorization-I/1-call-graph/AppCreationScripts/sample.json +++ b/2-Authorization-I/1-call-graph/AppCreationScripts/sample.json @@ -1,38 +1,39 @@ { "Sample": { - "Title": "Vanilla JavaScript single-page application using MSAL.js to authorize users for calling Microsoft Graph", - "Level": 100, - "Client": "Vanilla JavaScript SPA", - "Service": "Microsoft Graph", - "RepositoryUrl": "ms-identity-javascript-tutorial/2-Authorization-I/1-call-graph", - "Endpoint": "AAD v2.0" + "Title": "Vanilla JavaScript single-page application using MSAL.js to authenticate users to call Microsoft Graph", + "Level": 100, + "Client": "Vanilla JavaScript SPA", + "Service": "Microsoft Graph", + "RepositoryUrl": "ms-identity-javascript-tutorial", + "Endpoint": "AAD v2.0", + "Languages": ["javascript"], + "Description": "Vanilla JavaScript single-page application using MSAL.js to authenticate users and calling the Microsoft Graph API on their behalf", + "Products": ["azure-active-directory", "msal-js", "msal-browser"], + "Platform": "JavaScript" }, "AADApps": [ { - "Id": "spa", - "Name": "ms-identity-javascript-c2s1", - "Kind": "SinglePageApplication", - "HomePage": "http://localhost:3000/", - "ReplyUrls": "http://localhost:3000/", - "Audience": "AzureADMyOrg", - "RequiredResourcesAccess": [ - { - "Resource": "Microsoft Graph", - "DelegatedPermissions": [ - "User.Read" - ] - } - ], - "ManualSteps": [ - { - "Comment": "Navigate to the Manifest page, find the 'replyUrlsWithType' section and change the type of redirect URI to 'Spa'" - } - ] + "Id": "client", + "Name": "ms-identity-javascript-c2s1", + "Kind": "SinglePageApplication", + "HomePage": "http://localhost:3000/", + "SampleSubPath": "2-Authorization-I\\1-call-graph", + "ReplyUrls": "http://localhost:3000, http://localhost:3000/redirect", + "Audience": "AzureADMyOrg", + "OptionalClaims": { + "IdTokenClaims": ["acct", "login_hint"] + }, + "RequiredResourcesAccess": [ + { + "Resource": "Microsoft Graph", + "DelegatedPermissions": ["User.Read", "Contacts.Read"] + } + ] } ], "CodeConfiguration": [ { - "App": "spa", + "App": "client", "SettingKind": "Replace", "SettingFile": "\\..\\App\\authConfig.js", "Mappings": [ @@ -41,7 +42,7 @@ "value": ".AppId" }, { - "key": "Enter_the_Tenant_Info_Here", + "key": "Enter_the_Tenant_Id_Here", "value": "$tenantId" } ] diff --git a/2-Authorization-I/1-call-graph/README-incremental.md b/2-Authorization-I/1-call-graph/README-incremental.md index 7a4a21e..912bce6 100644 --- a/2-Authorization-I/1-call-graph/README-incremental.md +++ b/2-Authorization-I/1-call-graph/README-incremental.md @@ -3,10 +3,9 @@ 1. [Overview](#overview) 1. [Scenario](#scenario) 1. [Contents](#contents) - 1. [Setup](#setup) - 1. [Registration](#registration) - 1. [Running the sample](#running-the-sample) + 1. [Setup the sample](#setup-the-sample) 1. [Explore the sample](#explore-the-sample) + 1. [Troubleshooting](#troubleshooting) 1. [About the code](#about-the-code) 1. [More information](#more-information) 1. [Community Help and Support](#community-help-and-support) @@ -37,39 +36,113 @@ In addition, this sample also demonstrates how to use the [Microsoft Graph JavaS | `App/ui.js` | Contains UI logic. | | `server.js` | Simple Express server for `index.html`. | -## Setup +## Setup the sample -Locate the sample folder, then type: +### Step 1: Clone or download this repository + +From your shell or command line: + +```console +git clone https://github.com/Azure-Samples/ms-identity-javascript-tutorial.git +``` + +or download and extract the repository *.zip* file. + +> :warning: To avoid path length limitations on Windows, we recommend cloning into a directory near the root of your drive. + +### Step 2: Install project dependencies ```console + cd 2-Authorization-I\1-call-graph npm install ``` -## Registration +### Step 3: Register the sample application(s) in your tenant -### Update the client app registration (ms-identity-javascript-c1s1-spa) +There is one project in this sample. To register it, you can: -1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure AD** service. -1. Select the **App Registrations** blade on the left, then find and select the application that you have registered in the previous tutorial (`ms-identity-javascript-c1s1-spa`). -1. In the app's registration screen, click on the **API permissions** blade in the left to open the page where we add access to the APIs that your application needs. - - Click the **Add a permission** button and then, - - Ensure that the **Microsoft APIs** tab is selected. - - In the *Commonly used Microsoft APIs* section, click on **Microsoft Graph** - - In the **Delegated permissions** section, select the **User.Read** in the list. Use the search box if necessary. - - Click on the **Add permissions** button at the bottom. +* follow the steps below for manually register your apps +* or use PowerShell scripts that: + * **automatically** creates the Azure AD applications and related objects (passwords, permissions, dependencies) for you. + * modify the projects' configuration files. -### Configure the client app to use your app registration +
+ Expand this section if you want to use this automation: -1. Open the `App\authConfig.js` file. -1. Find the key `Enter_the_Application_Id_Here` and replace the existing value with the application ID (clientId) of the `ms-identity-javascript-c1s1` application copied from the Azure portal. -1. Find the key `Enter_the_Tenant_Info_Here` and replace the existing value with `https://login.microsoftonline.com/`. -1. Find the key `Enter_the_Redirect_Uri_Here` and replace the existing value with the base address of the `ms-identity-javascript-c1s1` project (by default `http://localhost:3000`). + > :warning: If you have never used **Microsoft Graph PowerShell** before, we recommend you go through the [App Creation Scripts Guide](./AppCreationScripts/AppCreationScripts.md) once to ensure that your environment is prepared correctly for this step. + + 1. On Windows, run PowerShell as **Administrator** and navigate to the root of the cloned directory + 1. In PowerShell run: + + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force + ``` + + 1. Run the script to create your Azure AD application and configure the code of the sample application accordingly. + 1. For interactive process -in PowerShell, run: + + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 -TenantId "[Optional] - your tenant id" -AzureEnvironmentName "[Optional] - Azure environment, defaults to 'Global'" + ``` + + > Other ways of running the scripts are described in [App Creation Scripts guide](./AppCreationScripts/AppCreationScripts.md). The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios. + +
+ +#### Choose the Azure AD tenant where you want to create your applications + +To manually register the apps, as a first step you'll need to: -## Running the sample +1. Sign in to the [Azure portal](https://portal.azure.com). +1. If your account is present in more than one Azure AD tenant, select your profile at the top right corner in the menu on top of the page, and then **switch directory** to change your portal session to the desired Azure AD tenant. -Locate the sample folder, then type: +#### Register the client app (ms-identity-javascript-c2s1) + +1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure Active Directory** 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: + 1. In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `ms-identity-javascript-c2s1`. + 1. Under **Supported account types**, select **Accounts in this organizational directory only** + 1. Select **Register** to create the application. +1. In the **Overview** blade, find and note the **Application (client) ID**. You use this value in your app's configuration file(s) later in your code. +1. In the app's registration screen, select the **Authentication** blade to the left. +1. If you don't have a platform added, select **Add a platform** and select the **Single-page application** option. + 1. In the **Redirect URI** section enter the following redirect URIs: + 1. `http://localhost:3000` + 1. `http://localhost:3000/redirect` + 1. Click **Save** to save your changes. +1. Since this app signs-in users, we will now proceed to select **delegated permissions**, which is is required by apps signing-in users. + 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: + 1. Select the **Add a permission** button and then: + 1. Ensure that the **Microsoft APIs** tab is selected. + 1. In the *Commonly used Microsoft APIs* section, select **Microsoft Graph** + 1. In the **Delegated permissions** section, select **User.Read**, **Contacts.Read** in the list. Use the search box if necessary. + 1. Select the **Add permissions** button at the bottom. + +##### Configure Optional Claims + +1. Still on the same app registration, select the **Token configuration** blade to the left. +1. Select **Add optional claim**: + 1. Select **optional claim type**, then choose **ID**. + 1. Select the optional claim **acct**. + > Provides user's account status in tenant. If the user is a **member** of the tenant, the value is *0*. If they're a **guest**, the value is *1*. + 1. Select **Add** to save your changes. + +##### Configure the client app (ms-identity-javascript-c2s1) to use your app registration + +Open the project in your IDE (like Visual Studio or Visual Studio Code) to configure the code. + +> In the steps below, "ClientID" is the same as "Application ID" or "AppId". + +1. Open the `App\authConfig.js` file. +1. Find the key `Enter_the_Application_Id_Here` and replace the existing value with the application ID (clientId) of `ms-identity-javascript-c2s1` app copied from the Azure portal. +1. Find the key `Enter_the_Tenant_Info_Here` and replace the existing value with your Azure AD tenant/directory ID. + +### Step 4: Running the sample ```console + cd 2-Authorization-I\1-call-graph npm start ``` @@ -78,14 +151,24 @@ Locate the sample folder, then type: 1. Open your browser and navigate to `http://localhost:3000`. 1. Click the **sign-in** button on the top right corner. 1. Next, click the **See my profile** button on the left. This will make a MS Graph call. -1. Click the **Read my mails** button below to see your mails. +1. Click the **Read my contacts** button below to see your contacts. ![Screenshot](./ReadmeFiles/screenshot.png) +> :information_source: Did the sample not work for you as expected? Then please reach out to us using the [GitHub Issues](../../../../issues) page. + ## We'd love your feedback! Were we successful in addressing your learning objective? Consider taking a moment to [share your experience with us](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR73pcsbpbxNJuZCMKN0lURpUNDVHTkg2VVhWMTNYUTZEM05YS1hSN01EOSQlQCN0PWcu). +## Troubleshooting + +
+ Expand for troubleshooting info + +Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. +Make sure that your questions or comments are tagged with [`azure-active-directory` `msal-js` `ms-identity` `adal` `msal`]. + ## About the code ### Protected resources and scopes @@ -210,21 +293,148 @@ class MyAuthenticationProvider { See [graph.js](./App/graph.js). The Graph client then can be used as shown below: ```javascript -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 updateUI(response, graphConfig.graphMailEndpoint.uri); - }).catch((error) => { - console.log(error); + return handleClaimsChallenge(account, response, graphConfig.graphContactsEndpoint.uri); + }) + .then((response) => { + 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); + } }); } ``` +### Handle Continuous Access Evaluation (CAE) challenge from Microsoft Graph + +Continuous access evaluation (CAE) enables applications to do just-in time token validation, for instance enforcing user session revocation in the case of password change/reset but there are other benefits. For details, see [Continuous access evaluation](https://docs.microsoft.com/azure/active-directory/conditional-access/concept-continuous-access-evaluation). + +Microsoft Graph is now CAE-enabled in Preview. This means that it can ask its client apps for more claims when conditional access policies require it. Your can enable your application to be ready to consume CAE-enabled APIs by: + +1. Declaring that the client app is capable of handling claims challenges. +1. Processing these challenges when they are thrown by the web API. + +#### Declare the CAE capability in the configuration + +This sample app declares that it's CAE-capable by adding the `clientCapabilities` property in the configuration in `authConfig.js`: + +```javascript + const msalConfig = { + auth: { + clientId: 'Enter_the_Application_Id_Here', + authority: 'https://login.microsoftonline.com/Enter_the_Tenant_Info_Here', + redirectUri: "/", + postLogoutRedirectUri: "/", + navigateToLoginRequestUrl: true, + clientCapabilities: ["CP1"] // this lets the resource owner know that this client is capable of handling claims challenge. + } + } + + const msalInstance = new PublicClientApplication(msalConfig); +``` + +#### Processing the CAE challenge from Microsoft Graph + +Once the client app receives the CAE claims challenge from Microsoft Graph, it needs to present the user with a prompt for satisfying the challenge via Azure AD authorization endpoint. To do so, we use MSAL's `acquireTokenRedirect` and `acquireTokenPopup` API's and provide the claims challenge as a parameter in the token request. This is shown in [fetch.js](./App/fetch.js), where we handle the response from the Microsoft Graph API with the `handleClaimsChallenge` method: + +```javascript + /** + * 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}`); + } +}; +``` + +After that, we require a new access token via the `acquireTokenPopup` and `acquireTokenRedirect` APIs, fetch the claims challenge from the browser's localStorage, and pass it to the `acquireTokenPopup` and `acquireTokenRedirect` APIs in the request parameter. + +```javascript +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); + }); +} +``` + ## Next Tutorial Continue with the next tutorial: [Protect and call a web API](../../3-Authorization-II/1-call-api/README-incremental.md). @@ -241,4 +451,4 @@ Make sure that your questions or comments are tagged with [`azure-ad` `azure-ad- If you find a bug in the sample, please raise the issue on [GitHub Issues](../../issues). -To provide a recommendation, visit the following [User Voice page](https://feedback.azure.com/forums/169401-azure-active-directory). +To provide a recommendation, visit the following [User Voice page](https://feedback.azure.com/forums/169401-azure-active-directory). \ No newline at end of file diff --git a/2-Authorization-I/1-call-graph/README.md b/2-Authorization-I/1-call-graph/README.md index 079a9ef..25282e7 100644 --- a/2-Authorization-I/1-call-graph/README.md +++ b/2-Authorization-I/1-call-graph/README.md @@ -1,25 +1,45 @@ -# Vanilla JavaScript single-page application using MSAL.js to authorize users for calling Microsoft Graph - - 1. [Overview](#overview) - 1. [Scenario](#scenario) - 1. [Contents](#contents) - 1. [Prerequisites](#prerequisites) - 1. [Setup](#setup) - 1. [Registration](#registration) - 1. [Running the sample](#running-the-sample) - 1. [Explore the sample](#explore-the-sample) - 1. [About the code](#about-the-code) - 1. [More information](#more-information) - 1. [Community Help and Support](#community-help-and-support) - 1. [Contributing](#contributing) - 1. [Code of Conduct](#code-of-conduct) +--- +page_type: sample +name: Vanilla JavaScript single-page application using MSAL.js to authenticate users to call Microsoft Graph +description: Vanilla JavaScript single-page application using MSAL.js to authenticate users and calling the Microsoft Graph API on their behalf +languages: + - javascript +products: + - azure-active-directory + - msal-js + - msal-browser +urlFragment: ms-identity-javascript-tutorial +extensions: +- services: ms-identity +- platform: JavaScript +- endpoint: AAD v2.0 +- level: 100 +- client: Vanilla JavaScript SPA +- service: Microsoft Graph +--- + +# Vanilla JavaScript single-page application using MSAL.js to authenticate users to call Microsoft Graph + +* [Overview](#overview) +* [Scenario](#scenario) +* [Contents](#contents) +* [Prerequisites](#prerequisites) +* [Setup the sample](#setup-the-sample) +* [Explore the sample](#explore-the-sample) +* [Troubleshooting](#troubleshooting) +* [About the code](#about-the-code) +* [Next Steps](#next-steps) +* [Contributing](#contributing) +* [Learn More](#learn-more) ## Overview -This sample demonstrates a Vanilla JavaScript single-page application that lets users authenticate against [Azure Active Directory](https://docs.microsoft.com/azure/active-directory/fundamentals/active-directory-whatis) (Azure AD) using the [Microsoft Authentication Library for JavaScript](https://github.com/AzureAD/microsoft-authentication-library-for-js) (MSAL.js), then acquires an **Access Token** for Microsoft Graph and calls the [Microsoft Graph API](https://docs.microsoft.com/graph/overview). In doing so, it also illustrates various authorization concepts, such as [Access Tokens](https://docs.microsoft.com/azure/active-directory/develop/access-tokens), [Authorization Code Grant](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow), [Dynamic Scopes and Incremental Consent](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent), **silent requests** and more. +This sample demonstrates a Vanilla JavaScript single-page application that lets users authenticate against [Azure Active Directory](https://docs.microsoft.com/azure/active-directory/fundamentals/active-directory-whatis) (Azure AD) using the [Microsoft Authentication Library for JavaScript](https://github.com/AzureAD/microsoft-authentication-library-for-js) (MSAL.js), then acquires an **[Access Token](https://aka.ms/access-tokens)** for Microsoft Graph and calls the [Microsoft Graph API](https://docs.microsoft.com/graph/overview). In doing so, it also illustrates various authorization concepts, such as [Access Tokens](https://docs.microsoft.com/azure/active-directory/develop/access-tokens), [Authorization Code Grant](https://docs.microsoft.com/azure/active-directory/develop/v2-oauth2-auth-code-flow), [Dynamic Scopes and Incremental Consent](https://docs.microsoft.com/azure/active-directory/develop/v2-permissions-and-consent), **silent requests** and more. In addition, this sample also demonstrates how to use the [Microsoft Graph JavaScript SDK](https://github.com/microsoftgraph/msgraph-sdk-javascript) client with MSAL as a custom authentication provider to query the Graph API. Note that you are not required to implement a custom provider, as the v3.0 (preview) of the SDK offers a [default provider](https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/AuthCodeMSALBrowserAuthenticationProvider.md) that implements MSAL.js. +> :information_source: To learn how applications integrate with [Microsoft Graph](https://aka.ms/graph), consider going through the recorded session:: [An introduction to Microsoft Graph for developers](https://www.youtube.com/watch?v=EBbnpFdB92A) + ## Scenario 1. The client application uses the **MSAL.js** to sign-in a user and obtain a JWT **Access Token** from **Azure AD**: @@ -42,38 +62,37 @@ In addition, this sample also demonstrates how to use the [Microsoft Graph JavaS ## Prerequisites -- [Node.js](https://nodejs.org/en/download/) must be installed to run this sample. -- A modern web browser. This sample uses **ES6** conventions and will not run on **Internet Explorer**. -- [Visual Studio Code](https://code.visualstudio.com/download) is recommended for running and editing this sample. -- [VS Code Azure Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack) extension is recommended for interacting with Azure through VS Code Interface. -- An **Azure AD** tenant. For more information see: [How to get an Azure AD tenant](https://azure.microsoft.com/documentation/articles/active-directory-howto-tenant/) -- A user account in your **Azure AD** tenant. +* [Node.js](https://nodejs.org/en/download/) must be installed to run this sample. +* [Visual Studio Code](https://code.visualstudio.com/download) is recommended for running and editing this sample. +* [VS Code Azure Tools](https://marketplace.visualstudio.com/items?itemName=ms-vscode.vscode-node-azure-pack) extension is recommended for interacting with Azure through VS Code Interface. +* A modern web browser. +* An **Azure AD** tenant. For more information, see: [How to get an Azure AD tenant](https://docs.microsoft.com/azure/active-directory/develop/test-setup-environment#get-a-test-tenant) +* A user account in your **Azure AD** tenant. + +>This sample will not work with a **personal Microsoft account**. If you're signed in to the [Azure portal](https://portal.azure.com) with a personal Microsoft account and have not created a user account in your directory before, you will need to create one before proceeding. -## Setup +## Setup the sample ### Step 1: Clone or download this repository From your shell or command line: ```console - git clone https://github.com/Azure-Samples/tutorial.git +git clone https://github.com/Azure-Samples/ms-identity-javascript-tutorial.git ``` -or download and extract the repository .zip file. +or download and extract the repository *.zip* file. > :warning: To avoid path length limitations on Windows, we recommend cloning into a directory near the root of your drive. ### Step 2: Install project dependencies ```console - cd ms-identity-javascript-tutorial - cd 2-Authorization-I/1-call-graph + cd 2-Authorization-I\1-call-graph npm install ``` -## Registration - -### Register the sample application(s) with your Azure Active Directory tenant +### Step 3: Register the sample application(s) in your tenant There is one project in this sample. To register it, you can: @@ -83,70 +102,84 @@ There is one project in this sample. To register it, you can: - modify the projects' configuration files.
- Expand this section if you want to use this automation: + Expand this section if you want to use this automation: -> :warning: If you have never used **Azure AD Powershell** before, we recommend you go through the [App Creation Scripts](./AppCreationScripts/AppCreationScripts.md) once to ensure that your environment is prepared correctly for this step. + > :warning: If you have never used **Microsoft Graph PowerShell** before, we recommend you go through the [App Creation Scripts Guide](./AppCreationScripts/AppCreationScripts.md) once to ensure that your environment is prepared correctly for this step. + + 1. On Windows, run PowerShell as **Administrator** and navigate to the root of the cloned directory + 1. In PowerShell run: -1. On Windows, run PowerShell as **Administrator** and navigate to the root of the cloned directory -1. In PowerShell run: + ```PowerShell + Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force + ``` - ```PowerShell - Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope Process -Force - ``` + 1. Run the script to create your Azure AD application and configure the code of the sample application accordingly. + 1. For interactive process -in PowerShell, run: -1. Run the script to create your Azure AD application and configure the code of the sample application accordingly. -1. In PowerShell run: + ```PowerShell + cd .\AppCreationScripts\ + .\Configure.ps1 -TenantId "[Optional] - your tenant id" -AzureEnvironmentName "[Optional] - Azure environment, defaults to 'Global'" + ``` - ```PowerShell - cd .\AppCreationScripts\ - .\Configure.ps1 - ``` - - > Other ways of running the scripts are described in [App Creation Scripts](./AppCreationScripts/AppCreationScripts.md) - > The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios. + > Other ways of running the scripts are described in [App Creation Scripts guide](./AppCreationScripts/AppCreationScripts.md). The scripts also provide a guide to automated application registration, configuration and removal which can help in your CI/CD scenarios.
-### Choose the Azure AD tenant where you want to create your applications +#### Choose the Azure AD tenant where you want to create your applications -As a first step you'll need to: +To manually register the apps, as a first step you'll need to: 1. Sign in to the [Azure portal](https://portal.azure.com). 1. If your account is present in more than one Azure AD tenant, select your profile at the top right corner in the menu on top of the page, and then **switch directory** to change your portal session to the desired Azure AD tenant. -### Register the app +#### Register the client app (ms-identity-javascript-c2s1) -1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure AD** service. +1. Navigate to the [Azure portal](https://portal.azure.com) and select the **Azure Active Directory** 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-javascript-tutorial-c2s1`. - - Under **Supported account types**, select **Accounts in this organizational directory only**. - - In the **Redirect URI (optional)** section, select **Single-Page Application** in the combo-box and enter the following redirect URI: `http://localhost:3000/`. -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, click on the **API permissions** blade in the left to open the page where we add access to the APIs that your application needs. - - Click the **Add a permission** button and then, - - Ensure that the **Microsoft APIs** tab is selected. - - In the *Commonly used Microsoft APIs* section, click on **Microsoft Graph** - - In the **Delegated permissions** section, select the **User.Read** in the list. Use the search box if necessary. - - Click on the **Add permissions** button at the bottom. - -#### Configure the app to use your app registration + 1. In the **Name** section, enter a meaningful application name that will be displayed to users of the app, for example `ms-identity-javascript-c2s1`. + 1. Under **Supported account types**, select **Accounts in this organizational directory only** + 1. Select **Register** to create the application. +1. In the **Overview** blade, find and note the **Application (client) ID**. You use this value in your app's configuration file(s) later in your code. +1. In the app's registration screen, select the **Authentication** blade to the left. +1. If you don't have a platform added, select **Add a platform** and select the **Single-page application** option. + 1. In the **Redirect URI** section enter the following redirect URIs: + 1. `http://localhost:3000` + 1. `http://localhost:3000/redirect` + 1. Click **Save** to save your changes. +1. Since this app signs-in users, we will now proceed to select **delegated permissions**, which is is required by apps signing-in users. + 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: + 1. Select the **Add a permission** button and then: + 1. Ensure that the **Microsoft APIs** tab is selected. + 1. In the *Commonly used Microsoft APIs* section, select **Microsoft Graph** + 1. In the **Delegated permissions** section, select **User.Read**, **Contacts.Read** in the list. Use the search box if necessary. + 1. Select the **Add permissions** button at the bottom. + +##### Configure Optional Claims + +1. Still on the same app registration, select the **Token configuration** blade to the left. +1. Select **Add optional claim**: + 1. Select **optional claim type**, then choose **ID**. + 1. Select the optional claim **acct**. + > Provides user's account status in tenant. If the user is a **member** of the tenant, the value is *0*. If they're a **guest**, the value is *1*. + 1. Select the optional claim **login_hint**. + > An opaque, reliable login hint claim. This claim is the best value to use for the login_hint OAuth parameter in all flows to get SSO.See $[optional claims](https://docs.microsoft.com/azure/active-directory/develop/active-directory-optional-claims) for more details on this optional claim. + 1. Select **Add** to save your changes. + +##### Configure the client app (ms-identity-javascript-c2s1) to use your app registration Open the project in your IDE (like Visual Studio or Visual Studio Code) to configure the code. > In the steps below, "ClientID" is the same as "Application ID" or "AppId". -1. Open the `app\authConfig.js` file. Then: -1. Find the key `Enter_the_Application_Id_Here` and replace the existing value with the application ID (clientId) of the `ms-identity-javascript-tutorial-c2s1` application copied from the Azure portal. -1. Find the key `Enter_the_Tenant_Info_Here` and replace the existing value with your *tenanted* authority string. For example, `https://login.microsoftonline.com/` -1. Find the key `Enter_the_Redirect_Uri_Here` and replace the existing value with the Redirect URI for `ms-identity-javascript-tutorial-c2s1` app. For example, `http://localhost:3000/`. +1. Open the `App\authConfig.js` file. +1. Find the key `Enter_the_Application_Id_Here` and replace the existing value with the application ID (clientId) of `ms-identity-javascript-c2s1` app copied from the Azure portal. +1. Find the key `Enter_the_Tenant_Id_Here` and replace the existing value with your Azure AD tenant/directory ID. -## Running the sample +### Step 4: Running the sample ```console - cd 2-Authorization-I/1-call-graph + cd 2-Authorization-I\1-call-graph npm start ``` @@ -155,13 +188,26 @@ Open the project in your IDE (like Visual Studio or Visual Studio Code) to confi 1. Open your browser and navigate to `http://localhost:3000`. 1. Click the **sign-in** button on the top right corner. 1. Next, click the **See my profile** button on the left. This will make a MS Graph call. -1. Click the **Read my mails** button below to see your mails. +1. Click the **Read my contacts** button below to see your contacts. ![Screenshot](./ReadmeFiles/screenshot.png) +> :information_source: Did the sample not work for you as expected? Then please reach out to us using the [GitHub Issues](../../../../issues) page. + ## We'd love your feedback! -Were we successful in addressing your learning objective? Consider taking a moment to [share your experience with us](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR73pcsbpbxNJuZCMKN0lURpUNDVHTkg2VVhWMTNYUTZEM05YS1hSN01EOSQlQCN0PWcu). +Were we successful in addressing your learning objective? Consider taking a moment to[share your experience with us](https://forms.office.com/Pages/ResponsePage.aspx?id=v4j5cvGGr0GRqy180BHbR73pcsbpbxNJuZCMKN0lURpUNDVHTkg2VVhWMTNYUTZEM05YS1hSN01EOSQlQCN0PWcu). + +## Troubleshooting + +
+ Expand for troubleshooting info + +Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. +Make sure that your questions or comments are tagged with [`azure-active-directory` `msal-js` `ms-identity` `adal` `msal`]. + +To provide feedback on or suggest features for Azure Active Directory, visit [User Voice page](https://feedback.azure.com/d365community/forum/79b1327d-d925-ec11-b6e6-000d3a4f06a4). +
## About the code @@ -178,7 +224,7 @@ In **Azure AD**, the scopes (permissions) set directly on the application regist scopes: [ "openid", "profile", "User.Read" ] }; const tokenRequest = { - scopes: [ "Mail.Read" ] + scopes: [ "Contacts.Read" ] }; // will return an ID Token and an Access Token with scopes: "openid", "profile" and "User.Read" @@ -237,7 +283,7 @@ The **MSAL.js** exposes the `acquireTokenSilent()` API which is meant to retriev ### Access Token validation -Clients should treat access tokens as opaque strings, as the contents of the token are intended for the **resource only** (such as a web API or Microsoft Graph). For validation and debugging purposes, developers can decode **JWT**s (*JSON Web Tokens*) using a site like [jwt.ms](https://jwt.ms). +Clients should treat access tokens as opaque strings, as the contents of the token are intended for the **resource only** (such as a web API or Microsoft Graph). For validation and debugging purposes, developers can decode **JWT**s (*JSON Web Tokens*) using a site like [jwt.ms](https://jwt.ms). This sample does not need to validate Access Tokens it acquires. ### Calling the Microsoft Graph API @@ -287,65 +333,178 @@ class MyAuthenticationProvider { See [graph.js](./App/graph.js). The Graph client then can be used as shown below: ```javascript -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 updateUI(response, graphConfig.graphMailEndpoint.uri); - }).catch((error) => { - console.log(error); + return handleClaimsChallenge(account, response, graphConfig.graphContactsEndpoint.uri); + }) + .then((response) => { + 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); + } }); } ``` -## More information +### Handle Continuous Access Evaluation (CAE) challenge from Microsoft Graph -Configure your application: +Continuous access evaluation (CAE) enables applications to do just-in time token validation, for instance enforcing user session revocation in the case of password change/reset but there are other benefits. For details, see [Continuous access evaluation](https://docs.microsoft.com/azure/active-directory/conditional-access/concept-continuous-access-evaluation). -- [Initialize client applications using MSAL.js](https://docs.microsoft.com/azure/active-directory/develop/msal-js-initializing-client-applications) -- [Single sign-on with MSAL.js](https://docs.microsoft.com/azure/active-directory/develop/msal-js-sso) -- [Handle MSAL.js exceptions and errors](https://docs.microsoft.com/azure/active-directory/develop/msal-handling-exceptions?tabs=javascript) -- [Logging in MSAL.js applications](https://docs.microsoft.com/azure/active-directory/develop/msal-logging?tabs=javascript) -- [Pass custom state in authentication requests using MSAL.js](https://docs.microsoft.com/azure/active-directory/develop/msal-js-pass-custom-state-authentication-request) -- [Prompt behavior in MSAL.js interactive requests](https://docs.microsoft.com/azure/active-directory/develop/msal-js-prompt-behavior) +Microsoft Graph is now CAE-enabled in Preview. This means that it can ask its client apps for more claims when conditional access policies require it. Your can enable your application to be ready to consume CAE-enabled APIs by: -Learn more about the Microsoft identity platform: +1. Declaring that the client app is capable of handling claims challenges. +1. Processing these challenges when they are thrown by the web API. -- [Microsoft identity platform (Azure Active Directory for developers)](https://docs.microsoft.com/azure/active-directory/develop/) -- [Overview of Microsoft Authentication Library (MSAL)](https://docs.microsoft.com/azure/active-directory/develop/msal-overview) -- [Understanding Azure AD application consent experiences](https://docs.microsoft.com/azure/active-directory/develop/application-consent-experience) -- [Understand user and admin consent](https://docs.microsoft.com/azure/active-directory/develop/howto-convert-app-to-be-multi-tenant#understand-user-and-admin-consent) -- [Microsoft identity platform and OpenID Connect protocol](https://docs.microsoft.com/azure/active-directory/develop/v2-protocols-oidc) -- [Microsoft identity platform ID Tokens](https://docs.microsoft.com/azure/active-directory/develop/id-tokens) +#### Declare the CAE capability in the configuration -For more information about how OAuth 2.0 protocols work in this scenario and other scenarios, see [Authentication Scenarios for Azure AD](https://docs.microsoft.com/azure/active-directory/develop/authentication-flows-app-scenarios). +This sample app declares that it's CAE-capable by adding the `clientCapabilities` property in the configuration in `authConfig.js`: -## Community Help and Support +```javascript + const msalConfig = { + auth: { + clientId: 'Enter_the_Application_Id_Here', + authority: 'https://login.microsoftonline.com/Enter_the_Tenant_Info_Here', + redirectUri: "/", + postLogoutRedirectUri: "/", + navigateToLoginRequestUrl: true, + clientCapabilities: ["CP1"] // this lets the resource owner know that this client is capable of handling claims challenge. + } + } -Use [Stack Overflow](http://stackoverflow.com/questions/tagged/msal) to get support from the community. -Ask your questions on Stack Overflow first and browse existing issues to see if someone has asked your question before. -Make sure that your questions or comments are tagged with [`azure-ad` `azure-ad-b2c` `ms-identity` `msal`]. + const msalInstance = new PublicClientApplication(msalConfig); +``` -If you find a bug in the sample, please raise the issue on [GitHub Issues](../../issues). +#### Processing the CAE challenge from Microsoft Graph -To provide a recommendation, visit the following [User Voice page](https://feedback.azure.com/forums/169401-azure-active-directory). +Once the client app receives the CAE claims challenge from Microsoft Graph, it needs to present the user with a prompt for satisfying the challenge via Azure AD authorization endpoint. To do so, we use MSAL's `acquireTokenRedirect` and `acquireTokenPopup` API's and provide the claims challenge as a parameter in the token request. This is shown in [fetch.js](./App/fetch.js), where we handle the response from the Microsoft Graph API with the `handleClaimsChallenge` method: -## Contributing +```javascript + /** + * 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}`); + } +}; +``` -If you'd like to contribute to this sample, see [CONTRIBUTING.MD](../../CONTRIBUTING.md). +After that, we require a new access token via the `acquireTokenPopup` and `acquireTokenRedirect` APIs, fetch the claims challenge from the browser's localStorage, and pass it to the `acquireTokenPopup` and `acquireTokenRedirect` APIs in the request parameter. -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +```javascript +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); + }); +} +``` + +## Next Steps + +Learn how to: -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +* [Vanilla JavaScript single-page application (SPA) using MSAL.js to authorize users for calling a protected web API on Azure AD](https://github.com/Azure-Samples/ms-identity-javascript-tutorial/tree/main/3-Authorization-II/1-call-api) +* [A Node.js Web API secured by Azure AD and calling Microsoft Graph on behalf of a signed-in user](https://github.com/Azure-Samples/ms-identity-javascript-tutorial/tree/main/4-AdvancedGrants/1-call-api-graph) -## Code of Conduct +## Contributing + +If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). For more information, see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. -This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). -For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or -contact [opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. +## Learn More + +* [Microsoft identity platform (Azure Active Directory for developers)](https://docs.microsoft.com/azure/active-directory/develop/) +* [Azure AD code samples](https://docs.microsoft.com/azure/active-directory/develop/sample-v2-code) +* [Overview of Microsoft Authentication Library (MSAL)](https://docs.microsoft.com/azure/active-directory/develop/msal-overview) +* [Register an application with the Microsoft identity platform](https://docs.microsoft.com/azure/active-directory/develop/quickstart-register-app) +* [Configure a client application to access web APIs](https://docs.microsoft.com/azure/active-directory/develop/quickstart-configure-app-access-web-apis) +* [Understanding Azure AD application consent experiences](https://docs.microsoft.com/azure/active-directory/develop/application-consent-experience) +* [Understand user and admin consent](https://docs.microsoft.com/azure/active-directory/develop/howto-convert-app-to-be-multi-tenant#understand-user-and-admin-consent) +* [Application and service principal objects in Azure Active Directory](https://docs.microsoft.com/azure/active-directory/develop/app-objects-and-service-principals) +* [Authentication Scenarios for Azure AD](https://docs.microsoft.com/azure/active-directory/develop/authentication-flows-app-scenarios) +* [Building Zero Trust ready apps](https://aka.ms/ztdevsession) +* [National Clouds](https://docs.microsoft.com/azure/active-directory/develop/authentication-national-cloud#app-registration-endpoints) +* [Initialize client applications using MSAL.js](https://docs.microsoft.com/azure/active-directory/develop/msal-js-initializing-client-applications) +* [Single sign-on with MSAL.js](https://docs.microsoft.com/azure/active-directory/develop/msal-js-sso) +* [Handle MSAL.js exceptions and errors](https://docs.microsoft.com/azure/active-directory/develop/msal-handling-exceptions?tabs=javascript) +* [Logging in MSAL.js applications](https://docs.microsoft.com/azure/active-directory/develop/msal-logging?tabs=javascript) +* [Pass custom state in authentication requests using MSAL.js](https://docs.microsoft.com/azure/active-directory/develop/msal-js-pass-custom-state-authentication-request) +* [Prompt behavior in MSAL.js interactive requests](https://docs.microsoft.com/azure/active-directory/develop/msal-js-prompt-behavior) +* [Use MSAL.js to work with Azure AD B2C](https://docs.microsoft.com/azure/active-directory/develop/msal-b2c-overview) diff --git a/2-Authorization-I/1-call-graph/ReadmeFiles/screenshot.png b/2-Authorization-I/1-call-graph/ReadmeFiles/screenshot.png index f4893d0..6fe0c75 100644 Binary files a/2-Authorization-I/1-call-graph/ReadmeFiles/screenshot.png and b/2-Authorization-I/1-call-graph/ReadmeFiles/screenshot.png differ diff --git a/2-Authorization-I/1-call-graph/package-lock.json b/2-Authorization-I/1-call-graph/package-lock.json index d5c5585..1928d3b 100644 --- a/2-Authorization-I/1-call-graph/package-lock.json +++ b/2-Authorization-I/1-call-graph/package-lock.json @@ -6252,9 +6252,9 @@ "dev": true }, "caniuse-lite": { - "version": "1.0.30001280", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001280.tgz", - "integrity": "sha512-kFXwYvHe5rix25uwueBxC569o53J6TpnGu0BEEn+6Lhl2vsnAumRFWEBhDft1fwyo6m1r4i+RqA4+163FpeFcA==", + "version": "1.0.30001436", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001436.tgz", + "integrity": "sha512-ZmWkKsnC2ifEPoWUvSAIGyOYwT+keAaaWPHiQ9DfMqS1t6tfuyFYoWR78TeZtznkEQ64+vGXH9cZrElwR2Mrxg==", "dev": true }, "chalk": { diff --git a/2-Authorization-I/1-call-graph/server.js b/2-Authorization-I/1-call-graph/server.js index fa9a68f..9ee042d 100644 --- a/2-Authorization-I/1-call-graph/server.js +++ b/2-Authorization-I/1-call-graph/server.js @@ -13,6 +13,10 @@ app.use(morgan('dev')); // Setup app folders. app.use(express.static('App')); +app.get('/redirect', (req, res) => { + res.sendFile(path.join(__dirname + '/App/redirect.html')); +}); + // Set up a route for index.html app.get('*', (req, res) => { res.sendFile(path.join(__dirname + '/index.html'));
ApplicationAppIdUrl in the Azure portal