Skip to content

Commit be015e1

Browse files
committed
Lesson 4
1 parent 54bc4e8 commit be015e1

File tree

2 files changed

+202
-2
lines changed
  • module4-authentication-and-security

2 files changed

+202
-2
lines changed

module4-authentication-and-security/r1.2-adding-authorization-layer/README.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -166,7 +166,7 @@ const { User } = require("../db"); // as exported in the db file
166166
router.post("/elevate", async (req, res) => {
167167
const user = req.session.user;
168168

169-
// Check if the use has signed in, and is admin
169+
// Check if the user has signed in, and is admin
170170
if (user && user.isAdmin) {
171171
const { subject_id, role_id } = req.body;
172172

@@ -211,7 +211,7 @@ if (user && (user.isAdmin || user.isModerator)) {
211211

212212
There is still a problem with our code though. It is very messy with lots of if-else and we can introduce a bug very easily by not returning or badly structures if-else clauses.
213213

214-
In the next lesson, we will introduce an easier way to enforce our guards using a feature called **middleware**. We can define an `onlyAdmins`, `onlyModerators`, and `onlyAuthenticated` middlewares that would stop unauthorized requests, and only let authorized access through.
214+
In the next lesson, we will introduce an easier way to enforce our guards using a feature called **middleware**. We can define an `onlyAdmins`, `onlyModerators`, and `onlyAuthenticated` middleware that would stop unauthorized requests, and only let authorized access through.
215215

216216
## **Conclusion**
217217

Lines changed: 200 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,200 @@
1+
# Authorization through middleware
2+
3+
In the previous lesson, we introduced authorization and implemented authentication and authorization guards using if-else statements directly inside the route controllers. This isn't an acceptable approach as the code will be very messy and bug-prone. It is better to define authorization logic outside the controllers, and use middlewares to enforce it.
4+
5+
This lesson objectives are:
6+
7+
- Introduce middleware as a feature in Express.js and many other backend frameworks
8+
- Implement authentication guard
9+
- Implement authorization guard
10+
- Guard certain endpoints with these middlewares
11+
12+
## **Middleware**
13+
14+
Middleware are functions that have access to the [request object](https://expressjs.com/en/4x/api.html#req) (`req`), the [response object](https://expressjs.com/en/4x/api.html#res) (`res`), and the next middleware function in the application’s request-response cycle. The next middleware function is commonly denoted by a variable named `next`.
15+
16+
```js
17+
const loggerMiddleware = (req, res, next) => {
18+
// you have full access to the request and response objects
19+
console.log("Request Type:", req.method);
20+
// end the middleware by calling next()
21+
next();
22+
};
23+
```
24+
25+
Middleware functions can perform the following tasks:
26+
27+
- Execute any code.
28+
- Make changes to the request and the response objects.
29+
- End the request-response cycle.
30+
- Call the next middleware function in the stack.
31+
32+
If the current middleware function does not end the request-response cycle, it must call `next()` to pass control to the next middleware function. Otherwise, the request will be left hanging.
33+
34+
All the functions we used inside the controllers, or that we passed to the router endpoint, are middleware functions. They were used as the last middleware because we haven't called the `next()` function and ended the cycle by calling `res.render()` or `res.json()`.
35+
36+
An Express application can use the following types of middleware:
37+
38+
- [Application-level middleware](https://expressjs.com/en/guide/using-middleware.html#middleware.application)
39+
- [Router-level middleware](https://expressjs.com/en/guide/using-middleware.html#middleware.router)
40+
- [Error-handling middleware](https://expressjs.com/en/guide/using-middleware.html#middleware.error-handling)
41+
- [Built-in middleware](https://expressjs.com/en/guide/using-middleware.html#middleware.built-in)
42+
- [Third-party middleware](https://expressjs.com/en/guide/using-middleware.html#middleware.third-party)
43+
44+
You can load application-level and router-level middleware with an optional mount path. You can also load a series of middleware functions together, which creates a sub-stack of the middleware system at a mount point. Check out Express documentation regarding [using middleware](https://expressjs.com/en/guide/using-middleware.html).
45+
46+
We've seen middleware in previous lessons when we used the `express-session` and `express-jwt`. They are application-level middleware that we used to create the session object or the user object inside `req` that we subsequently used in the controller. By default, `req` wouldn't contain `session` or `user` however, in the case of `express-session` the following simplified steps were done for _every_ request:
47+
48+
- Read the cookies from `req` and grab the `sessionID`.
49+
- If no `sessionID` is found in the request's cookies:
50+
- Create a new session and grab its ID.
51+
- Queue a `Set-Cookie` header with `sessionID` to the next response by modifying `res`.
52+
- Else read the session associated with requests `sessionID` from the session adapter.
53+
- Append the created or read `session` to `req`.
54+
- Call `next()` to let the app continue its req-res cycle.
55+
56+
### **Middleware precedence**
57+
58+
In Express.js middleware are applied in their order of use. Remember that every function that has `req` and `res` is a middleware function. And the middleware stack is established when you use `app.use` or `router.use`.
59+
60+
```js
61+
app.use(session());
62+
app.use(logger());
63+
```
64+
65+
In the code above, the `express-session` middleware will be executed first, then when it calls `next()`, the `logger` middleware will be executed till it calls `next()` as well, and so on.
66+
67+
So it is important whenever you define your middleware that you make sure the required middleware for it are executed before it.
68+
69+
For example, in your `onlyAuthenticated` middleware, you will need to check `req.session.user` (or `req.user` if you use `express-jwt`). Hence, it is important that the session middleware is applied before your middleware. Otherwise, you won't have `session` inside `req`.
70+
71+
### **Middleware mount-path**
72+
73+
Middleware can be scoped as well. It can be for every request, or for specific endpoints, or even specific HTTP methods:
74+
75+
```js
76+
app.use(function (req, res, next) {
77+
// will be called on every request. Application wide mount path
78+
next();
79+
});
80+
81+
app.use("/dashboard", function (req, res, next) {
82+
// will be called on all request to /dashboard
83+
next();
84+
});
85+
86+
app.get("/user", function (req, res, next) {
87+
// will be invoked only when a GET requesting /user endpoint
88+
next();
89+
});
90+
```
91+
92+
You can even enforce the middleware on only specific router instances and controllers:
93+
94+
```js
95+
// ./routes/user.js
96+
const express = require("express");
97+
const router = express.Router();
98+
99+
router.use(function (req, res, next) {
100+
// will be called on all routes in this file ONLY
101+
next();
102+
});
103+
```
104+
105+
### **Error-handling middlware**
106+
107+
All the middleware we've seen so far take only **three** arguments, `req`, `res`, and `next`. You can define an error-handling middleware by providing **four** arguments: `err`, `req`, `res`, and `next`. These middleware are important to handle errors that are thrown in your app and provide a safe fallback response to clients without exposing your app's security.
108+
109+
```js
110+
app.use(function (err, req, res, next) {
111+
console.error(err.stack);
112+
res.status(500).send("Something broke!");
113+
});
114+
```
115+
116+
Usually, an error logging service like [sentry](https://sentry.io/) is also contacted in this middleware to log and track the error so you get notified when your app has run into an unhandled exception, with the required request-response details that can help the developers reproduce the error to fix it.
117+
118+
> ⚠️ **Warning**: Error-handling middleware always takes **four** arguments. You must provide four arguments to identify it as an error-handling middleware function. Even if you don’t need to use the next object, you must specify it to maintain the signature. Otherwise, the next object will be interpreted as regular middleware and will fail to handle errors.
119+
120+
## **Auth guard middleware**
121+
122+
To implement `onlyAdmins`, `onlyModerators`, and `onlyAuthenticated` middleware, we create a new middleware containing source file:
123+
124+
```js
125+
// ./middlware/index.js
126+
127+
function onlyAuthenticated(req, res, next) {
128+
if (req.session.user) {
129+
next(); // user is authenticated
130+
} else {
131+
res.status(401).redirect("/login");
132+
}
133+
}
134+
135+
function onlyAdmins(req, res, next) {
136+
const user = req.session.user;
137+
138+
if (user && user.isAdmin) {
139+
next();
140+
} else {
141+
res.status(401).json({ error: "Unauthorized" });
142+
}
143+
}
144+
145+
function onlyModerators(req, res, next) {
146+
const user = req.session.user;
147+
if (user && (user.isAdmin || user.isModerator)) {
148+
next();
149+
} else {
150+
res.status(401).json({ error: "Unauthorized" });
151+
}
152+
}
153+
154+
module.exports = { onlyAuthenticated, onlyAdmins, onlyModerators };
155+
```
156+
157+
then inside our router, we can guard it by applying the middleware **before** the endpoint handler function:
158+
159+
```js
160+
// ./routes/role.js
161+
const express = require("express");
162+
const router = express.Router();
163+
const { User } = require("../db"); // as exported in the db file
164+
const { onlyAdmins } = require('../middleware');
165+
166+
router.post("/elevate", onlyAdmins, async (req, res) => {
167+
const { subject_id, role_id } = req.body;
168+
169+
// check role_id is correct
170+
const roles = Object.values(User.roles);
171+
if (!roles.includes(parseInt(role_id, 10))) {
172+
res.status(400).json({
173+
error: `role_id is incorrect. Accepted values are: ${roles.join(", ")}`,
174+
});
175+
return; // exit
176+
}
177+
178+
// check subject to be elevated exists
179+
// we will query users with defaultScope to exclude password
180+
const subject = await User.findByPk(subject_id);
181+
182+
if (!subject) {
183+
res.status(400).json({
184+
error: `User with id: ${subject_id} cannot be found.`,
185+
});
186+
} else {
187+
subject.role_id = parseInt(role_id, 10);
188+
await subject.save(); // save dirty instance to database
189+
res.status(202).json({ status: "OK" });
190+
}
191+
}
192+
```
193+
194+
Because we are using the `onlyAdmins` middleware, there is no need to check if the user is logged in or is an admin. The code above won't be executed if the user isn't authorized.
195+
196+
> ⚠️ **Warning**: When you apply a middleware, you simply pass it as a **function**. Above we passed `onlyAdmins` without `()` parentheses. Middleware can be implemented using a configuration function as well, which is a function that takes configuration arguments and returns a middleware function. Read more about it in Express [documentation](https://expressjs.com/en/guide/writing-middleware.html#:~:text=Using%20Express%20middleware.-,Configurable%20middleware,-If%20you%20need).
197+
198+
## **Conclusion**
199+
200+
In this lesson, we learned that everything in Express.js is basically a middleware that gets invoked somewhere. And by using middleware, we can structure our business logic in a clean approach. Middleware can be used for basically everything, and one of their use cases is to enforce authorization.

0 commit comments

Comments
 (0)