Skip to content
This repository was archived by the owner on May 3, 2024. It is now read-only.

Commit aa2fc72

Browse files
committed
add about the code section
1 parent 3c31231 commit aa2fc72

File tree

2 files changed

+147
-2
lines changed

2 files changed

+147
-2
lines changed

6-AdvancedScenarios/3-call-api-acrs/API/app.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ app.use(passport.initialize());
9191

9292
passport.use(bearerStrategy);
9393

94-
// protec api endpoints
94+
// protected api endpoints
9595
app.use('/api',
9696
passport.authenticate('oauth-bearer', { session: false }), // validate access tokens
9797
routeGuard, // check for auth context

6-AdvancedScenarios/3-call-api-acrs/README.md

Lines changed: 146 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -287,26 +287,171 @@ Select the value and create the policy as required. For example, you might want
287287

288288
### Checking for client capabilities
289289

290+
The client capabilities claim (`xms_cc`) indicate whether a client application can satisfy the claims challenge generated by a conditional access policy. To obtain this claim in an access token, we enable the `clientCapabilities` configuration option in [authConfig.js](./SPA/src/authConfig.js):
291+
290292
```javascript
293+
const msalConfig = {
294+
auth: {
295+
clientId: 'Enter_the_Application_Id_Here',
296+
authority: 'https://login.microsoftonline.com/Enter_the_Tenant_Info_Here',
297+
redirectUri: "/",
298+
postLogoutRedirectUri: "/",
299+
navigateToLoginRequestUrl: true,
300+
clientCapabilities: ["CP1"] // this lets the resource owner know that this client is capable of handling claims challenge.
301+
}
302+
}
291303

304+
const msalInstance = new PublicClientApplication(msalConfig);
292305
```
293306

294307
### Checking for auth context
295308

309+
In [app.js](./API/app.js), we add custom a `routeGuard` middleware to handle incoming requests web API's todolist endpoints:
310+
311+
```javascript
312+
app.use('/api',
313+
passport.authenticate('oauth-bearer', { session: false }), // validate access tokens
314+
routeGuard, // check for auth context
315+
todolistRoutes
316+
);
317+
```
318+
319+
The `routeGuard` middleware checks the mock database for any auth context entries, and inspects the access token in the authorization header of the incoming request to see if it contains the necessary claims. If it does, it passes the request to the next middleware in chain. If it doesn't, the `checkForRequiredAuthContext` middleware takes over. This is shown in [routeGuard.js](./API/utils/routeGuard.js):
320+
296321
```javascript
322+
const authContextGuard = (req, res, next) => {
323+
const acrs = AuthContext.getAuthContexts(); // get the current auth contexts from db
324+
325+
// if there is no auth context in the db, let the request through
326+
if (acrs.length === 0) {
327+
return next();
328+
} else {
329+
const authContext = acrs.find(ac => ac.operation === req.method && ac.tenantId === req.authInfo.tid);
330+
331+
if (authContext) {
332+
// if found, check the request for the required claims
333+
return checkForRequiredAuthContext(req, res, next, authContext.authContextId);
334+
}
297335

336+
next();
337+
}
338+
}
298339
```
299340

300-
### Generating claims challenge
341+
In [claimsManager.js](./API/utils/claimsManager.js):
301342

302343
```javascript
344+
const checkForRequiredAuthContext = (req, res, next, authContextId) => {
345+
if (!req.authInfo['acrs'] || !req.authInfo['acrs'].includes(authContextId)) {
346+
if (isClientCapableOfClaimsChallenge(req.authInfo)) {
347+
348+
const claimsChallenge = generateClaimsChallenge(authContextId);
349+
350+
return res.status(claimsChallenge.statusCode)
351+
.set(claimsChallenge.headers[0], claimsChallenge.headers[1])
352+
.json({error: claimsChallenge.message});
353+
354+
} else {
355+
return res.status(403).json({ error: 'Client is not capable' });
356+
}
357+
} else {
358+
next();
359+
}
360+
}
303361

362+
const isClientCapableOfClaimsChallenge = (accessTokenClaims) => {
363+
if (accessTokenClaims['xms_cc'] && accessTokenClaims['xms_cc'].includes('CP1')) {
364+
return true;
365+
}
366+
367+
return false;
368+
}
369+
```
370+
371+
### Generating claims challenge
372+
373+
If there is an auth context entry in the mock database and the incoming request does not contain an access token with the necessary claims, the web API needs to create a **claims challenge** and send it to client application to allow the user to satisfy the challenge (for instance, perform multi-factor authentication). This is shown in [claimsManager.js](./API/utils/claimsManager.js):
374+
375+
```javascript
376+
const generateClaimsChallenge = (authContextId) => {
377+
const clientId = process.env.CLIENT_ID;
378+
379+
const statusCode = 401;
380+
381+
// claims challenge object
382+
const challenge = { access_token: { acrs: { essential: true, value: authContextId }}};
383+
384+
// base64 encode the challenge object
385+
const base64str = Buffer.from(JSON.stringify(challenge)).toString('base64');
386+
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 + "\", cc_type=\"authcontext\""];
387+
const message = "The presented access tokens had insufficient claims. Please request for claims designated in the www-authentication header and try again.";
388+
389+
return {
390+
headers,
391+
statusCode,
392+
message
393+
}
394+
}
304395
```
305396

306397
### Handling claims challenge
307398

399+
Once the client app receives the claims challenge, 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 `acquireTokenPopup()` API 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 a HTTP DELETE request the to web API with the `handleClaimsChallenge` method:
400+
308401
```javascript
402+
import { BrowserAuthError } from "@azure/msal-browser";
403+
import { protectedResources } from "./authConfig";
404+
import { msalInstance } from "./index";
405+
406+
export const deleteTask = async (id) => {
407+
const accessToken = await getToken();
408+
409+
const headers = new Headers();
410+
const bearer = `Bearer ${accessToken}`;
411+
412+
headers.append("Authorization", bearer);
413+
414+
const options = {
415+
method: "DELETE",
416+
headers: headers
417+
};
418+
419+
return fetch(protectedResources.apiTodoList.todoListEndpoint + `/${id}`, options)
420+
.then(handleClaimsChallenge)
421+
.catch(error => console.log(error));
422+
}
423+
424+
const handleClaimsChallenge = async (response) => {
425+
if (response.status === 401) {
426+
if (response.headers.get('www-authenticate')) {
427+
const authenticateHeader = response.headers.get("www-authenticate");
428+
429+
const claimsChallenge = authenticateHeader.split(" ")
430+
.find(entry => entry.includes("claims=")).split('="')[1].split('",')[0];
431+
432+
try {
433+
await msalInstance.acquireTokenPopup({
434+
claims: window.atob(claimsChallenge), // decode the base64 string
435+
scopes: protectedResources.apiTodoList.scopes
436+
});
437+
} catch (error) {
438+
// catch if popups are blocked
439+
if (error instanceof BrowserAuthError &&
440+
(error.errorCode === "popup_window_error" || error.errorCode === "empty_window_error")) {
441+
442+
await msalInstance.acquireTokenRedirect({
443+
claims: window.atob(claimsChallenge),
444+
scopes: protectedResources.apiTodoList.scopes
445+
});
446+
}
447+
}
448+
} else {
449+
return { error: "unknown header" }
450+
}
451+
}
309452

453+
return response.json();
454+
}
310455
```
311456

312457
## More information

0 commit comments

Comments
 (0)