From 9dd070d5e032e8d70aebab3d960594925767db0f Mon Sep 17 00:00:00 2001 From: salman90 Date: Sun, 29 Jan 2023 09:40:34 -0800 Subject: [PATCH 1/4] support CA and CAE --- .../API/controllers/profileController.js | 19 ++++++-- .../1-call-api-obo/API/util/claimUtils.js | 48 +++++++++++++++++++ .../SPA/src/components/NavigationBar.jsx | 39 ++++++++------- .../1-call-api-obo/SPA/src/pages/Profile.jsx | 47 ++++++++++++++++-- .../SPA/src/utils/storageUtils.js | 28 +++++++++++ 5 files changed, 155 insertions(+), 26 deletions(-) create mode 100644 6-AdvancedScenarios/1-call-api-obo/API/util/claimUtils.js create mode 100644 6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/storageUtils.js diff --git a/6-AdvancedScenarios/1-call-api-obo/API/controllers/profileController.js b/6-AdvancedScenarios/1-call-api-obo/API/controllers/profileController.js index 2892ef9d..a2e36134 100644 --- a/6-AdvancedScenarios/1-call-api-obo/API/controllers/profileController.js +++ b/6-AdvancedScenarios/1-call-api-obo/API/controllers/profileController.js @@ -1,6 +1,7 @@ const { ResponseType } = require('@microsoft/microsoft-graph-client'); const { getOboToken } = require('../auth/onBehalfOfClient'); const { getGraphClient } = require('../util/graphClient'); +const msal = require('@azure/msal-node'); const authConfig = require('../authConfig'); @@ -8,6 +9,7 @@ const { isAppOnlyToken, hasRequiredDelegatedPermissions, } = require('../auth/permissionUtils'); +const { handleClaimsChallenge } = require("../util/claimUtils"); exports.getProfile = async (req, res, next) => { if (isAppOnlyToken(req.authInfo)) { @@ -24,11 +26,20 @@ exports.getProfile = async (req, res, next) => { .api('/me') .responseType(ResponseType.RAW) .get(); - - const response = await graphResponse.json(); - res.json(response); + + const graphData = await handleClaimsChallenge(graphResponse); + if (graphData && graphData.errorMessage === 'claims_challenge_occurred') { + throw graphData; + } + res.status(200).json(graphData); } catch (error) { - next(error); + if(error instanceof msal.InteractionRequiredAuthError) { + res.status(403).json(error); + } else if (error.message === "claims_challenge_occurred") { + res.status(401).json(error); + } else { + next(error); + } } } else { next(new Error('User does not have the required permissions')); diff --git a/6-AdvancedScenarios/1-call-api-obo/API/util/claimUtils.js b/6-AdvancedScenarios/1-call-api-obo/API/util/claimUtils.js new file mode 100644 index 00000000..b2a99dab --- /dev/null +++ b/6-AdvancedScenarios/1-call-api-obo/API/util/claimUtils.js @@ -0,0 +1,48 @@ +/** + * 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 (response) => { + if (response.status === 200) { + return await response.json(); + } else if (response.status === 401) { + if (response.headers.get('WWW-Authenticate')) { + const authenticateHeader = response.headers.get('WWW-Authenticate'); + const claimsChallenge = parseChallenges(authenticateHeader); + if (claimsChallenge && claimsChallenge.claims) { + return { + errorMessage: '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()] = decodeURI(value.replace(/['"]+/g, '')); + }); + + return challengeMap; +}; + +module.exports = { + handleClaimsChallenge: handleClaimsChallenge, +}; diff --git a/6-AdvancedScenarios/1-call-api-obo/SPA/src/components/NavigationBar.jsx b/6-AdvancedScenarios/1-call-api-obo/SPA/src/components/NavigationBar.jsx index 3aa389bf..85b3b838 100644 --- a/6-AdvancedScenarios/1-call-api-obo/SPA/src/components/NavigationBar.jsx +++ b/6-AdvancedScenarios/1-call-api-obo/SPA/src/components/NavigationBar.jsx @@ -1,6 +1,7 @@ -import { AuthenticatedTemplate, UnauthenticatedTemplate, useMsal } from "@azure/msal-react"; -import { Nav, Navbar, Dropdown, DropdownButton } from "react-bootstrap"; -import { loginRequest } from "../authConfig"; +import { AuthenticatedTemplate, UnauthenticatedTemplate, useMsal } from '@azure/msal-react'; +import { Nav, Navbar, Dropdown, DropdownButton } from 'react-bootstrap'; +import { loginRequest } from '../authConfig'; +import { clearStorage } from '../utils/storageUtils'; export const NavigationBar = () => { const { instance } = useMsal(); @@ -13,31 +14,35 @@ export const NavigationBar = () => { const handleLoginPopup = () => { /** - * When using popup and silent APIs, we recommend setting the redirectUri to a blank page or a page + * When using popup and silent APIs, we recommend setting the redirectUri to a blank page or a page * that does not implement MSAL. Keep in mind that all redirect routes must be registered with the application - * For more information, please follow this link: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/login-user.md#redirecturi-considerations + * For more information, please follow this link: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/login-user.md#redirecturi-considerations */ - instance.loginPopup({ - ...loginRequest, - redirectUri: "/redirect.html" - }).catch((error) => console.log(error)); + instance + .loginPopup({ + ...loginRequest, + redirectUri: '/redirect.html', + }) + .catch((error) => console.log(error)); }; const handleLoginRedirect = () => { - instance.loginRedirect(loginRequest).catch((error) => console.log(error)); + instance.loginRedirect(loginRequest).catch((error) => console.log(error)); }; const handleLogoutPopup = () => { - instance + clearStorage(activeAccount); + instance .logoutPopup({ - mainWindowRedirectUri: '/', // redirects the top level app after logout - account: instance.getActiveAccount(), - }) - .catch((error) => console.log(error)); + mainWindowRedirectUri: '/', // redirects the top level app after logout + account: instance.getActiveAccount(), + }) + .catch((error) => console.log(error)); }; const handleLogoutRedirect = () => { - instance.logoutRedirect().catch((error) => console.log(error)); + clearStorage(activeAccount); + instance.logoutRedirect().catch((error) => console.log(error)); }; /** @@ -54,7 +59,7 @@ export const NavigationBar = () => { Profile - +
{ /** @@ -14,12 +16,17 @@ const ProfileContent = () => { * that tells you what msal is currently doing. For more, visit: * https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-react/docs/hooks.md */ - const { instance, } = useMsal(); + const { instance } = useMsal(); const account = instance.getActiveAccount(); const [graphData, setGraphData] = useState(null); + const resource = new URL(protectedResources.apiHello.endpoint).hostname; + const request = { scopes: protectedResources.apiHello.scopes, account: account, + 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 { login, result, error } = useMsalAuthentication(InteractionType.Popup, { @@ -45,12 +52,42 @@ const ProfileContent = () => { if (result) { callApiWithToken(result.accessToken, protectedResources.apiHello.endpoint) .then((response) => { - setGraphData(response) + if ( + response && + (response.name === 'InteractionRequiredAuthError' || + response.errorMessage === 'claims_challenge_occurred') + ) { + throw response; + } + setGraphData(response); }) .catch((error) => { - console.log(error) - }) - + if ( + error && + error.errorMessage.includes('50076') && + error.name === 'InteractionRequiredAuthError' + ) { + /** + * This method stores the claims 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( + window.btoa(error.claims), + `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}` + ); + login(InteractionType.Redirect, request); + } else if (error.errorMessage === 'claims_challenge_occurred') { + addClaimsToStorage( + error.payload, + `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}` + ); + login(InteractionType.Redirect, request); + } else { + console.log(error); + } + }); } // eslint-disable-next-line }, [graphData, result, error, login]); diff --git a/6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/storageUtils.js b/6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/storageUtils.js new file mode 100644 index 00000000..f35e51ec --- /dev/null +++ b/6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/storageUtils.js @@ -0,0 +1,28 @@ +import { msalConfig } from "../authConfig"; + +/** + * This method stores the claims to the sessionStorage in the browser to be used when acquiring a token + * @param {String} claimsChallenge + */ +export const addClaimsToStorage = (claims, claimsChallengeId) => { + sessionStorage.setItem(claimsChallengeId, claims); +}; + +/** + * This method return the claims from sessionStorage in the browser + * @param {String} claimsChallengeId + * @returns + */ +export const getClaimsFromStorage = (claimsChallengeId) => { + return sessionStorage.getItem(claimsChallengeId); +}; + +/** + * This method clears localStorage of any claims challenge entry + * @param {Object} account + */ +export const clearStorage = (account) => { + for (var key in sessionStorage) { + if (key.startsWith(`cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}`)) sessionStorage.removeItem(key); + } +}; \ No newline at end of file From 005748ff1648e5bd288ef02541be6ad791f4fe47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fan=20Eri=C5=9Fen?= Date: Thu, 2 Feb 2023 11:00:07 -0800 Subject: [PATCH 2/4] update claims challenge response --- 6-AdvancedScenarios/1-call-api-obo/API/app.js | 21 ++++--- .../1-call-api-obo/API/auth/claimUtils.js | 36 ++++++++++++ .../API/auth/permissionUtils.js | 2 +- .../API/controllers/profileController.js | 56 +++++++++++++------ .../1-call-api-obo/API/util/claimUtils.js | 48 ---------------- .../API/{util => utils}/graphClient.js | 0 .../1-call-api-obo/SPA/package-lock.json | 21 +++++++ .../SPA/src/components/NavigationBar.jsx | 22 ++++---- .../1-call-api-obo/SPA/src/fetch.js | 50 ++++++++++++++++- .../1-call-api-obo/SPA/src/pages/Profile.jsx | 46 +++------------ .../SPA/src/utils/claimUtils.js | 19 +++++++ .../SPA/src/utils/storageUtils.js | 2 +- 12 files changed, 194 insertions(+), 129 deletions(-) create mode 100644 6-AdvancedScenarios/1-call-api-obo/API/auth/claimUtils.js delete mode 100644 6-AdvancedScenarios/1-call-api-obo/API/util/claimUtils.js rename 6-AdvancedScenarios/1-call-api-obo/API/{util => utils}/graphClient.js (100%) diff --git a/6-AdvancedScenarios/1-call-api-obo/API/app.js b/6-AdvancedScenarios/1-call-api-obo/API/app.js index ce9a842c..67ff8b68 100644 --- a/6-AdvancedScenarios/1-call-api-obo/API/app.js +++ b/6-AdvancedScenarios/1-call-api-obo/API/app.js @@ -26,22 +26,24 @@ const app = express(); * where an attacker can cause the application to crash or become unresponsive by issuing a large number of * requests at the same time. For more information, visit: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html */ - const limiter = rateLimit({ - windowMs: 15 * 60 * 1000, // 15 minutes - max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) - standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers - legacyHeaders: false, // Disable the `X-RateLimit-*` headers +const limiter = rateLimit({ + windowMs: 15 * 60 * 1000, // 15 minutes + max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes) + standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers + legacyHeaders: false, // Disable the `X-RateLimit-*` headers }); // Apply the rate limiting middleware to all requests -app.use(limiter) - +app.use(limiter); /** * Enable CORS middleware. In production, modify as to allow only designated origins and methods. * If you are using Azure App Service, we recommend removing the line below and configure CORS on the App Service itself. */ -app.use(cors()); +app.use(cors({ + origin: '*', + exposedHeaders: "WWW-Authenticate", +})); app.use(morgan('dev')); app.use(express.urlencoded({ extended: false })); @@ -58,7 +60,7 @@ const bearerStrategy = new passportAzureAd.BearerStrategy( loggingLevel: authConfig.settings.loggingLevel, loggingNoPII: authConfig.settings.loggingNoPII, }, - (req,token, done) => { + (req, token, done) => { /** * Below you can do extended token validation and check for additional claims, such as: * - check if the caller's tenant is in the allowed tenants list via the 'tid' claim (for multi-tenant applications) @@ -94,6 +96,7 @@ const bearerStrategy = new passportAzureAd.BearerStrategy( ); app.use(passport.initialize()); + passport.use(bearerStrategy); app.use( diff --git a/6-AdvancedScenarios/1-call-api-obo/API/auth/claimUtils.js b/6-AdvancedScenarios/1-call-api-obo/API/auth/claimUtils.js new file mode 100644 index 00000000..01f3ffc4 --- /dev/null +++ b/6-AdvancedScenarios/1-call-api-obo/API/auth/claimUtils.js @@ -0,0 +1,36 @@ + +const authConfig = require('../authConfig'); + +/** + * xms_cc claim in the access token indicates that the client app of user is capable of + * handling claims challenges. See for more: https://docs.microsoft.com/en-us/azure/active-directory/develop/claims-challenge#client-capabilities + * @param {Object} accessTokenClaims: + */ +const isClientCapableOfClaimsChallenge = (accessTokenClaims) => { + if (accessTokenClaims['xms_cc'] && accessTokenClaims['xms_cc'].includes('CP1')) { + return true; + } + + return false; +} + +/** + * Generates www-authenticate header and claims challenge for a given authentication context id. For more information, see: + * https://docs.microsoft.com/en-us/azure/active-directory/develop/claims-challenge#claims-challenge-header-format + */ +const generateClaimsChallenge = (claims) => { + const clientId = authConfig.credentials.clientID; + + // base64 encode the challenge object + const base64str = Buffer.from(JSON.stringify(claims)).toString('base64'); + const headers = ["WWW-Authenticate", "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/v2.0/authorize\", client_id=\"" + clientId + "\", error=\"insufficient_claims\", claims=\"" + base64str + "\""]; + + return { + headers + }; +} + +module.exports = { + isClientCapableOfClaimsChallenge, + generateClaimsChallenge +} \ No newline at end of file diff --git a/6-AdvancedScenarios/1-call-api-obo/API/auth/permissionUtils.js b/6-AdvancedScenarios/1-call-api-obo/API/auth/permissionUtils.js index 2199c281..35c88c65 100644 --- a/6-AdvancedScenarios/1-call-api-obo/API/auth/permissionUtils.js +++ b/6-AdvancedScenarios/1-call-api-obo/API/auth/permissionUtils.js @@ -45,5 +45,5 @@ const hasRequiredDelegatedPermissions = (accessTokenPayload, requiredPermission) module.exports = { isAppOnlyToken, - hasRequiredDelegatedPermissions, + hasRequiredDelegatedPermissions } diff --git a/6-AdvancedScenarios/1-call-api-obo/API/controllers/profileController.js b/6-AdvancedScenarios/1-call-api-obo/API/controllers/profileController.js index a2e36134..fde5cb88 100644 --- a/6-AdvancedScenarios/1-call-api-obo/API/controllers/profileController.js +++ b/6-AdvancedScenarios/1-call-api-obo/API/controllers/profileController.js @@ -1,16 +1,13 @@ +const msal = require('@azure/msal-node'); const { ResponseType } = require('@microsoft/microsoft-graph-client'); + const { getOboToken } = require('../auth/onBehalfOfClient'); -const { getGraphClient } = require('../util/graphClient'); -const msal = require('@azure/msal-node'); +const { getGraphClient } = require('../utils/graphClient'); +const { isAppOnlyToken, hasRequiredDelegatedPermissions } = require('../auth/permissionUtils'); +const { isClientCapableOfClaimsChallenge, generateClaimsChallenge } = require('../auth/claimUtils'); const authConfig = require('../authConfig'); -const { - isAppOnlyToken, - hasRequiredDelegatedPermissions, -} = require('../auth/permissionUtils'); -const { handleClaimsChallenge } = require("../util/claimUtils"); - exports.getProfile = async (req, res, next) => { if (isAppOnlyToken(req.authInfo)) { return next(new Error('This route requires a user token')); @@ -22,24 +19,47 @@ exports.getProfile = async (req, res, next) => { if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.profile.delegatedPermissions.scopes)) { try { const accessToken = await getOboToken(tokenValue); + const graphResponse = await getGraphClient(accessToken) .api('/me') .responseType(ResponseType.RAW) .get(); - - const graphData = await handleClaimsChallenge(graphResponse); - if (graphData && graphData.errorMessage === 'claims_challenge_occurred') { - throw graphData; + + if (graphResponse.status === 401) { + if (graphResponse.headers.get('WWW-Authenticate')) { + + if (isClientCapableOfClaimsChallenge(req.authInfo)) { + /** + * Append the WWW-Authenticate header from the Microsoft Graph response to the response to + * the client app. To learn more, visit: https://learn.microsoft.com/azure/active-directory/develop/app-resilience-continuous-access-evaluation + */ + return res.status(401) + .set('WWW-Authenticate', graphResponse.headers.get('WWW-Authenticate').toString()) + .json({ errorMessage: 'Continuous access evaluation resulted in claims challenge' }); + } + + return res.status(401).json({ errorMessage: 'Continuous access evaluation resulted in claims challenge but the client is not capable. Please enable client capabilities and try again' }); + } + + throw new Error('Unauthorized'); } + + const graphData = await graphResponse.json(); res.status(200).json(graphData); } catch (error) { - if(error instanceof msal.InteractionRequiredAuthError) { - res.status(403).json(error); - } else if (error.message === "claims_challenge_occurred") { - res.status(401).json(error); - } else { - next(error); + if (error instanceof msal.InteractionRequiredAuthError) { + if (error.claims) { + const claimsChallenge = generateClaimsChallenge(error.claims); + + return res.status(401) + .set(claimsChallenge.headers[0], claimsChallenge.headers[1]) + .json({ errorMessage: error.errorMessage }); + } + + return res.status(401).json(error); } + + next(error); } } else { next(new Error('User does not have the required permissions')); diff --git a/6-AdvancedScenarios/1-call-api-obo/API/util/claimUtils.js b/6-AdvancedScenarios/1-call-api-obo/API/util/claimUtils.js deleted file mode 100644 index b2a99dab..00000000 --- a/6-AdvancedScenarios/1-call-api-obo/API/util/claimUtils.js +++ /dev/null @@ -1,48 +0,0 @@ -/** - * 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 (response) => { - if (response.status === 200) { - return await response.json(); - } else if (response.status === 401) { - if (response.headers.get('WWW-Authenticate')) { - const authenticateHeader = response.headers.get('WWW-Authenticate'); - const claimsChallenge = parseChallenges(authenticateHeader); - if (claimsChallenge && claimsChallenge.claims) { - return { - errorMessage: '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()] = decodeURI(value.replace(/['"]+/g, '')); - }); - - return challengeMap; -}; - -module.exports = { - handleClaimsChallenge: handleClaimsChallenge, -}; diff --git a/6-AdvancedScenarios/1-call-api-obo/API/util/graphClient.js b/6-AdvancedScenarios/1-call-api-obo/API/utils/graphClient.js similarity index 100% rename from 6-AdvancedScenarios/1-call-api-obo/API/util/graphClient.js rename to 6-AdvancedScenarios/1-call-api-obo/API/utils/graphClient.js diff --git a/6-AdvancedScenarios/1-call-api-obo/SPA/package-lock.json b/6-AdvancedScenarios/1-call-api-obo/SPA/package-lock.json index b82bbf8c..36b95137 100644 --- a/6-AdvancedScenarios/1-call-api-obo/SPA/package-lock.json +++ b/6-AdvancedScenarios/1-call-api-obo/SPA/package-lock.json @@ -8997,6 +8997,20 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "node_modules/fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", @@ -25178,6 +25192,13 @@ "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", "dev": true }, + "fsevents": { + "version": "2.3.2", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.2.tgz", + "integrity": "sha512-xiqMQR4xAeHTuB9uWm+fFRcIOgKBMiOBP+eXiyT7jsgVCq1bkVygt00oASowB7EdtpOHaaPgKt812P9ab+DDKA==", + "dev": true, + "optional": true + }, "function-bind": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz", diff --git a/6-AdvancedScenarios/1-call-api-obo/SPA/src/components/NavigationBar.jsx b/6-AdvancedScenarios/1-call-api-obo/SPA/src/components/NavigationBar.jsx index 85b3b838..31381398 100644 --- a/6-AdvancedScenarios/1-call-api-obo/SPA/src/components/NavigationBar.jsx +++ b/6-AdvancedScenarios/1-call-api-obo/SPA/src/components/NavigationBar.jsx @@ -18,12 +18,10 @@ export const NavigationBar = () => { * that does not implement MSAL. Keep in mind that all redirect routes must be registered with the application * For more information, please follow this link: https://github.com/AzureAD/microsoft-authentication-library-for-js/blob/dev/lib/msal-browser/docs/login-user.md#redirecturi-considerations */ - instance - .loginPopup({ - ...loginRequest, - redirectUri: '/redirect.html', - }) - .catch((error) => console.log(error)); + instance.loginPopup({ + ...loginRequest, + redirectUri: '/redirect.html', + }).catch((error) => console.log(error)); }; const handleLoginRedirect = () => { @@ -32,16 +30,16 @@ export const NavigationBar = () => { const handleLogoutPopup = () => { clearStorage(activeAccount); - instance - .logoutPopup({ - mainWindowRedirectUri: '/', // redirects the top level app after logout - account: instance.getActiveAccount(), - }) - .catch((error) => console.log(error)); + + instance.logoutPopup({ + mainWindowRedirectUri: '/', // redirects the top level app after logout + account: instance.getActiveAccount(), + }).catch((error) => console.log(error)); }; const handleLogoutRedirect = () => { clearStorage(activeAccount); + instance.logoutRedirect().catch((error) => console.log(error)); }; diff --git a/6-AdvancedScenarios/1-call-api-obo/SPA/src/fetch.js b/6-AdvancedScenarios/1-call-api-obo/SPA/src/fetch.js index e45958ce..88d05940 100644 --- a/6-AdvancedScenarios/1-call-api-obo/SPA/src/fetch.js +++ b/6-AdvancedScenarios/1-call-api-obo/SPA/src/fetch.js @@ -3,7 +3,17 @@ * Licensed under the MIT License. */ -export const callApiWithToken = async (accessToken, apiEndpoint) => { +import { msalConfig } from './authConfig'; +import { addClaimsToStorage } from './utils/storageUtils'; +import { parseChallenges } from './utils/claimUtils'; + +/** + * Makes a fetch call to the API endpoint with the access token in the Authorization header + * @param {string} accessToken + * @param {string} apiEndpoint + * @returns + */ +export const callApiWithToken = async (accessToken, apiEndpoint, account) => { const headers = new Headers(); const bearer = `Bearer ${accessToken}`; @@ -15,6 +25,40 @@ export const callApiWithToken = async (accessToken, apiEndpoint) => { }; const response = await fetch(apiEndpoint, options); - const responseJson = await response.json(); - return responseJson; + return handleClaimsChallenge(response, apiEndpoint, account); +}; + +/** + * 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( + `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${new URL(apiEndpoint).hostname}`, + claimsChallenge.claims, + ); + + throw new Error(`claims_challenge_occurred`); + } + + throw new Error(`Unauthorized: ${response.status}`); + } else { + throw new Error(`Something went wrong with the request: ${response.status}`); + } }; \ No newline at end of file diff --git a/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx b/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx index abf02df9..93fd7f71 100644 --- a/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx +++ b/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx @@ -3,11 +3,11 @@ import { useEffect, useState } from 'react'; import { useMsalAuthentication, useMsal } from '@azure/msal-react'; import { InteractionType } from '@azure/msal-browser'; -import { protectedResources } from '../authConfig'; +import { msalConfig, protectedResources } from '../authConfig'; +import { getClaimsFromStorage } from '../utils/storageUtils'; import { callApiWithToken } from '../fetch'; + import { ProfileData } from '../components/DataDisplay'; -import { addClaimsToStorage, getClaimsFromStorage } from '../utils/storageUtils'; -import { msalConfig } from '../authConfig'; const ProfileContent = () => { /** @@ -25,8 +25,8 @@ const ProfileContent = () => { scopes: protectedResources.apiHello.scopes, account: account, 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 { login, result, error } = useMsalAuthentication(InteractionType.Popup, { @@ -50,42 +50,14 @@ const ProfileContent = () => { } if (result) { - callApiWithToken(result.accessToken, protectedResources.apiHello.endpoint) - .then((response) => { - if ( - response && - (response.name === 'InteractionRequiredAuthError' || - response.errorMessage === 'claims_challenge_occurred') - ) { - throw response; - } - setGraphData(response); - }) + callApiWithToken(result.accessToken, protectedResources.apiHello.endpoint, account) + .then((response) => setGraphData(response)) .catch((error) => { - if ( - error && - error.errorMessage.includes('50076') && - error.name === 'InteractionRequiredAuthError' - ) { - /** - * This method stores the claims 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( - window.btoa(error.claims), - `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}` - ); - login(InteractionType.Redirect, request); - } else if (error.errorMessage === 'claims_challenge_occurred') { - addClaimsToStorage( - error.payload, - `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${resource}` - ); + if (error.errorMessage === 'claims_challenge_occurred') { login(InteractionType.Redirect, request); } else { console.log(error); + setGraphData(error) } }); } diff --git a/6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/claimUtils.js b/6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/claimUtils.js index f3425e5b..bf279009 100644 --- a/6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/claimUtils.js +++ b/6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/claimUtils.js @@ -1,3 +1,22 @@ +/** + * 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(', '); + const challengeMap = {}; + + challenges.forEach((challenge) => { + const [key, value] = challenge.split('='); + challengeMap[key.trim()] = window.decodeURI(value.replace(/(^"|"$)/g, '')); + }); + + return challengeMap; +} + + /** * Populate claims table with appropriate description * @param {Object} claims ID token claims diff --git a/6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/storageUtils.js b/6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/storageUtils.js index f35e51ec..19c1861f 100644 --- a/6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/storageUtils.js +++ b/6-AdvancedScenarios/1-call-api-obo/SPA/src/utils/storageUtils.js @@ -4,7 +4,7 @@ import { msalConfig } from "../authConfig"; * This method stores the claims to the sessionStorage in the browser to be used when acquiring a token * @param {String} claimsChallenge */ -export const addClaimsToStorage = (claims, claimsChallengeId) => { +export const addClaimsToStorage = (claimsChallengeId, claims) => { sessionStorage.setItem(claimsChallengeId, claims); }; From ee2aea99ed34da3006da21b6a4b635cc84dbdd1d Mon Sep 17 00:00:00 2001 From: salman90 Date: Mon, 6 Feb 2023 17:52:38 -0800 Subject: [PATCH 3/4] mior updates to CA --- 6-AdvancedScenarios/1-call-api-obo/API/auth/claimUtils.js | 3 ++- 6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx | 4 ++-- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/6-AdvancedScenarios/1-call-api-obo/API/auth/claimUtils.js b/6-AdvancedScenarios/1-call-api-obo/API/auth/claimUtils.js index 01f3ffc4..0bbe990c 100644 --- a/6-AdvancedScenarios/1-call-api-obo/API/auth/claimUtils.js +++ b/6-AdvancedScenarios/1-call-api-obo/API/auth/claimUtils.js @@ -22,7 +22,8 @@ const generateClaimsChallenge = (claims) => { const clientId = authConfig.credentials.clientID; // base64 encode the challenge object - const base64str = Buffer.from(JSON.stringify(claims)).toString('base64'); + let bufferObj = Buffer.from(claims, 'utf8'); + let base64str = bufferObj.toString('base64'); const headers = ["WWW-Authenticate", "Bearer realm=\"\", authorization_uri=\"https://login.microsoftonline.com/common/oauth2/v2.0/authorize\", client_id=\"" + clientId + "\", error=\"insufficient_claims\", claims=\"" + base64str + "\""]; return { diff --git a/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx b/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx index 93fd7f71..7cfb60f3 100644 --- a/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx +++ b/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx @@ -53,11 +53,11 @@ const ProfileContent = () => { callApiWithToken(result.accessToken, protectedResources.apiHello.endpoint, account) .then((response) => setGraphData(response)) .catch((error) => { - if (error.errorMessage === 'claims_challenge_occurred') { + if (error.message === 'claims_challenge_occurred') { login(InteractionType.Redirect, request); } else { console.log(error); - setGraphData(error) + setGraphData(error); } }); } From 2167cee8dd7195f68d4f6391828e8e3ffe059ef6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Do=C4=9Fan=20Eri=C5=9Fen?= Date: Tue, 7 Feb 2023 15:17:44 -0800 Subject: [PATCH 4/4] update readme --- .../1-call-api-obo/API/package-lock.json | 42 +++--- .../1-call-api-obo/API/package.json | 2 +- 6-AdvancedScenarios/1-call-api-obo/README.md | 142 ++++++++++++++++++ .../1-call-api-obo/SPA/src/pages/Profile.jsx | 11 +- 4 files changed, 169 insertions(+), 28 deletions(-) diff --git a/6-AdvancedScenarios/1-call-api-obo/API/package-lock.json b/6-AdvancedScenarios/1-call-api-obo/API/package-lock.json index 8ee1536f..00e0f6a1 100644 --- a/6-AdvancedScenarios/1-call-api-obo/API/package-lock.json +++ b/6-AdvancedScenarios/1-call-api-obo/API/package-lock.json @@ -9,7 +9,7 @@ "version": "1.0.0", "license": "ISC", "dependencies": { - "@azure/msal-node": "^1.14.6", + "@azure/msal-node": "^1.15.0", "@microsoft/microsoft-graph-client": "^3.0.4", "cors": "^2.8.5", "express": "^4.14.0", @@ -29,19 +29,19 @@ } }, "node_modules/@azure/msal-common": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-9.0.2.tgz", - "integrity": "sha512-qzwxuF8kZAp+rNUactMCgJh8fblq9D4lSqrrIxMDzLjgSZtjN32ix7r/HBe8QdOr76II9SVVPcMkX4sPzPfQ7w==", + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-10.0.0.tgz", + "integrity": "sha512-/LghpT93jsZLy55QzTsRZWMx6R1Mjc1Aktwps8sKSGE3WbrGwbSsh2uhDlpl6FMcKChYjJ0ochThWwwOodrQNg==", "engines": { "node": ">=0.8.0" } }, "node_modules/@azure/msal-node": { - "version": "1.14.6", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.14.6.tgz", - "integrity": "sha512-em/qqFL5tLMxMPl9vormAs13OgZpmQoJbiQ/GlWr+BA77eCLoL+Ehr5xRHowYo+LFe5b+p+PJVkRvT+mLvOkwA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.15.0.tgz", + "integrity": "sha512-fwC5M0c8pxOAzmScPbpx7j28YVTDebUaizlVF7bR0xvlU0r3VWW5OobCcr9ybqKS6wGyO7u4EhXJS9rjRWAuwA==", "dependencies": { - "@azure/msal-common": "^9.0.2", + "@azure/msal-common": "^10.0.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" }, @@ -1892,9 +1892,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "node_modules/cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, "node_modules/cors": { @@ -5810,16 +5810,16 @@ }, "dependencies": { "@azure/msal-common": { - "version": "9.0.2", - "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-9.0.2.tgz", - "integrity": "sha512-qzwxuF8kZAp+rNUactMCgJh8fblq9D4lSqrrIxMDzLjgSZtjN32ix7r/HBe8QdOr76II9SVVPcMkX4sPzPfQ7w==" + "version": "10.0.0", + "resolved": "https://registry.npmjs.org/@azure/msal-common/-/msal-common-10.0.0.tgz", + "integrity": "sha512-/LghpT93jsZLy55QzTsRZWMx6R1Mjc1Aktwps8sKSGE3WbrGwbSsh2uhDlpl6FMcKChYjJ0ochThWwwOodrQNg==" }, "@azure/msal-node": { - "version": "1.14.6", - "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.14.6.tgz", - "integrity": "sha512-em/qqFL5tLMxMPl9vormAs13OgZpmQoJbiQ/GlWr+BA77eCLoL+Ehr5xRHowYo+LFe5b+p+PJVkRvT+mLvOkwA==", + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/@azure/msal-node/-/msal-node-1.15.0.tgz", + "integrity": "sha512-fwC5M0c8pxOAzmScPbpx7j28YVTDebUaizlVF7bR0xvlU0r3VWW5OobCcr9ybqKS6wGyO7u4EhXJS9rjRWAuwA==", "requires": { - "@azure/msal-common": "^9.0.2", + "@azure/msal-common": "^10.0.0", "jsonwebtoken": "^9.0.0", "uuid": "^8.3.0" } @@ -7232,9 +7232,9 @@ "integrity": "sha1-4wOogrNCzD7oylE6eZmXNNqzriw=" }, "cookiejar": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.2.tgz", - "integrity": "sha512-Mw+adcfzPxcPeI+0WlvRrr/3lGVO0bD75SxX6811cxSh1Wbxx7xZBGK1eVtDf6si8rg2lhnUjsVLMFMfbRIuwA==", + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz", + "integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==", "dev": true }, "cors": { diff --git a/6-AdvancedScenarios/1-call-api-obo/API/package.json b/6-AdvancedScenarios/1-call-api-obo/API/package.json index 3b5b2516..dfd4fab1 100644 --- a/6-AdvancedScenarios/1-call-api-obo/API/package.json +++ b/6-AdvancedScenarios/1-call-api-obo/API/package.json @@ -13,7 +13,7 @@ "test": "jest --forceExit" }, "dependencies": { - "@azure/msal-node": "^1.14.6", + "@azure/msal-node": "^1.15.0", "@microsoft/microsoft-graph-client": "^3.0.4", "cors": "^2.8.5", "express": "^4.14.0", diff --git a/6-AdvancedScenarios/1-call-api-obo/README.md b/6-AdvancedScenarios/1-call-api-obo/README.md index 8513eff8..3bfa595a 100644 --- a/6-AdvancedScenarios/1-call-api-obo/README.md +++ b/6-AdvancedScenarios/1-call-api-obo/README.md @@ -354,6 +354,148 @@ const getGraphClient = (accessToken) => { }; ``` +### Handle Continuous Access Evaluation (CAE) challenges 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. 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](https://aka.ms/claimschallenge). +2. Processing the claim challenge when they are thrown by MS Graph Api. + +#### Declare the CAE capability in the configuration + +Apps using MSAL can declare CAE-capability by adding the `clientCapabilities` property in the configuration object. This is shown in [authConfig.js](./SPA/src/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); +``` + +Note that both the SPA and the web API projects need to declare this, since the web API in this sample also obtains tokens via the **on-behalf-of** flow (see [onBehalfOfClient.js](./API/auth/onBehalfOfClient.js)). + +#### Processing the CAE challenge from Microsoft Graph + +Once the middle-tier web API (msal-node-api) app receives the CAE claims challenge from Microsoft Graph, it needs to process the challenge and redirect the user back to Azure AD for re-authorization. However, since the middle-tier web API does not have UI to carry out this, it needs to propagate the error to the client app (msal-react-spa) instead, where it can be handled. This is shown in [profileController.js](./API/controllers/profileController.js): + +```javascript +exports.getProfile = async (req, res, next) => { + if (isAppOnlyToken(req.authInfo)) { + return next(new Error('This route requires a user token')); + } + + const userToken = req.get('authorization'); + const [bearer, tokenValue] = userToken.split(' '); + + if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.profile.delegatedPermissions.scopes)) { + try { + const accessToken = await getOboToken(tokenValue); + + const graphResponse = await getGraphClient(accessToken) + .api('/me') + .responseType(ResponseType.RAW) + .get(); + + if (graphResponse.status === 401) { + if (graphResponse.headers.get('WWW-Authenticate')) { + + if (isClientCapableOfClaimsChallenge(req.authInfo)) { + /** + * Append the WWW-Authenticate header from the Microsoft Graph response to the response to + * the client app. To learn more, visit: https://learn.microsoft.com/azure/active-directory/develop/app-resilience-continuous-access-evaluation + */ + return res.status(401) + .set('WWW-Authenticate', graphResponse.headers.get('WWW-Authenticate').toString()) + .json({ errorMessage: 'Continuous access evaluation resulted in claims challenge' }); + } + + return res.status(401).json({ errorMessage: 'Continuous access evaluation resulted in claims challenge but the client is not capable. Please enable client capabilities and try again' }); + } + + throw new Error('Unauthorized'); + } + + const graphData = await graphResponse.json(); + res.status(200).json(graphData); + } catch (error) { + if (error instanceof msal.InteractionRequiredAuthError) { + if (error.claims) { + const claimsChallenge = generateClaimsChallenge(error.claims); + + return res.status(401) + .set(claimsChallenge.headers[0], claimsChallenge.headers[1]) + .json({ errorMessage: error.errorMessage }); + } + + return res.status(401).json(error); + } + + next(error); + } + } else { + next(new Error('User does not have the required permissions')); + } +}; + +``` + +On the client side, we use MSAL's `acquireToken` API and provide the claims challenge as a parameter in the token request (see [Profile.jsx](./SPA/src/pages/Profile.jsx)). To retrieve the claims challenge from the API response, refer to the [fetch.js](./SPA/src/fetch.js), where we handle the response with the `handleClaimsChallenge` method: + +```javascript +export const callApiWithToken = async (accessToken, apiEndpoint, account) => { + const headers = new Headers(); + const bearer = `Bearer ${accessToken}`; + + headers.append("Authorization", bearer); + + const options = { + method: "GET", + headers: headers + }; + + const response = await fetch(apiEndpoint, options); + return handleClaimsChallenge(response, apiEndpoint, account); +} + +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( + `cc.${msalConfig.auth.clientId}.${account.idTokenClaims.oid}.${new URL(apiEndpoint).hostname}`, + claimsChallenge.claims, + ); + + throw new Error(`claims_challenge_occurred`); + } + + throw new Error(`Unauthorized: ${response.status}`); + } else { + throw new Error(`Something went wrong with the request: ${response.status}`); + } +}; +``` + ## Contributing If you'd like to contribute to this sample, see [CONTRIBUTING.MD](/CONTRIBUTING.md). diff --git a/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx b/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx index 7cfb60f3..66dbcc60 100644 --- a/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx +++ b/6-AdvancedScenarios/1-call-api-obo/SPA/src/pages/Profile.jsx @@ -29,7 +29,7 @@ const ProfileContent = () => { : undefined, // e.g {"access_token":{"xms_cc":{"values":["cp1"]}}} }; - const { login, result, error } = useMsalAuthentication(InteractionType.Popup, { + const { acquireToken, result, error } = useMsalAuthentication(InteractionType.Popup, { ...request, redirectUri: '/redirect.html', }); @@ -42,7 +42,7 @@ const ProfileContent = () => { 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); + acquireToken(InteractionType.Redirect, request); } console.log(error); @@ -51,18 +51,17 @@ const ProfileContent = () => { if (result) { callApiWithToken(result.accessToken, protectedResources.apiHello.endpoint, account) - .then((response) => setGraphData(response)) + .then((response) => setGraphData(response.value)) .catch((error) => { if (error.message === 'claims_challenge_occurred') { - login(InteractionType.Redirect, request); + acquireToken(InteractionType.Redirect, request); } else { console.log(error); - setGraphData(error); } }); } // eslint-disable-next-line - }, [graphData, result, error, login]); + }, [graphData, result, error, acquireToken]); if (error) { return
Error: {error.message}
;