Skip to content

Commit 92e7478

Browse files
committed
update 3-2
1 parent 3d23ca8 commit 92e7478

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

41 files changed

+7783
-17338
lines changed

1-Authentication/1-sign-in/README-incremental.md

Lines changed: 0 additions & 305 deletions
This file was deleted.

1-Authentication/2-sign-in-b2c/README-incremental.md

Lines changed: 0 additions & 329 deletions
This file was deleted.

2-Authorization-I/1-call-graph/README-incremental.md

Lines changed: 0 additions & 454 deletions
This file was deleted.
Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
const express = require('express');
2+
const morgan = require('morgan');
3+
const cors = require('cors');
4+
5+
const rateLimit = require('express-rate-limit');
6+
7+
const passport = require('passport');
8+
const passportAzureAd = require('passport-azure-ad');
9+
10+
11+
const authConfig = require('./authConfig.js');
12+
const router = require('./routes/index');
13+
14+
const app = express();
15+
16+
/**
17+
* If your app is behind a proxy, reverse proxy or a load balancer, consider
18+
* letting express know that you are behind that proxy. To do so, uncomment
19+
* the line below.
20+
*/
21+
22+
// app.set('trust proxy', /* numberOfProxies */);
23+
24+
/**
25+
* HTTP request handlers should not perform expensive operations such as accessing the file system,
26+
* executing an operating system command or interacting with a database without limiting the rate at
27+
* which requests are accepted. Otherwise, the application becomes vulnerable to denial-of-service attacks
28+
* where an attacker can cause the application to crash or become unresponsive by issuing a large number of
29+
* requests at the same time. For more information, visit: https://cheatsheetseries.owasp.org/cheatsheets/Denial_of_Service_Cheat_Sheet.html
30+
*/
31+
const limiter = rateLimit({
32+
windowMs: 15 * 60 * 1000, // 15 minutes
33+
max: 100, // Limit each IP to 100 requests per `window` (here, per 15 minutes)
34+
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
35+
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
36+
});
37+
38+
// Apply the rate limiting middleware to all requests
39+
app.use(limiter);
40+
41+
app.use(cors());
42+
43+
app.use(express.json())
44+
app.use(express.urlencoded({ extended: false}));
45+
app.use(morgan('dev'));
46+
47+
const options = {
48+
identityMetadata: `https://${authConfig.metadata.b2cDomain}/${authConfig.credentials.tenantName}/${authConfig.policies.policyName}/${authConfig.metadata.version}/${authConfig.metadata.discovery}`,
49+
clientID: authConfig.credentials.clientID,
50+
audience: authConfig.credentials.clientID,
51+
policyName: authConfig.policies.policyName,
52+
isB2C: authConfig.settings.isB2C,
53+
validateIssuer: authConfig.settings.validateIssuer,
54+
loggingLevel: authConfig.settings.loggingLevel,
55+
passReqToCallback: authConfig.settings.passReqToCallback,
56+
loggingNoPII: authConfig.settings.loggingNoPII, // set this to true in the authConfig.js if you want to enable logging and debugging
57+
};
58+
59+
const bearerStrategy = new passportAzureAd.BearerStrategy(options, (req,token, done) => {
60+
/**
61+
* Below you can do extended token validation and check for additional claims, such as:
62+
* - check if the delegated permissions in the 'scp' are the same as the ones declared in the application registration.
63+
*
64+
* Bear in mind that you can do any of the above checks within the individual routes and/or controllers as well.
65+
* For more information, visit: https://learn.microsoft.com/en-us/azure/active-directory-b2c/tokens-overview
66+
*/
67+
68+
/**
69+
* Lines below verifies if the caller's client ID is in the list of allowed clients.
70+
* This ensures only the applications with the right client ID can access this API.
71+
* To do so, we use "azp" claim in the access token. Uncomment the lines below to enable this check.
72+
*/
73+
// if (!myAllowedClientsList.includes(token.azp)) {
74+
// return done(new Error('Unauthorized'), {}, "Client not allowed");
75+
// }
76+
77+
// const myAllowedClientsList = [
78+
// /* add here the client IDs of the applications that are allowed to call this API */
79+
// ]
80+
81+
/**
82+
* Access tokens that have no 'scp' (for delegated permissions).
83+
*/
84+
if (!token.hasOwnProperty('scp')) {
85+
return done(new Error('Unauthorized'), null, 'No delegated permissions found');
86+
}
87+
88+
done(null, {}, token);
89+
});
90+
91+
92+
app.use(passport.initialize());
93+
94+
passport.use(bearerStrategy);
95+
96+
app.use(
97+
'/api',
98+
(req, res, next) => {
99+
passport.authenticate(
100+
'oauth-bearer',
101+
{
102+
session: false,
103+
},
104+
(err, user, info) => {
105+
if (err) {
106+
/**
107+
* An error occurred during authorization. Either pass the error to the next function
108+
* for Express error handler to handle, or send a response with the appropriate status code.
109+
*/
110+
return res.status(401).json({ error: err.message });
111+
}
112+
113+
if (!user) {
114+
// If no user object found, send a 401 response.
115+
return res.status(401).json({ error: 'Unauthorized' });
116+
}
117+
118+
if (info) {
119+
// access token payload will be available in req.authInfo downstream
120+
req.authInfo = info;
121+
return next();
122+
}
123+
}
124+
)(req, res, next);
125+
},
126+
router, // the router with all the routes
127+
(err, req, res, next) => {
128+
/**
129+
* Add your custom error handling logic here. For more information, see:
130+
* http://expressjs.com/en/guide/error-handling.html
131+
*/
132+
133+
// set locals, only providing error in development
134+
res.locals.message = err.message;
135+
res.locals.error = req.app.get('env') === 'development' ? err : {};
136+
137+
// send error response
138+
res.status(err.status || 500).send(err);
139+
}
140+
);
141+
142+
const port = process.env.PORT || 5000;
143+
144+
app.listen(port, () => {
145+
console.log('Listening on port ' + port);
146+
});
147+
148+
module.exports = app;
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/**
2+
* Ensures that the access token has the specified delegated permissions.
3+
* @param {Object} accessTokenPayload: Parsed access token payload
4+
* @param {Array} requiredPermission: list of required permissions
5+
* @returns {boolean}
6+
*/
7+
const hasRequiredDelegatedPermissions = (accessTokenPayload, requiredPermission) => {
8+
const normalizedRequiredPermissions = requiredPermission.map((permission) => permission.toUpperCase());
9+
10+
if (accessTokenPayload.hasOwnProperty('scp') && accessTokenPayload.scp.split(' ')
11+
.some(claim => normalizedRequiredPermissions.includes(claim.toUpperCase()))) {
12+
return true;
13+
}
14+
return false;
15+
16+
};
17+
18+
module.exports = {
19+
hasRequiredDelegatedPermissions
20+
};
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
const passportConfig = {
2+
credentials: {
3+
tenantName: 'fabrikamb2c.onmicrosoft.com',
4+
clientID: 'e29ac359-6a90-4f9e-b31c-8f64e1ac20cb',
5+
},
6+
policies: {
7+
policyName: 'B2C_1_susi_v2',
8+
},
9+
metadata: {
10+
b2cDomain: 'fabrikamb2c.b2clogin.com',
11+
authority: 'login.microsoftonline.com',
12+
discovery: '.well-known/openid-configuration',
13+
version: 'v2.0',
14+
},
15+
settings: {
16+
isB2C: true,
17+
validateIssuer: false,
18+
passReqToCallback: true,
19+
loggingLevel: 'info',
20+
loggingNoPII: false,
21+
},
22+
protectedRoutes: {
23+
todolist: {
24+
endpoint: '/api/todolist',
25+
delegatedPermissions: {
26+
read: ['ToDoList.Read', 'ToDoList.ReadWrite'],
27+
write: ['ToDoList.ReadWrite'],
28+
},
29+
},
30+
},
31+
};
32+
33+
module.exports = passportConfig;

3-Authorization-II/2-call-api-b2c/API/config.json

Lines changed: 0 additions & 22 deletions
This file was deleted.
Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,101 @@
1+
const lowdb = require('lowdb');
2+
const FileSync = require('lowdb/adapters/FileSync');
3+
const adapter = new FileSync('./data/db.json');
4+
const db = lowdb(adapter);
5+
const { v4: uuidv4 } = require('uuid');
6+
7+
const {
8+
hasRequiredDelegatedPermissions,
9+
} = require('../auth/permissionUtils');
10+
11+
const authConfig = require('../authConfig');
12+
13+
exports.getTodo = (req, res, next) => {
14+
if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.read)) {
15+
try {
16+
/**
17+
* The 'oid' (object id) is the only claim that should be used to uniquely identify
18+
* a user in an Azure AD tenant. The token might have one or more of the following claim,
19+
* that might seem like a unique identifier, but is not and should not be used as such,
20+
* especially for systems which act as system of record (SOR):
21+
*
22+
* - upn (user principal name): might be unique amongst the active set of users in a tenant but
23+
* tend to get reassigned to new employees as employees leave the organization and
24+
* others take their place or might change to reflect a personal change like marriage.
25+
*
26+
* - email: might be unique amongst the active set of users in a tenant but tend to get
27+
* reassigned to new employees as employees leave the organization and others take their place.
28+
*/
29+
const owner = req.authInfo['oid'];
30+
const id = req.params.id;
31+
32+
const todo = db.get('todos')
33+
.filter({ owner: owner })
34+
.find({ id: id })
35+
.value();
36+
37+
res.status(200).send(todo);
38+
} catch (error) {
39+
next(error);
40+
}
41+
} else {
42+
next(new Error('User does not have the required permissions'))
43+
}
44+
}
45+
46+
exports.getTodos = (req, res, next) => {
47+
if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.read)) {
48+
try {
49+
const owner = req.authInfo['oid'];
50+
51+
const todos = db.get('todos')
52+
.filter({ owner: owner })
53+
.value();
54+
55+
res.status(200).send(todos);
56+
} catch (error) {
57+
next(error);
58+
}
59+
} else {
60+
next(new Error('User does not have the required permissions'))
61+
}
62+
}
63+
64+
exports.postTodo = (req, res, next) => {
65+
if (hasRequiredApplicationPermissions(req.authInfo, authConfig.protectedRoutes.todolist.applicationPermissions.write)) {
66+
try {
67+
const todo = {
68+
description: req.body.description,
69+
id: uuidv4(),
70+
owner: req.authInfo['oid'] // oid is the only claim that should be used to uniquely identify a user in an Azure AD tenant
71+
};
72+
73+
db.get('todos').push(todo).write();
74+
75+
res.status(200).json(todo);
76+
} catch (error) {
77+
next(error);
78+
}
79+
} else (
80+
next(new Error('User or application does not have the required permissions'))
81+
)
82+
}
83+
84+
exports.deleteTodo = (req, res, next) => {
85+
if (hasRequiredDelegatedPermissions(req.authInfo, authConfig.protectedRoutes.todolist.delegatedPermissions.write)) {
86+
try {
87+
const id = req.params.id;
88+
const owner = req.authInfo['oid'];
89+
90+
db.get('todos')
91+
.remove({ owner: owner, id: id })
92+
.write();
93+
94+
res.status(200).json({ message: "success" });
95+
} catch (error) {
96+
next(error);
97+
}
98+
} else {
99+
next(new Error('User does not have the required permissions'))
100+
}
101+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"todos": []
3+
}

3-Authorization-II/2-call-api-b2c/API/index.js

Lines changed: 0 additions & 60 deletions
This file was deleted.

0 commit comments

Comments
 (0)