From d9bde0c204902c9cf012724c40bb1e8d30e24173 Mon Sep 17 00:00:00 2001 From: Roch Moreau Date: Tue, 22 Nov 2022 15:43:32 +0100 Subject: [PATCH 1/5] Add Workshop 4 Instructions --- Workshop4-Authentication.md | 109 ++++++++++++++++++++++++++++++++++++ 1 file changed, 109 insertions(+) create mode 100644 Workshop4-Authentication.md diff --git a/Workshop4-Authentication.md b/Workshop4-Authentication.md new file mode 100644 index 0000000..abc505f --- /dev/null +++ b/Workshop4-Authentication.md @@ -0,0 +1,109 @@ +# Workshop 4 - User Authentication (Passport, BCrypt) + +## 🌟 Goal + +> Authenticate users and secure access to backend data + +## 👷 Prerequisites + +1. Install dependencies `bcrypt`, `passport`, `passport-local`, `passport-jwt` and `jsonwebtoken` + +## 💡 Concept + +Authentication is used to know which user performs an action. The backend can deny access to resources for +not authenticated users. The authentication can also be used to scope the user's access. + +There are multiple techniques for limiting users permissions, we will see some on another workshop. The goal for +today is to offer registration and authentication capabilities. + +#### Registration + +Anyone can register an account on our backend by giving a `username` and a `password`. +> We NEVER store plaintext passwords ! + +Once a registration request is received, the password is `hashed` with bcrypt, and the user is stored with this +password hash in database. + +Use a `salt` when hashing password, to protect your data against +[Rainbow Table](https://fr.wikipedia.org/wiki/Rainbow_table) attacks (table containing a list of hash with their +non-hashed value) + +#### Login + +A registered user can "Log-In" to our backend by providing its `username` and `password`. + +Once a login request is received, the backend will hash the password and compare it with the hash stored on the user. +If hashes match, the backend will deliver a JSON Web Token (JWT), a proof of authentication containing the user's ID. + +#### Authentication + +For any protected requests, the user must give its JWT (issued by backend on Login). A middleware will check JWT +authenticity and accept or deny the request. + +The JWT is given in request's headers: `Authorization: Bearer eyJh[...]` + +Client (User) -> Makes a `POST /locations` request with its JWT -> JWT Middleware checks JWT: + +1. Case 1: Valid JWT -> Proceed to location creation +2. Case 2: Invalid JWT (expired, unsigned, wrong signature...) -> 403 Error + +You can play with JWT at [JWT.io](https://jwt.io/) + +#### Passport ? Middleware ? What are those ?? + +A middleware is a function called before or after a route handler. Basically, you can have an existing route: + +```javascript +router.get('/locations', (req, res) => res.send(200).body({ locations: [] })) +``` + +Now you want to protect it from unauthenticated users. Middlewares can help ! + +```javascript +router.get('/locations', checkUser, (req, res) => res.send(200).body({ locations: [] })) +``` + +Or, to apply it to multiple routes: + +```javascript +router.use(checkUser) +router.get('/locations', (req, res) => res.send(200).body({ locations: [] })) +``` + +The wonderful thing is that middlewares have access to `req` and `res` objects, meaning they can throw errors before the +actual route handler, or **add data to request!** + +This is where Passport middlewares shines: + +```javascript +router.use(checkUser) // Passport Middleware. If User is authenticated, it is added to req +router.get('/locations', (req, res) => res.send(200).body({ locations: [], user: req.user })) +``` + +## 🗒 What to do + +1. Create a new resource: `Users` + 1. Create a new `users` folder containing a user model, service and controller + 1. A `user` has a **username** and a **password** + 2. Register the `users.controller.js` in Express router (`index.js`). The controller must offer the following + routes: + 1. Register POST `/users/register` + 2. Login POST `/users/login` + 3. Get self GET `/users/me` + 4. Update self PUT/PATCH `/users/me` + 5. Delete self DELETE `/users/me` + 6. Get all GET `/users` (remember to not return users passwords on this route) +2. Implement the User Registration route + 1. Ensure username is unique + 2. Hash the password with bcrypt and save it in Mongo with the username +3. Create a folder to store Passport Strategies (local and JWT strategies) +4. Implement the User Login route with `passport-local` + 1. Use a `passport-local` strategy for this, as a middleware before the **route handler**: + 1. Find the user by its username (404 if not found) + 2. Hash the password with bcrypt and compare it with found user in Mongo (403 if not matching) + 2. **Route handler**: Sign a JWT (`jsonwebtoken` package) containing the user's ID as `sub`. Use a JWT Secret from + your `.env file` +5. Implement a JWT Middleware with a JWT Passport Strategy (`passport-jwt`) + 1. Get User from Mongo with the JWT `sub` data +6. Use the JWT middleware to protect all `/locations` routes +7. Use the JWT middleware to implement and protect the `/users/me` CRUD routes From d5c0cd0632a00794820eff1ccf2914877d314a3d Mon Sep 17 00:00:00 2001 From: Roch Moreau Date: Sun, 11 Dec 2022 22:38:23 +0100 Subject: [PATCH 2/5] Little fixes on Workshop4 MD Add Workshop5 MD instructions --- Workshop4-Authentication.md | 10 +++--- Workshop5-Authorization.md | 70 +++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+), 5 deletions(-) create mode 100644 Workshop5-Authorization.md diff --git a/Workshop4-Authentication.md b/Workshop4-Authentication.md index abc505f..6a5abe0 100644 --- a/Workshop4-Authentication.md +++ b/Workshop4-Authentication.md @@ -89,9 +89,9 @@ router.get('/locations', (req, res) => res.send(200).body({ locations: [], user: routes: 1. Register POST `/users/register` 2. Login POST `/users/login` - 3. Get self GET `/users/me` - 4. Update self PUT/PATCH `/users/me` - 5. Delete self DELETE `/users/me` + 3. Get self GET `/users/me` (implement it later, using JWT Auth) + 4. Update self PUT/PATCH `/users/me` (implement it later, using JWT Auth) + 5. Delete self DELETE `/users/me` (implement it later, using JWT Auth) 6. Get all GET `/users` (remember to not return users passwords on this route) 2. Implement the User Registration route 1. Ensure username is unique @@ -99,8 +99,8 @@ router.get('/locations', (req, res) => res.send(200).body({ locations: [], user: 3. Create a folder to store Passport Strategies (local and JWT strategies) 4. Implement the User Login route with `passport-local` 1. Use a `passport-local` strategy for this, as a middleware before the **route handler**: - 1. Find the user by its username (404 if not found) - 2. Hash the password with bcrypt and compare it with found user in Mongo (403 if not matching) + 1. Find the user by its username + 2. Hash the password with bcrypt and compare it with found user in Mongo (403 if not matching, Passport handles it) 2. **Route handler**: Sign a JWT (`jsonwebtoken` package) containing the user's ID as `sub`. Use a JWT Secret from your `.env file` 5. Implement a JWT Middleware with a JWT Passport Strategy (`passport-jwt`) diff --git a/Workshop5-Authorization.md b/Workshop5-Authorization.md new file mode 100644 index 0000000..14cfb3d --- /dev/null +++ b/Workshop5-Authorization.md @@ -0,0 +1,70 @@ +# Workshop 4 - User Authorization (Middleware) + +## 🌟 Goal + +> Limit user access to resources + +## 💡 Concept + +Authorization is a process used to limit user's access to resources, by applying one or multiple rules. + +We will implement an RBAC (Role Based Access Control) authorization middleware. The purpose of this middleware +is to check whether the User at the origin of the request has a role registered in resource's allowed roles. + +For this, users must have a `role` property, to save their roles. Be careful not to let users update their own role ! +(no privilege elevation) + +And resources (API Endpoints) must declare a list of "Allowed Roles". + +For the middleware to function properly, it must be used after an authentication middleware, to know which user made the request. + +## 🗒 What to do + +1. In the model `Users`, add a `role` property +2. Create a new Authorization middleware. It is a Higher-Order Function taking 1 parameter: `(allowedRoles)` an array of allowed roles, and returning a function with 3 parameters: `(req, res, next)` (express parameters) + 1. This middleware returns a 403 when + 1. No user is given in `req.user` (missing authentication) + 2. User's role is not included in parameter `allowedRoles` +3. Call this middleware on routes to protect + + +## Quick word on Higher-Order Functions + +You probably already heard of them, from math classes of CS classes. Taking Wikipedia's definition: +> [...] a higher-order function (HOF) is a function that does at least one of the following: +> - takes one or more functions as arguments (i.e. a procedural parameter, which is a parameter of a procedure that is itself a procedure), +> - returns a function as its result. + +Today we are working with the second type, HOF that `returns a function as its result` + +In Javascript, they are implemented like this: +```javascript +const roleMiddleware = (allowedRoles) => (req, res, next) => allowedRoles.includes(req.user?.role) ? next() : res.status(403).send() +``` + +Of without arrow functions: +```javascript +function roleMiddleware (allowedRoles) { + return function (req, res, next) { + if (allowedRoles.includes(req.user?.role)) { + return next() + } + return res.status(403).send() + } +} +``` + +## HOF with Express + +The examples above are valid middlewares for express. Express expects functions taking `(req, res, next)` as parameters +this is why your functions handling logic in your `controllers` have these parameters. + +`req` holds all the data received, `res` keeps functions to send a response, and `next` is a function that +sends a signal to `Express` to try and start the next function/middleware in the router configuration. + +When you need to pass some extra arguments to a middleware, like a list of `allowedRoles`, HOF comes handy: +```javascript +router.get('/users/me', authMiddleware, roleMiddleware(['admin']), (res,req) => { + res.status(200).send(req.user) +}) +``` From c9ca4093abf7b30705145441ec2f5435db7b0bd5 Mon Sep 17 00:00:00 2001 From: Roch Moreau Date: Mon, 12 Dec 2022 08:32:29 +0100 Subject: [PATCH 3/5] Fix Workshop 5 Title --- Workshop5-Authorization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Workshop5-Authorization.md b/Workshop5-Authorization.md index 14cfb3d..8b2cc18 100644 --- a/Workshop5-Authorization.md +++ b/Workshop5-Authorization.md @@ -1,4 +1,4 @@ -# Workshop 4 - User Authorization (Middleware) +# Workshop 5 - User Authorization (Middleware) ## 🌟 Goal From eb876aa5e65c334c5926a256f3ccfef5c5d9af7a Mon Sep 17 00:00:00 2001 From: Roch Moreau Date: Mon, 12 Dec 2022 10:16:37 +0100 Subject: [PATCH 4/5] Fix js example in workshop5 MD --- Workshop5-Authorization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Workshop5-Authorization.md b/Workshop5-Authorization.md index 8b2cc18..29df07a 100644 --- a/Workshop5-Authorization.md +++ b/Workshop5-Authorization.md @@ -64,7 +64,7 @@ sends a signal to `Express` to try and start the next function/middleware in the When you need to pass some extra arguments to a middleware, like a list of `allowedRoles`, HOF comes handy: ```javascript -router.get('/users/me', authMiddleware, roleMiddleware(['admin']), (res,req) => { +router.get('/locations', authMiddleware, roleMiddleware(['admin']), (req,res) => { res.status(200).send(req.user) }) ``` From 466d8003e19eb355ae3ac456516a85fe097680a2 Mon Sep 17 00:00:00 2001 From: Roch Moreau Date: Mon, 12 Dec 2022 10:21:02 +0100 Subject: [PATCH 5/5] Better explanation of authMiddleware in Workshop5 --- Workshop5-Authorization.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Workshop5-Authorization.md b/Workshop5-Authorization.md index 29df07a..dadb0ff 100644 --- a/Workshop5-Authorization.md +++ b/Workshop5-Authorization.md @@ -64,7 +64,7 @@ sends a signal to `Express` to try and start the next function/middleware in the When you need to pass some extra arguments to a middleware, like a list of `allowedRoles`, HOF comes handy: ```javascript -router.get('/locations', authMiddleware, roleMiddleware(['admin']), (req,res) => { +router.get('/locations', passport.authenticate(.......), roleMiddleware(['admin']), (req,res) => { res.status(200).send(req.user) }) ```