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

Commit ad53473

Browse files
committed
add caching to backend, add cache config options, update readme
1 parent a7a32ec commit ad53473

18 files changed

+275
-264
lines changed

5-AccessControl/2-call-api-groups/API/app.js

+31-18
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,27 @@ const cors = require('cors');
44

55
const passport = require('passport');
66
const passportAzureAd = require('passport-azure-ad');
7+
const NodeCache = require('node-cache');
78

8-
const authConfig = require('./authConfig');
9+
const authConfig = require('./authConfig.json');
910
const router = require('./routes/router');
1011
const routeGuard = require('./auth/guard');
1112

12-
const app = express();
13+
/**
14+
* IMPORTANT: In case of overage, group list is cached for 1 hr by default, and thus cached groups
15+
* will miss any changes to a users group membership for this duration. For capturing real-time
16+
* changes to a user's group membership, consider implementing Microsoft Graph change notifications.
17+
* For more information, visit: https://learn.microsoft.com/graph/api/resources/webhooks
18+
*/
19+
const nodeCache = new NodeCache({
20+
stdTTL: authConfig.cacheTTL, // in seconds
21+
checkperiod: 60 * 100,
22+
deleteOnExpire: true
23+
});
24+
25+
const cacheProvider = require('./utils/cacheProvider')(nodeCache);
1326

27+
const app = express();
1428
/**
1529
* Enable CORS middleware. In production, modify as to allow only designated origins and methods.
1630
* If you are using Azure App Service, we recommend removing the line below and configure CORS on the App Service itself.
@@ -89,7 +103,6 @@ app.use('/api', (req, res, next) => {
89103
return res.status(401).json({ error: err.message });
90104
}
91105

92-
93106
if (!user) {
94107
// If no user object found, send a 401 response.
95108
return res.status(401).json({ error: 'Unauthorized' });
@@ -103,21 +116,21 @@ app.use('/api', (req, res, next) => {
103116
})(req, res, next);
104117
},
105118

106-
routeGuard(authConfig.accessMatrix),
107-
router,
108-
(err, req, res, next) => {
109-
/**
110-
* Add your custom error handling logic here. For more information, see:
111-
* http://expressjs.com/en/guide/error-handling.html
112-
*/
113-
114-
// set locals, only providing error in development
115-
res.locals.message = err.message;
116-
res.locals.error = req.app.get('env') === 'development' ? err : {};
117-
118-
// send error response
119-
res.status(err.status || 500).send(err);
120-
}
119+
routeGuard(authConfig.accessMatrix, cacheProvider),
120+
router,
121+
(err, req, res, next) => {
122+
/**
123+
* Add your custom error handling logic here. For more information, see:
124+
* http://expressjs.com/en/guide/error-handling.html
125+
*/
126+
127+
// set locals, only providing error in development
128+
res.locals.message = err.message;
129+
res.locals.error = req.app.get('env') === 'development' ? err : {};
130+
131+
// send error response
132+
res.status(err.status || 500).send(err);
133+
}
121134
);
122135

123136
const port = process.env.PORT || 5000;

5-AccessControl/2-call-api-groups/API/auth/guard.js

+7-7
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
const handleOverage = require('./overage');
2-
const { requestHasRequiredAttributes } = require('./permissionUtils');
2+
const { hasRequiredGroups, hasOverageOccurred } = require('./permissionUtils');
33

4-
const routeGuard = (accessMatrix) => {
4+
const routeGuard = (accessMatrix, cache) => {
55
return async (req, res, next) => {
66
if (req.authInfo.groups === undefined) {
7-
if (req.authInfo['_claim_names'] && req.authInfo['_claim_sources']) {
8-
return handleOverage(req, res, next);
9-
} else {
10-
return res.status(403).json({ error: 'No group claim found!' });
7+
if (hasOverageOccurred(req.authInfo)) {
8+
return handleOverage(req, res, next, cache);
119
}
10+
11+
return res.status(403).json({ error: 'No group claim found!' });
1212
} else {
13-
if (!requestHasRequiredAttributes(accessMatrix, req.path, req.method, req.authInfo['groups'])) {
13+
if (!hasRequiredGroups(accessMatrix, req.path, req.method, req.authInfo['groups'])) {
1414
return res.status(403).json({ error: 'User does not have the group, method or path' });
1515
}
1616
}

5-AccessControl/2-call-api-groups/API/auth/overage.js

+22-3
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@
55

66
const msal = require('@azure/msal-node');
77

8+
const { hasRequiredGroups } = require("./permissionUtils");
89
const { getFilteredGroups } = require('../utils/graphClient');
9-
const { requestHasRequiredAttributes } = require("./permissionUtils");
1010

1111
const config = require('../authConfig.json');
1212

@@ -45,13 +45,32 @@ const getOboToken = async (oboAssertion) => {
4545
}
4646
};
4747

48-
const handleOverage = async (req, res, next) => {
48+
const handleOverage = async (req, res, next, cacheProvider) => {
4949
const authHeader = req.headers.authorization;
5050
const accessToken = authHeader.split(' ')[1];
5151

52+
const { oid } = req.authInfo;
53+
54+
// check if the user has an entry in the cache
55+
if (cacheProvider.has(oid)) {
56+
const { groups, sourceTokenId } = cacheProvider.get(oid);
57+
58+
if (sourceTokenId === accessToken['uti']) {
59+
res.locals.groups = groups;
60+
return checkAccess(req, res, next);
61+
}
62+
}
63+
5264
try {
5365
const oboToken = await getOboToken(accessToken);
5466
res.locals.groups = await getFilteredGroups(oboToken, config.accessMatrix.todolist.groups);
67+
68+
// cache the groups and the source token id
69+
cacheProvider.set(oid, {
70+
groups: res.locals.groups,
71+
sourceTokenId: accessToken['uti']
72+
});
73+
5574
return checkAccess(req, res, next);
5675
} catch (error) {
5776
console.log(error);
@@ -60,7 +79,7 @@ const handleOverage = async (req, res, next) => {
6079
};
6180

6281
const checkAccess = (req, res, next) => {
63-
if (!requestHasRequiredAttributes(config.accessMatrix, req.path, req.method, res.locals.groups)) {
82+
if (!hasRequiredGroups(config.accessMatrix, req.path, req.method, res.locals.groups)) {
6483
return res.status(403).json({ error: 'User does not have the group, method or path' });
6584
}
6685
next();

5-AccessControl/2-call-api-groups/API/auth/permissionUtils.js

+12-2
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
* @param {Array} groups
2424
* @returns boolean
2525
*/
26-
const requestHasRequiredAttributes = (accessMatrix, path, method, groups) => {
26+
const hasRequiredGroups = (accessMatrix, path, method, groups) => {
2727
const accessRule = Object.values(accessMatrix).find((accessRule) => path.includes(accessRule.path));
2828

2929
if (accessRule.methods.includes(method)) {
@@ -37,7 +37,17 @@ const requestHasRequiredAttributes = (accessMatrix, path, method, groups) => {
3737
return false;
3838
};
3939

40+
/**
41+
* Checks if the access token has claims indicating that overage has occurred.
42+
* @param {Object} accessToken
43+
* @returns {boolean}
44+
*/
45+
const hasOverageOccurred = (accessTokenPayload) => {
46+
return accessTokenPayload.hasOwnProperty('_claim_names') && accessTokenPayload['_claim_names'].hasOwnProperty('groups');
47+
};
48+
4049
module.exports = {
4150
hasRequiredDelegatedPermissions,
42-
requestHasRequiredAttributes,
51+
hasRequiredGroups,
52+
hasOverageOccurred
4353
};

5-AccessControl/2-call-api-groups/API/authConfig.json

+2-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
"settings": {
1313
"validateIssuer": true,
1414
"passReqToCallback": false,
15-
"loggingLevel": "info"
15+
"loggingLevel": "info",
16+
"cacheTTL": 3600
1617
},
1718
"protectedResources": {
1819
"graphAPI": {

5-AccessControl/2-call-api-groups/API/package-lock.json

+13
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

5-AccessControl/2-call-api-groups/API/package.json

+1
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
"isomorphic-fetch": "^3.0.0",
1717
"lowdb": "^1.0.0",
1818
"morgan": "^1.10.0",
19+
"node-cache": "^5.1.2",
1920
"passport": "^0.6.0",
2021
"passport-azure-ad": "^4.3.4"
2122
},
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* Pass below an instance of the cache client of your choice
3+
* that implements SET, GET, HAS and DELETE methods.
4+
* @param {Object} provider
5+
* @returns
6+
*/
7+
module.exports = (provider) => {
8+
9+
/**
10+
* Sets a key-value pair in the cache.
11+
* @param {String} key
12+
* @param {any} item
13+
*/
14+
const set = (key, item) => {
15+
provider.set(key, item);
16+
console.log(`Cache set: ${key} - ${item}`);
17+
}
18+
19+
const get = (key) => {
20+
return provider.get(key);
21+
}
22+
23+
const has = (key) => {
24+
console.log('Cache hit for key: ' + key);
25+
return provider.has(key);
26+
}
27+
28+
const del = (key) => {
29+
provider.del(key);
30+
console.log('Cache deleted for key: ' + key);
31+
}
32+
33+
return {
34+
get,
35+
set,
36+
has,
37+
del
38+
}
39+
}

5-AccessControl/2-call-api-groups/AppCreationScripts/sample.json

-4
Original file line numberDiff line numberDiff line change
@@ -22,10 +22,6 @@
2222
"GroupMembershipClaims": "SecurityGroup",
2323
"PasswordCredentials": "Auto",
2424
"scopes": ["access_via_group_assignments"],
25-
"Sample": {
26-
"SampleSubPath": "5-AccessControl\\2-call-api-groups\\SPA",
27-
"ProjectDirectory": "\\2-call-api-groups\\SPA"
28-
},
2925
"SecurityGroups": [
3026
{
3127
"Name": "GroupAdmin",

0 commit comments

Comments
 (0)