From a98ade2a96935fd7903ae7ac4a4b288f7c0e457c Mon Sep 17 00:00:00 2001 From: salman90 Date: Fri, 3 Mar 2023 14:10:08 -0800 Subject: [PATCH 1/3] added cutom hook to app --- 2-Authorization-I/1-call-graph/README.md | 135 +++++++++--------- .../SPA/src/hooks/useFetchWithMsal.jsx | 81 +++++++++++ .../1-call-graph/SPA/src/pages/Contacts.jsx | 69 ++------- .../1-call-graph/SPA/src/pages/Profile.jsx | 66 ++------- 4 files changed, 165 insertions(+), 186 deletions(-) create mode 100644 2-Authorization-I/1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx diff --git a/2-Authorization-I/1-call-graph/README.md b/2-Authorization-I/1-call-graph/README.md index 5de333c7..03f54a14 100644 --- a/2-Authorization-I/1-call-graph/README.md +++ b/2-Authorization-I/1-call-graph/README.md @@ -44,16 +44,17 @@ Here you'll learn how to [sign-in](https://docs.microsoft.com/azure/active-direc ## Contents -| File/folder | Description | -|-------------------------------------|----------------------------------------------------------------------------| -| `App.jsx` | Main application logic resides here. | -| `fetch.jsx` | Provides a helper method for making fetch calls using bearer token scheme. | -| `graph.jsx` | Instantiates Graph SDK client using a custom authentication provider. | -| `authConfig.js` | Contains authentication configuration parameters. | -| `pages/Home.jsx` | Contains a table with ID token claims and description | -| `pages/Profile.jsx` | Calls Microsoft Graph `/me` endpoint with Graph SDK. | -| `pages/Contacts.jsx` | Calls Microsoft Graph `/me/contacts` endpoint with Graph SDK. | -| `components/AccountPicker.jsx` | Contains logic to handle multiple `account` selection with MSAL.js | +| File/folder | Description | +|-------------------------------------|---------------------------------------------------------------------------------- | +| `App.jsx` | Main application logic resides here. | +| `fetch.jsx` | Provides a helper method for making fetch calls using bearer token scheme. | +| `graph.jsx` | Instantiates Graph SDK client using a custom authentication provider. | +| `authConfig.js` | Contains authentication configuration parameters. | +| `pages/Home.jsx` | Contains a table with ID token claims and description | +| `pages/Profile.jsx` | Calls Microsoft Graph `/me` by executing `useFetchWithMsal` custom hook. | +| `pages/Contacts.jsx` | Calls Microsoft Graph `/me/contacts` by executing `useFetchWithMsal` custom hook. | +| `components/AccountPicker.jsx` | Contains logic to handle multiple `account` selection with MSAL.js | +| `hooks/useFetchWithMsal.jsx` | Contains token acquisition logic to call Microsoft Graph endpoints with Graph SDK. | ## Prerequisites @@ -363,68 +364,57 @@ Once the client app receives the CAE claims challenge from Microsoft Graph, it n }; ``` -After that, we require a new access token via the `useMsalAuthentication` hook, fetch the claims challenge from the browser's localStorage, and pass it to the `useMsalAuthentication` hook in the request parameter. +After that, we require a new access token via the `useMsalAuthentication` hook, fetch the claims challenge from the browser's localStorage, and pass it to the `useMsalAuthentication` hook in the request parameter as shown in the [useFetchWithMsal](./SPA/src/hooks/useFetchWithMsal.jsx) custom hook: . ```javascript -export const Profile = () => { +const useFetchWithMsal = (request, endpoint) => { + const [error, setError] = useState(null); const { instance } = useMsal(); const account = instance.getActiveAccount(); - const [graphData, setGraphData] = useState(null); - const resource = new URL(protectedResources.graphMe.endpoint).hostname; + const resource = new URL(endpoint).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"]}}} - const request = { - scopes: protectedResources.graphMe.scopes, + + const { + result, + login, + error: msalError, + } = useMsalAuthentication(InteractionType.Popup, { + ...request, + redirectUri: '/redirect.html', account: account, claims: claims, - }; - - const { login, result, error } = useMsalAuthentication(InteractionType.Popup, request); - useEffect(() => { - if (!!graphData) { - return; - } - - if (!!error) { - // in case popup is blocked, use redirect instead - if (error.errorCode === 'popup_window_error' || error.errorCode === 'empty_window_error') { - login(InteractionType.Redirect, request); - } + }); - console.log(error); + /** + * Execute a fetch request with Graph SDK + * @param {String} endpoint + * @returns JSON response + */ + const execute = async (endpoint) => { + if (msalError) { + setError(msalError); return; } - if (result) { - getGraphClient(result.accessToken) - .api('/me') - .responseType(ResponseType.RAW) - .get() - .then((response) => { - return handleClaimsChallenge(response, protectedResources.graphMe.endpoint); - }) - .then((response) => { - if (response && response.error === 'claims_challenge_occurred') throw response.error; - setGraphData(response); - }) - .catch((error) => { - if (error === 'claims_challenge_occurred') { - login(InteractionType.Redirect, request); - } - console.log(error); - }); + let accessToken = result.accessToken; + // do something with the access token } - }, [graphData, result, error, login]); + }; - if (error) { - return
Error: {error.message}
; - } - return <>{graphData ? : null}; + return { + error, + result: result, + execute: useCallback(execute, [result, msalError]), + }; }; + +export default useFetchWithMsal; ``` ### Access token validation @@ -451,27 +441,30 @@ For more details on what's inside the access token, clients should use the token }; ``` -See [graph.js](./SPA/src/graph.js). The Graph client then can be used in your components as shown below: +See [graph.js](./SPA/src/graph.js). The Graph client then can be used in your application as shown in the [useFetchWithMsal](./SPA/src/hooks/useFetchWithMsal.jsx) custom hook: ```javascript -if (result) { - getGraphClient(result.accessToken) - .api('/me') - .responseType(ResponseType.RAW) - .get() - .then((response) => { - return handleClaimsChallenge(response, protectedResources.graphMe.endpoint); - }) - .then((response) => { - if (response && response.error === 'claims_challenge_occurred') throw response.error; - setGraphData(response); - }) - .catch((error) => { - if (error === 'claims_challenge_occurred') { + if (result) { + let accessToken = result.accessToken; + + try { + const graphResponse = await getGraphClient(accessToken) + .api(endpoint) + .responseType(ResponseType.RAW) + .get(); + const responseHasClaimsChallenge = await handleClaimsChallenge(graphResponse); + if (responseHasClaimsChallenge && responseHasClaimsChallenge.error === 'claims_challenge_occurred') { + throw responseHasClaimsChallenge.error; + } else { + return responseHasClaimsChallenge; + } + } catch (error) { + if (error === 'claims_challenge_occurred') { login(InteractionType.Redirect, request); - } - console.log(error); - }); + } else { + setError(error); + } + } } ``` diff --git a/2-Authorization-I/1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx b/2-Authorization-I/1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx new file mode 100644 index 00000000..822cf302 --- /dev/null +++ b/2-Authorization-I/1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx @@ -0,0 +1,81 @@ +import { useState, useCallback } from 'react'; +import { useMsalAuthentication, useMsal } from '@azure/msal-react'; +import { InteractionType } from '@azure/msal-browser'; +import { ResponseType } from '@microsoft/microsoft-graph-client'; +import { getGraphClient } from '../graph'; +import { getClaimsFromStorage } from '../utils/storageUtils'; +import { msalConfig } from '../authConfig'; +import { handleClaimsChallenge } from '../fetch'; + +/** + * Custom hook to call a Graph API using Graph SDK + * @param {PopupRequest} request + * @param {String} endpoint + * @returns + */ +const useFetchWithMsal = (request, endpoint) => { + const [error, setError] = useState(null); + const { instance } = useMsal(); + const account = instance.getActiveAccount(); + const resource = new URL(endpoint).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"]}}} + + const { + result, + login, + error: msalError, + } = useMsalAuthentication(InteractionType.Popup, { + ...request, + redirectUri: '/redirect.html', + account: account, + claims: claims, + }); + + /** + * Execute a fetch request with Graph SDK + * @param {String} endpoint + * @returns JSON response + */ + const execute = async (endpoint) => { + if (msalError) { + setError(msalError); + return; + } + if (result) { + let accessToken = result.accessToken; + + try { + const graphResponse = await getGraphClient(accessToken) + .api(endpoint) + .responseType(ResponseType.RAW) + .get(); + const responseHasClaimsChallenge = await handleClaimsChallenge(graphResponse); + if (responseHasClaimsChallenge && responseHasClaimsChallenge.error === 'claims_challenge_occurred') { + throw responseHasClaimsChallenge.error; + } else { + return responseHasClaimsChallenge; + } + } catch (error) { + if (error === 'claims_challenge_occurred') { + login(InteractionType.Redirect, request); + } else { + setError(error); + } + } + } + }; + + return { + error, + result: result, + execute: useCallback(execute, [result, msalError]), + }; +}; + +export default useFetchWithMsal; \ No newline at end of file diff --git a/2-Authorization-I/1-call-graph/SPA/src/pages/Contacts.jsx b/2-Authorization-I/1-call-graph/SPA/src/pages/Contacts.jsx index ecdd0280..6a9a832a 100644 --- a/2-Authorization-I/1-call-graph/SPA/src/pages/Contacts.jsx +++ b/2-Authorization-I/1-call-graph/SPA/src/pages/Contacts.jsx @@ -1,80 +1,31 @@ import { useEffect, useState } from 'react'; -import { useMsalAuthentication, useMsal } from '@azure/msal-react'; -import { InteractionType } from '@azure/msal-browser'; import { ContactsData } from '../components/DataDisplay'; -import { protectedResources, msalConfig } from '../authConfig'; -import { getClaimsFromStorage } from '../utils/storageUtils'; -import { handleClaimsChallenge } from '../fetch'; -import { getGraphClient } from '../graph'; -import { ResponseType } from '@microsoft/microsoft-graph-client'; +import { protectedResources } from '../authConfig'; +import useFetchWithMsal from '../hooks/useFetchWithMsal'; export const Contacts = () => { - const { instance } = useMsal(); - const account = instance.getActiveAccount(); const [graphData, setGraphData] = useState(null); - - const resource = new URL(protectedResources.graphContacts.endpoint).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"]}}} - - const request = { + const request = { scopes: protectedResources.graphContacts.scopes, - account: account, - claims: claims }; - - const { login, result, error } = useMsalAuthentication(InteractionType.Popup, { - ...request, - redirectUri: '/redirect.html', - }); + const { error, result, execute } = useFetchWithMsal(request, protectedResources.graphContacts.endpoint); useEffect(() => { if (!!graphData) { return; } - if (!!error) { - if (error.errorCode === 'popup_window_error' || error.errorCode === 'empty_window_error') { - login(InteractionType.Redirect, request); - } - - console.log(error); - return; - } - - if (result) { - let accessToken = result.accessToken; - getGraphClient(accessToken) - .api('/me/contacts') - .responseType(ResponseType.RAW) - .get() - .then((response) => { - return handleClaimsChallenge(response, protectedResources.graphMe.endpoint, account); - }) - .then((response) => { - if (response && response.error === 'claims_challenge_occurred') throw response.error; - setGraphData(response); - }) - .catch((error) => { - if (error === 'claims_challenge_occurred') { - login(InteractionType.Redirect, request); - } else { - setGraphData(error); - } - }); - + if (!graphData) { + execute(protectedResources.graphContacts.endpoint).then((data) => { + setGraphData(data); + }); } - }, [graphData, result, error, login]); + }, [graphData, execute, result]); if (error) { return
Error: {error.message}
; } return <>{graphData ? : null}; -};; \ No newline at end of file +}; diff --git a/2-Authorization-I/1-call-graph/SPA/src/pages/Profile.jsx b/2-Authorization-I/1-call-graph/SPA/src/pages/Profile.jsx index 79991080..d7233d93 100644 --- a/2-Authorization-I/1-call-graph/SPA/src/pages/Profile.jsx +++ b/2-Authorization-I/1-call-graph/SPA/src/pages/Profile.jsx @@ -1,78 +1,32 @@ import { useEffect, useState } from 'react'; -import { useMsal, useMsalAuthentication } from '@azure/msal-react'; -import { InteractionType } from '@azure/msal-browser'; -import { ResponseType } from '@microsoft/microsoft-graph-client'; import { ProfileData } from '../components/DataDisplay'; -import { protectedResources, msalConfig } from '../authConfig'; -import { getClaimsFromStorage } from '../utils/storageUtils'; -import { handleClaimsChallenge } from '../fetch'; -import { getGraphClient } from '../graph'; +import { protectedResources } from '../authConfig'; +import useFetchWithMsal from '../hooks/useFetchWithMsal' export const Profile = () => { - const { instance } = useMsal(); - const account = instance.getActiveAccount(); const [graphData, setGraphData] = useState(null); - - const resource = new URL(protectedResources.graphMe.endpoint).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"]}}} - const request = { scopes: protectedResources.graphMe.scopes, - account: account, - claims: claims, }; - - const { login, result, error } = useMsalAuthentication(InteractionType.Popup, { - ...request, - redirectUri: '/redirect.html', - }); - + const { error, execute, result } = useFetchWithMsal(request, protectedResources.graphMe.endpoint); + useEffect(() => { if (!!graphData) { return; } - if (!!error) { - // in case popup is blocked, use redirect instead - if (error.errorCode === 'popup_window_error' || error.errorCode === 'empty_window_error') { - login(InteractionType.Redirect, request); - } - - console.log(error); - return; + if (!graphData) { + execute(protectedResources.graphMe.endpoint).then((data) => { + setGraphData(data); + }); } - if (result) { - let accessToken = result.accessToken; - getGraphClient(accessToken) - .api('/me') - .responseType(ResponseType.RAW) - .get() - .then((response) => { - return handleClaimsChallenge(response, protectedResources.graphMe.endpoint, account); - }) - .then((response) => { - if (response && response.error === 'claims_challenge_occurred') throw response.error; - setGraphData(response); - }) - .catch((error) => { - if (error === 'claims_challenge_occurred') { - login(InteractionType.Redirect, request); - } - console.log(error); - }); - } - }, [graphData, result, error, login]); + }, [graphData, execute, result]); if (error) { return
Error: {error.message}
; } + return <>{graphData ? : null}; }; From 08f4fa5a976a7d0164e01a2fef9146548ec4737f Mon Sep 17 00:00:00 2001 From: salman90 Date: Fri, 3 Mar 2023 17:03:09 -0800 Subject: [PATCH 2/3] fix issue with handleClaimsChallenge function --- .../1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/2-Authorization-I/1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx b/2-Authorization-I/1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx index 822cf302..a429e05b 100644 --- a/2-Authorization-I/1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx +++ b/2-Authorization-I/1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx @@ -36,7 +36,7 @@ const useFetchWithMsal = (request, endpoint) => { account: account, claims: claims, }); - + /** * Execute a fetch request with Graph SDK * @param {String} endpoint @@ -55,7 +55,7 @@ const useFetchWithMsal = (request, endpoint) => { .api(endpoint) .responseType(ResponseType.RAW) .get(); - const responseHasClaimsChallenge = await handleClaimsChallenge(graphResponse); + const responseHasClaimsChallenge = await handleClaimsChallenge(graphResponse, endpoint, account); if (responseHasClaimsChallenge && responseHasClaimsChallenge.error === 'claims_challenge_occurred') { throw responseHasClaimsChallenge.error; } else { From d381babd370febec3326b59186d1183142d073e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fan=20Eri=C5=9Fen?= Date: Wed, 8 Mar 2023 20:25:58 -0800 Subject: [PATCH 3/3] minor polish --- .../1-call-graph/README-incremental.md | 161 ++++++------------ 2-Authorization-I/1-call-graph/README.md | 109 ++++-------- .../1-call-graph/SPA/src/fetch.js | 42 ----- ...FetchWithMsal.jsx => useGraphWithMsal.jsx} | 37 ++-- .../1-call-graph/SPA/src/pages/Contacts.jsx | 8 +- .../1-call-graph/SPA/src/pages/Profile.jsx | 8 +- .../1-call-graph/SPA/src/utils/claimUtils.js | 53 +++++- 7 files changed, 162 insertions(+), 256 deletions(-) delete mode 100644 2-Authorization-I/1-call-graph/SPA/src/fetch.js rename 2-Authorization-I/1-call-graph/SPA/src/hooks/{useFetchWithMsal.jsx => useGraphWithMsal.jsx} (69%) diff --git a/2-Authorization-I/1-call-graph/README-incremental.md b/2-Authorization-I/1-call-graph/README-incremental.md index 06680bad..54364937 100644 --- a/2-Authorization-I/1-call-graph/README-incremental.md +++ b/2-Authorization-I/1-call-graph/README-incremental.md @@ -29,14 +29,14 @@ In this chapter we will extend our React single-page application (SPA) by making | File/folder | Description | |-------------------------------------|----------------------------------------------------------------------------| -| `App.jsx` | Main application logic resides here. | -| `fetch.jsx` | Provides a helper method for making fetch calls using bearer token scheme. | -| `graph.jsx` | Instantiates Graph SDK client using a custom authentication provider. | -| `authConfig.js` | Contains authentication configuration parameters. | -| `pages/Home.jsx` | Contains a table with ID token claims and description | -| `pages/Profile.jsx` | Calls Microsoft Graph `/me` endpoint with Graph SDK. | -| `pages/Contacts.jsx` | Calls Microsoft Graph `/me/contacts` endpoint with Graph SDK. | -| `components/AccountPicker.jsx` | Contains logic to handle multiple `account` selection with MSAL.js | +| `App.jsx` | Main application logic resides here. | +| `graph.jsx` | Instantiates Graph SDK client using MSAL as authentication provider. | +| `authConfig.js` | Contains authentication configuration parameters. | +| `pages/Home.jsx` | Contains a table with ID token claims and description | +| `pages/Profile.jsx` | Calls Microsoft Graph `/me` by executing `useGraphWithMsal` custom hook. | +| `pages/Contacts.jsx` | Calls Microsoft Graph `/me/contacts` by executing `useGraphWithMsal` custom hook. | +| `components/AccountPicker.jsx` | Contains logic to handle multiple `account` selection with MSAL.js | +| `hooks/useGraphWithMsal.jsx` | Contains token acquisition logic to call Microsoft Graph endpoints with Graph SDK. | ## Setup the sample @@ -240,95 +240,68 @@ This sample app declares that it's CAE-capable by adding the `clientCapabilities #### 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 `useMsalAuthentication` hook and provide the claims challenge as a parameter in the token request. This is shown in [fetch.js](./SPA/src/fetch.js), where we handle the response from the Microsoft Graph API with the `handleClaimsChallenge` method: +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 `useMsalAuthentication` hook and provide the claims challenge as a parameter in the token request. This is shown in the [useGraphWithMsal](./SPA/src/hooks/useGraphWithMsal.jsx) custom hook: ```javascript - const handleClaimsChallenge = async (response) => { - if (response.status === 200) { - return response.json(); - } else if (response.status === 401) { - if (response.headers.get('www-authenticate')) { - const account = msalInstance.getActiveAccount(); - 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, `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}`); - return { error: 'claims_challenge_occurred', payload: claimsChallenge }; - } - - 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 `useMsalAuthentication` hook, fetch the claims challenge from the browser's localStorage, and pass it to the `useMsalAuthentication` hook in the request parameter. - -```javascript -export const Profile = () => { +const useGraphWithMsal = (request, endpoint) => { + const [error, setError] = useState(null); const { instance } = useMsal(); + const account = instance.getActiveAccount(); - const [graphData, setGraphData] = useState(null); - const resource = new URL(protectedResources.graphMe.endpoint).hostname; + const resource = new URL(endpoint).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"]}}} - const request = { - scopes: protectedResources.graphMe.scopes, + ? + window.atob(getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}`)) + : + undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}} + + const { result, login, error: msalError } = useMsalAuthentication(InteractionType.Popup, { + ...request, + redirectUri: '/redirect.html', account: account, claims: claims, - }; + }); - const { login, result, error } = useMsalAuthentication(InteractionType.Popup, request); - useEffect(() => { - if (!!graphData) { + /** + * Execute a fetch request with Graph SDK + * @param {String} endpoint + * @returns JSON response + */ + const execute = async (endpoint) => { + if (msalError) { + setError(msalError); return; } - if (!!error) { - // in case popup is blocked, use redirect instead - if (error.errorCode === 'popup_window_error' || error.errorCode === 'empty_window_error') { - login(InteractionType.Redirect, request); - } + if (result) { + let accessToken = result.accessToken; - console.log(error); - return; - } + try { + const graphResponse = await getGraphClient(accessToken) + .api(endpoint) + .responseType(ResponseType.RAW) + .get(); - if (result) { - getGraphClient(result.accessToken) - .api('/me') - .responseType(ResponseType.RAW) - .get() - .then((response) => { - return handleClaimsChallenge(response, protectedResources.graphMe.endpoint); - }) - .then((response) => { - if (response && response.error === 'claims_challenge_occurred') throw response.error; - setGraphData(response); - }) - .catch((error) => { - if (error === 'claims_challenge_occurred') { - login(InteractionType.Redirect, request); - } - console.log(error); - }); + const responseHasClaimsChallenge = await handleClaimsChallenge(graphResponse, endpoint, account); + return responseHasClaimsChallenge; + + } catch (error) { + if (error.name === 'ClaimsChallengeAuthError') { + login(InteractionType.Redirect, request); + } else { + setError(error); + } + } } - }, [graphData, result, error, login]); + }; - if (error) { - return
Error: {error.message}
; - } - return <>{graphData ? : null}; + return { + error, + result: result, + execute: useCallback(execute, [result, msalError]), + }; }; ``` @@ -340,7 +313,7 @@ For more details on what's inside the access token, clients should use the token ### Calling the Microsoft Graph API -[Microsoft Graph JavaScript SDK](https://github.com/microsoftgraph/msgraph-sdk-javascript) provides various utility methods to query the Graph API. While the SDK has a default authentication provider that can be used in basic scenarios, it can also be extended to use with a custom authentication provider such as MSAL. To do so, we will initialize the Graph SDK client with an [authProvider function](https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CreatingClientInstance.md#2-create-with-options). In this case, user has to provide their own implementation for getting and refreshing accessToken. A callback will be passed into this `authProvider` function, accessToken or error needs to be passed in to that callback +[Microsoft Graph JavaScript SDK](https://github.com/microsoftgraph/msgraph-sdk-javascript) provides various utility methods to query the Graph API. While the SDK has a default authentication provider that can be used in basic scenarios, it can also be extended to use with a custom authentication provider such as MSAL. To do so, we will initialize the Graph SDK client with an [authProvider function](https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CreatingClientInstance.md#2-create-with-options). In this case, user has to provide their own implementation for getting and refreshing access tokens. ```javascript export const getGraphClient = (accessToken) => { @@ -356,29 +329,7 @@ For more details on what's inside the access token, clients should use the token }; ``` -See [graph.js](./SPA/src/graph.js). The Graph client then can be used in your components as shown below: - -```javascript -if (result) { - getGraphClient(result.accessToken) - .api('/me') - .responseType(ResponseType.RAW) - .get() - .then((response) => { - return handleClaimsChallenge(response, protectedResources.graphMe.endpoint); - }) - .then((response) => { - if (response && response.error === 'claims_challenge_occurred') throw response.error; - setGraphData(response); - }) - .catch((error) => { - if (error === 'claims_challenge_occurred') { - login(InteractionType.Redirect, request); - } - console.log(error); - }); - } -``` +The Graph client then can be used in your application as shown in the [useGraphWithMsal](./SPA/src/hooks/useGraphWithMsal.jsx) custom hook. ### Working with React routes diff --git a/2-Authorization-I/1-call-graph/README.md b/2-Authorization-I/1-call-graph/README.md index 03f54a14..1cf45377 100644 --- a/2-Authorization-I/1-call-graph/README.md +++ b/2-Authorization-I/1-call-graph/README.md @@ -47,14 +47,13 @@ Here you'll learn how to [sign-in](https://docs.microsoft.com/azure/active-direc | File/folder | Description | |-------------------------------------|---------------------------------------------------------------------------------- | | `App.jsx` | Main application logic resides here. | -| `fetch.jsx` | Provides a helper method for making fetch calls using bearer token scheme. | -| `graph.jsx` | Instantiates Graph SDK client using a custom authentication provider. | +| `graph.jsx` | Instantiates Graph SDK client using MSAL as authentication provider. | | `authConfig.js` | Contains authentication configuration parameters. | | `pages/Home.jsx` | Contains a table with ID token claims and description | -| `pages/Profile.jsx` | Calls Microsoft Graph `/me` by executing `useFetchWithMsal` custom hook. | -| `pages/Contacts.jsx` | Calls Microsoft Graph `/me/contacts` by executing `useFetchWithMsal` custom hook. | +| `pages/Profile.jsx` | Calls Microsoft Graph `/me` by executing `useGraphWithMsal` custom hook. | +| `pages/Contacts.jsx` | Calls Microsoft Graph `/me/contacts` by executing `useGraphWithMsal` custom hook. | | `components/AccountPicker.jsx` | Contains logic to handle multiple `account` selection with MSAL.js | -| `hooks/useFetchWithMsal.jsx` | Contains token acquisition logic to call Microsoft Graph endpoints with Graph SDK. | +| `hooks/useGraphWithMsal.jsx` | Contains token acquisition logic to call Microsoft Graph endpoints with Graph SDK. | ## Prerequisites @@ -335,56 +334,24 @@ This sample app declares that it's CAE-capable by adding the `clientCapabilities #### 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 `useMsalAuthentication` hook and provide the claims challenge as a parameter in the token request. This is shown in [fetch.js](./SPA/src/fetch.js), where we handle the response from the Microsoft Graph API with the `handleClaimsChallenge` method: +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 `useMsalAuthentication` hook and provide the claims challenge as a parameter in the token request. This is shown in the [useGraphWithMsal](./SPA/src/hooks/useGraphWithMsal.jsx) custom hook: ```javascript - const handleClaimsChallenge = async (response) => { - if (response.status === 200) { - return response.json(); - } else if (response.status === 401) { - if (response.headers.get('www-authenticate')) { - const account = msalInstance.getActiveAccount(); - 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, `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}`); - return { error: 'claims_challenge_occurred', payload: claimsChallenge }; - } - - 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 `useMsalAuthentication` hook, fetch the claims challenge from the browser's localStorage, and pass it to the `useMsalAuthentication` hook in the request parameter as shown in the [useFetchWithMsal](./SPA/src/hooks/useFetchWithMsal.jsx) custom hook: . - -```javascript -const useFetchWithMsal = (request, endpoint) => { +const useGraphWithMsal = (request, endpoint) => { const [error, setError] = useState(null); const { instance } = useMsal(); + const account = instance.getActiveAccount(); const resource = new URL(endpoint).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"]}}} - - const { - result, - login, - error: msalError, - } = useMsalAuthentication(InteractionType.Popup, { + ? + window.atob(getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}`)) + : + undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}} + + const { result, login, error: msalError } = useMsalAuthentication(InteractionType.Popup, { ...request, redirectUri: '/redirect.html', account: account, @@ -401,9 +368,26 @@ const useFetchWithMsal = (request, endpoint) => { setError(msalError); return; } + if (result) { let accessToken = result.accessToken; - // do something with the access token + + try { + const graphResponse = await getGraphClient(accessToken) + .api(endpoint) + .responseType(ResponseType.RAW) + .get(); + + const responseHasClaimsChallenge = await handleClaimsChallenge(graphResponse, endpoint, account); + return responseHasClaimsChallenge; + + } catch (error) { + if (error.name === 'ClaimsChallengeAuthError') { + login(InteractionType.Redirect, request); + } else { + setError(error); + } + } } }; @@ -413,8 +397,6 @@ const useFetchWithMsal = (request, endpoint) => { execute: useCallback(execute, [result, msalError]), }; }; - -export default useFetchWithMsal; ``` ### Access token validation @@ -425,7 +407,7 @@ For more details on what's inside the access token, clients should use the token ### Calling the Microsoft Graph API -[Microsoft Graph JavaScript SDK](https://github.com/microsoftgraph/msgraph-sdk-javascript) provides various utility methods to query the Graph API. While the SDK has a default authentication provider that can be used in basic scenarios, it can also be extended to use with a custom authentication provider such as MSAL. To do so, we will initialize the Graph SDK client with an [authProvider function](https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CreatingClientInstance.md#2-create-with-options). In this case, user has to provide their own implementation for getting and refreshing accessToken. A callback will be passed into this `authProvider` function, accessToken or error needs to be passed in to that callback. +[Microsoft Graph JavaScript SDK](https://github.com/microsoftgraph/msgraph-sdk-javascript) provides various utility methods to query the Graph API. While the SDK has a default authentication provider that can be used in basic scenarios, it can also be extended to use with a custom authentication provider such as MSAL. To do so, we will initialize the Graph SDK client with an [authProvider function](https://github.com/microsoftgraph/msgraph-sdk-javascript/blob/dev/docs/CreatingClientInstance.md#2-create-with-options). In this case, user has to provide their own implementation for getting and refreshing access tokens. ```javascript export const getGraphClient = (accessToken) => { @@ -441,32 +423,7 @@ For more details on what's inside the access token, clients should use the token }; ``` -See [graph.js](./SPA/src/graph.js). The Graph client then can be used in your application as shown in the [useFetchWithMsal](./SPA/src/hooks/useFetchWithMsal.jsx) custom hook: - -```javascript - if (result) { - let accessToken = result.accessToken; - - try { - const graphResponse = await getGraphClient(accessToken) - .api(endpoint) - .responseType(ResponseType.RAW) - .get(); - const responseHasClaimsChallenge = await handleClaimsChallenge(graphResponse); - if (responseHasClaimsChallenge && responseHasClaimsChallenge.error === 'claims_challenge_occurred') { - throw responseHasClaimsChallenge.error; - } else { - return responseHasClaimsChallenge; - } - } catch (error) { - if (error === 'claims_challenge_occurred') { - login(InteractionType.Redirect, request); - } else { - setError(error); - } - } - } -``` +The Graph client then can be used in your application as shown in the [useGraphWithMsal](./SPA/src/hooks/useGraphWithMsal.jsx) custom hook. ### Working with React routes diff --git a/2-Authorization-I/1-call-graph/SPA/src/fetch.js b/2-Authorization-I/1-call-graph/SPA/src/fetch.js deleted file mode 100644 index c46cdd9c..00000000 --- a/2-Authorization-I/1-call-graph/SPA/src/fetch.js +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Copyright (c) Microsoft Corporation. All rights reserved. - * Licensed under the MIT License. - */ - -import { msalConfig } from './authConfig'; -import { addClaimsToStorage } from './utils/storageUtils'; -import { parseChallenges } from './utils/claimUtils'; - -/** - * 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 - */ -export const handleClaimsChallenge = async (response, apiEndpoint, account) => { - 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}`); - } -}; diff --git a/2-Authorization-I/1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx b/2-Authorization-I/1-call-graph/SPA/src/hooks/useGraphWithMsal.jsx similarity index 69% rename from 2-Authorization-I/1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx rename to 2-Authorization-I/1-call-graph/SPA/src/hooks/useGraphWithMsal.jsx index a429e05b..0d4bd446 100644 --- a/2-Authorization-I/1-call-graph/SPA/src/hooks/useFetchWithMsal.jsx +++ b/2-Authorization-I/1-call-graph/SPA/src/hooks/useGraphWithMsal.jsx @@ -2,10 +2,11 @@ import { useState, useCallback } from 'react'; import { useMsalAuthentication, useMsal } from '@azure/msal-react'; import { InteractionType } from '@azure/msal-browser'; import { ResponseType } from '@microsoft/microsoft-graph-client'; + +import { msalConfig } from '../authConfig'; import { getGraphClient } from '../graph'; import { getClaimsFromStorage } from '../utils/storageUtils'; -import { msalConfig } from '../authConfig'; -import { handleClaimsChallenge } from '../fetch'; +import { handleClaimsChallenge } from '../utils/claimUtils'; /** * Custom hook to call a Graph API using Graph SDK @@ -13,30 +14,27 @@ import { handleClaimsChallenge } from '../fetch'; * @param {String} endpoint * @returns */ -const useFetchWithMsal = (request, endpoint) => { +const useGraphWithMsal = (request, endpoint) => { const [error, setError] = useState(null); const { instance } = useMsal(); + const account = instance.getActiveAccount(); const resource = new URL(endpoint).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"]}}} + ? + window.atob(getClaimsFromStorage(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}`)) + : + undefined; // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}} - const { - result, - login, - error: msalError, - } = useMsalAuthentication(InteractionType.Popup, { + const { result, login, error: msalError } = useMsalAuthentication(InteractionType.Popup, { ...request, redirectUri: '/redirect.html', account: account, claims: claims, }); - + /** * Execute a fetch request with Graph SDK * @param {String} endpoint @@ -47,6 +45,7 @@ const useFetchWithMsal = (request, endpoint) => { setError(msalError); return; } + if (result) { let accessToken = result.accessToken; @@ -55,14 +54,12 @@ const useFetchWithMsal = (request, endpoint) => { .api(endpoint) .responseType(ResponseType.RAW) .get(); + const responseHasClaimsChallenge = await handleClaimsChallenge(graphResponse, endpoint, account); - if (responseHasClaimsChallenge && responseHasClaimsChallenge.error === 'claims_challenge_occurred') { - throw responseHasClaimsChallenge.error; - } else { - return responseHasClaimsChallenge; - } + return responseHasClaimsChallenge; + } catch (error) { - if (error === 'claims_challenge_occurred') { + if (error.name === 'ClaimsChallengeAuthError') { login(InteractionType.Redirect, request); } else { setError(error); @@ -78,4 +75,4 @@ const useFetchWithMsal = (request, endpoint) => { }; }; -export default useFetchWithMsal; \ No newline at end of file +export default useGraphWithMsal; \ No newline at end of file diff --git a/2-Authorization-I/1-call-graph/SPA/src/pages/Contacts.jsx b/2-Authorization-I/1-call-graph/SPA/src/pages/Contacts.jsx index 6a9a832a..bf2314f4 100644 --- a/2-Authorization-I/1-call-graph/SPA/src/pages/Contacts.jsx +++ b/2-Authorization-I/1-call-graph/SPA/src/pages/Contacts.jsx @@ -2,14 +2,14 @@ import { useEffect, useState } from 'react'; import { ContactsData } from '../components/DataDisplay'; import { protectedResources } from '../authConfig'; -import useFetchWithMsal from '../hooks/useFetchWithMsal'; +import useGraphWithMsal from '../hooks/useGraphWithMsal'; export const Contacts = () => { const [graphData, setGraphData] = useState(null); - const request = { + + const { error, result, execute } = useGraphWithMsal({ scopes: protectedResources.graphContacts.scopes, - }; - const { error, result, execute } = useFetchWithMsal(request, protectedResources.graphContacts.endpoint); + }, protectedResources.graphContacts.endpoint); useEffect(() => { if (!!graphData) { diff --git a/2-Authorization-I/1-call-graph/SPA/src/pages/Profile.jsx b/2-Authorization-I/1-call-graph/SPA/src/pages/Profile.jsx index d7233d93..8d107300 100644 --- a/2-Authorization-I/1-call-graph/SPA/src/pages/Profile.jsx +++ b/2-Authorization-I/1-call-graph/SPA/src/pages/Profile.jsx @@ -2,14 +2,14 @@ import { useEffect, useState } from 'react'; import { ProfileData } from '../components/DataDisplay'; import { protectedResources } from '../authConfig'; -import useFetchWithMsal from '../hooks/useFetchWithMsal' +import useGraphWithMsal from '../hooks/useGraphWithMsal' export const Profile = () => { const [graphData, setGraphData] = useState(null); - const request = { + + const { error, execute, result } = useGraphWithMsal({ scopes: protectedResources.graphMe.scopes, - }; - const { error, execute, result } = useFetchWithMsal(request, protectedResources.graphMe.endpoint); + }, protectedResources.graphMe.endpoint); useEffect(() => { if (!!graphData) { diff --git a/2-Authorization-I/1-call-graph/SPA/src/utils/claimUtils.js b/2-Authorization-I/1-call-graph/SPA/src/utils/claimUtils.js index 5c655163..e6a1e2ef 100644 --- a/2-Authorization-I/1-call-graph/SPA/src/utils/claimUtils.js +++ b/2-Authorization-I/1-call-graph/SPA/src/utils/claimUtils.js @@ -1,3 +1,6 @@ +import { AccountInfo } from '@azure/msal-browser'; +import { addClaimsToStorage } from './storageUtils'; + /** * Populate claims table with appropriate description * @param {Object} claims ID token claims @@ -224,10 +227,10 @@ const changeDateFormat = (date) => { /** - * This method parses WWW-Authenticate authentication headers - * @param header - * @return {Object} challengeMap - */ + * This method parses WWW-Authenticate authentication headers + * @param header + * @return {Object} challengeMap + */ export const parseChallenges = (header) => { const schemeSeparator = header.indexOf(' '); const challenges = header.substring(schemeSeparator + 1).split(','); @@ -239,6 +242,46 @@ export const parseChallenges = (header) => { }); return challengeMap; -} +}; + +/** + * 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 + * @param {string} apiEndpoint + * @param {AccountInfo} account + * @returns response + */ +export const handleClaimsChallenge = async (response, apiEndpoint, account) => { + if (response.status === 200) { + return response.json(); + } + + 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}` + ); + const err = new Error("A claims challenge has occurred"); + err.name = 'ClaimsChallengeAuthError'; + throw err; + } + + throw new Error(`Unauthorized: ${response.status}`); + } else { + throw new Error(`Something went wrong with the request: ${response.status}`); + } +};