|
| 1 | +# Adding authorization layer |
| 2 | + |
| 3 | +With authorization, we introduce another level of app security. In this lesson, we will learn how to restrict some users from accessing certain routes of our app. The lesson objectives are: |
| 4 | + |
| 5 | +- Define what is Authorization, and how is it different from Authentication |
| 6 | +- Introduce role-based authorization |
| 7 | +- How to restrict access to routes using authorization |
| 8 | + |
| 9 | +## **Authorization** |
| 10 | + |
| 11 | +- Authorization is a process by which a server determines if the client has permission to use a resource or access a file. |
| 12 | +- Authorization is usually coupled with authentication so that the server has some concept of who the client is that is requesting access. |
| 13 | +- The type of authentication required for authorization may vary; passwords may be required in some cases but not in others. |
| 14 | +- In some cases, there is no authorization; any user may use a resource or access a file simply by asking for it. Most of the web pages on the Internet require no authentication or authorization. |
| 15 | + |
| 16 | +## **Authentication** |
| 17 | + |
| 18 | +- Authentication is used by a server when the server needs to know exactly who is accessing their information or site. |
| 19 | +- Authentication is used by a client when the client needs to know that the server is the system it claims to be. |
| 20 | +- In authentication, the user or computer has to prove its identity to the server or client. |
| 21 | +- Usually, authentication by a server entails the use of a user name and password. Other ways to authenticate can be through cards, retina scans, voice recognition, and fingerprints. |
| 22 | +- Authentication by a client usually involves the server giving a certificate to the client in which a trusted third party such as Verisign or Thawte states that the server belongs to the entity (such as a bank) that the client expects it to. This is required to initiate secure communication (HTTPs) between client and server. |
| 23 | +- Authentication does not determine what tasks the individual can do or what files the individual can see (aka scopes). Authentication merely identifies and verifies who the person or system is. |
| 24 | + |
| 25 | +## **Role-based authorization** |
| 26 | + |
| 27 | +By role-based authorization, we assign a role or roles to the user either when we register them, or by some other mechanism that happens later to elevate a user access level. |
| 28 | + |
| 29 | +In simple terms, we assign an attribute to the User model, that would tell us later when he/she signs in, what is their access level, or role. Based on that, our app can determine whether they can access that resource/endpoint. |
| 30 | + |
| 31 | +Let's assume we have a vlog web app. We have 3 access levels: |
| 32 | + |
| 33 | +- An anonymous user can watch videos and search on the website without signing up. |
| 34 | +- To upload videos or flag content, the user needs to create an account. |
| 35 | +- Our app needs moderators as well, who are users that can delete videos in case they violate the usage agreement, or are inappropriate. |
| 36 | +- And finally, our app needs admins, who have the ability to make moderators by elevating other users to a moderator access level. |
| 37 | + |
| 38 | +From these requirements, we can distinguish our app structure as follows: |
| 39 | + |
| 40 | +- Some endpoints wouldn't have any authentication guard, any request can reach them. Which are endpoints to read a list of videos, watch a specific video, or search for videos. |
| 41 | +- Certain endpoints like `/upload` and `/flag` will need an authentication guard. This is to check if we have a JWT in the request or a `user` in the session. If there isn't, they shouldn't be able to reach that resource. |
| 42 | +- Endpoints like `/delete` would need an authorization guard. Which is a guard that contains the authentication guard, plus it also checks if the user is a `moderator`, before letting them complete the action. Otherwise, it shouldn't let them through. |
| 43 | +- To elevate a user, an endpoint like `/elevate` would have the same authorization guard, but instead it will check if the user is an `admin`. |
| 44 | + |
| 45 | +### **Implementation** |
| 46 | + |
| 47 | +To implement this as simply as possible, we need to first define our roles as numbers. That is mainly to: |
| 48 | + |
| 49 | +- avoid hardcoded strings in our database, |
| 50 | +- it offers better scalability when our app grows and, |
| 51 | +- is easier to search and add more roles in the future |
| 52 | + |
| 53 | +hence, we can define our roles like this: |
| 54 | + |
| 55 | +- User: Role 1 (_default_) |
| 56 | +- Moderator: Role 2 |
| 57 | +- Admin: Role 3 |
| 58 | + |
| 59 | +Then we need to add a new attribute `role_id` to our User model schema: |
| 60 | + |
| 61 | +```js |
| 62 | +// ./models/user.js |
| 63 | +const ROLES = { |
| 64 | + USER: 1, |
| 65 | + MODERATOR: 2, |
| 66 | + ADMIN: 3 |
| 67 | +} |
| 68 | + |
| 69 | +// attributes definition (schema) |
| 70 | +const schema = { |
| 71 | + name: DataTypes.STRING, |
| 72 | + username: { |
| 73 | + type: DataTypes.STRING, |
| 74 | + allowNull: false, |
| 75 | + unique: true, |
| 76 | + }, |
| 77 | + hashed_password: DataTypes.BLOB, // you can store it as char too |
| 78 | + role_id: { |
| 79 | + type: DataTypes.INTEGER, // As we will identify it by numbers |
| 80 | + allowNull: false, // it shouldn't be null |
| 81 | + defaultValue: ROLES.USER, // defaults to 1 |
| 82 | + } |
| 83 | +}, |
| 84 | +``` |
| 85 | + |
| 86 | +With this setup, the registration logic can stay as seen in the previous lesson. Whenever a new user registers, they will by default gain a `role_id: 1` which is for normal users. |
| 87 | + |
| 88 | +> 💡 **TIP:** When you have role-based authorization with this setup, your app needs to start with at least one user seed that has an `admin` access level, usually called the app master. This is to prevent getting locked out of the app. |
| 89 | +
|
| 90 | +Then we need to add 2 instance methods to the `User` class: `isAdmin` and `isModerator`: |
| 91 | + |
| 92 | +```js |
| 93 | +// ./models/user.js |
| 94 | + |
| 95 | +const ROLES = { |
| 96 | + USER: 1, |
| 97 | + MODERATOR: 2, |
| 98 | + ADMIN: 3 |
| 99 | +} |
| 100 | + |
| 101 | +class User extends Model { |
| 102 | + static roles = ROLES; // to be able to access them else where |
| 103 | + ... |
| 104 | + |
| 105 | + // instance method |
| 106 | + async verify(password) { |
| 107 | + ... |
| 108 | + } |
| 109 | + |
| 110 | + isAdmin() { |
| 111 | + return this.role_id === ROLES.ADMIN; |
| 112 | + } |
| 113 | + |
| 114 | + isModerator() { |
| 115 | + return this.role_id === ROLES.MODERATOR; |
| 116 | + } |
| 117 | + |
| 118 | + ... |
| 119 | +} |
| 120 | +``` |
| 121 | + |
| 122 | +> ⚠️ **Warning:** Roles were defined as a JavaScript Object in the file above which isn't a best practice. Usually, they are hardcoded in the database with a `Role` model to prevent accidental tampering with roles. Or they are exported from another file that is well guarded against writing using UNIX `chmod` and `chown`. |
| 123 | +
|
| 124 | +The final change we need to do is to pass the user role when he/she logs in or registers. As seen in the previous lesson, the functions `login` and `register` returns `{ id, name, username }`. We need to add 2 more attributes to that: |
| 125 | + |
| 126 | +```js |
| 127 | +// ./models/user.js |
| 128 | + |
| 129 | +class User extends Model { |
| 130 | + ... |
| 131 | + |
| 132 | + static async register(username, name, password) { |
| 133 | + ... |
| 134 | + return { |
| 135 | + id, |
| 136 | + name, |
| 137 | + username, |
| 138 | + isAdmin: user.isAdmin(), |
| 139 | + isModerator: user.isModerator() |
| 140 | + } |
| 141 | + } |
| 142 | + |
| 143 | + static async login(username, password) { |
| 144 | + ... |
| 145 | + return { |
| 146 | + id, |
| 147 | + name, |
| 148 | + username, |
| 149 | + isAdmin: user.isAdmin(), |
| 150 | + isModerator: user.isModerator() |
| 151 | + } |
| 152 | + } |
| 153 | + |
| 154 | + ... |
| 155 | +} |
| 156 | +``` |
| 157 | + |
| 158 | +With this, our session or JWT will hold these claims. In our authorization guard we can simply check if `user && user.isAdmin` or `user && user.isModerator` and either let them carry out the action or not as follows: |
| 159 | + |
| 160 | +```js |
| 161 | +// ./routes/role.js |
| 162 | +const express = require("express"); |
| 163 | +const router = express.Router(); |
| 164 | +const { User } = require("../db"); // as exported in the db file |
| 165 | + |
| 166 | +router.post("/elevate", async (req, res) => { |
| 167 | + const user = req.session.user; |
| 168 | + |
| 169 | + // Check if the use has signed in, and is admin |
| 170 | + if (user && user.isAdmin) { |
| 171 | + const { subject_id, role_id } = req.body; |
| 172 | + |
| 173 | + // check role_id is correct |
| 174 | + const roles = Object.values(User.roles); |
| 175 | + if (!roles.includes(parseInt(role_id, 10))) { |
| 176 | + res.status(400).json({ |
| 177 | + error: `role_id is incorrect. Accepted values are: ${roles.join(", ")}`, |
| 178 | + }); |
| 179 | + return; // exit |
| 180 | + } |
| 181 | + |
| 182 | + // check subject to be elevated exists |
| 183 | + // we will query users with defaultScope to exclude password |
| 184 | + const subject = await User.findByPk(subject_id); |
| 185 | + |
| 186 | + if (!subject) { |
| 187 | + res.status(400).json({ |
| 188 | + error: `User with id: ${subject_id} cannot be found.`, |
| 189 | + }); |
| 190 | + } else { |
| 191 | + subject.role_id = parseInt(role_id, 10); |
| 192 | + await subject.save(); // save dirty instance to database |
| 193 | + res.status(202).json({ status: "OK" }); |
| 194 | + } |
| 195 | + } else { |
| 196 | + // user is not logged in, and not admin |
| 197 | + res.status(401).json({ |
| 198 | + error: `Unauthorized`, |
| 199 | + }); |
| 200 | + } |
| 201 | +}); |
| 202 | +``` |
| 203 | + |
| 204 | +With this, we've guarded our `/elevate` endpoint against non-admin and non-authenticated access. The same can be applied for other, moderators only, endpoints by checking: |
| 205 | + |
| 206 | +```js |
| 207 | +if (user && (user.isAdmin || user.isModerator)) { |
| 208 | + // carry out the action |
| 209 | +} |
| 210 | +``` |
| 211 | + |
| 212 | +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. |
| 213 | + |
| 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. |
| 215 | + |
| 216 | +## **Conclusion** |
| 217 | + |
| 218 | +In this lesson, we introduced authorization as a new security feature to our app. It would prevent users without the required access level from entering protected endpoints. |
| 219 | + |
| 220 | +Keep in mind that you're not limited to the approach we took above. With backend web development, your options are limitless. We merely took one of the simplest approaches. |
| 221 | + |
| 222 | +For more sophisticated applications with many features, each part of the app can be called a scope, and authorization would be scope-based, not role-based. However, that's a topic for another lesson. |
0 commit comments