Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 12 additions & 9 deletions 6-AdvancedScenarios/1-call-api-obo/API/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 }));
Expand All @@ -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)
Expand Down Expand Up @@ -94,6 +96,7 @@ const bearerStrategy = new passportAzureAd.BearerStrategy(
);

app.use(passport.initialize());

passport.use(bearerStrategy);

app.use(
Expand Down
37 changes: 37 additions & 0 deletions 6-AdvancedScenarios/1-call-api-obo/API/auth/claimUtils.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@

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
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 {
headers
};
}

module.exports = {
isClientCapableOfClaimsChallenge,
generateClaimsChallenge
}
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,5 @@ const hasRequiredDelegatedPermissions = (accessTokenPayload, requiredPermission)

module.exports = {
isAppOnlyToken,
hasRequiredDelegatedPermissions,
hasRequiredDelegatedPermissions
}
Original file line number Diff line number Diff line change
@@ -1,14 +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 { 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');

exports.getProfile = async (req, res, next) => {
if (isAppOnlyToken(req.authInfo)) {
return next(new Error('This route requires a user token'));
Expand All @@ -20,14 +19,46 @@ 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 response = await graphResponse.json();
res.json(response);
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 {
Expand Down
42 changes: 21 additions & 21 deletions 6-AdvancedScenarios/1-call-api-obo/API/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion 6-AdvancedScenarios/1-call-api-obo/API/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading