From 3c3a0fdebffce71b36b93d9b2691257670a2d663 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sat, 19 Oct 2024 23:10:15 +0100 Subject: [PATCH 01/34] Write intro and lesson overview --- nodeJS/authentication/sessions.md | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 nodeJS/authentication/sessions.md diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md new file mode 100644 index 00000000000..ba7cf3d360d --- /dev/null +++ b/nodeJS/authentication/sessions.md @@ -0,0 +1,31 @@ +### Introduction + +Now that we've been introduced to cookies and their various uses and properties, let's use them to help us implement something. We want to allow someone to log in once and let the server "remember" them, automatically recognising any future requests from them. + +### Lesson overview + +This section contains a general overview of topics that you will learn in this lesson. + +- Describe what sessions are. +- Explain how sessions and cookies can be used together to persist logins. +- Implement authentication with sessions. +- Use a database as a session store. +- Explain how and why passwords are hashed before being stored. + +### Assignment + +
+ +
+ +### Knowledge check + +The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge. + +- [From inside to outside, what is the order of box-model properties?](#the-box-model) + +### Additional resources + +This section contains helpful links to related content. It isn't required, so consider it supplemental. + +- It looks like this lesson doesn't have any additional resources yet. Help us expand this section by contributing to our curriculum. From e212eb64029c3ccd587a752cfc5f930b3b626f48 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Tue, 5 Nov 2024 14:44:27 +0000 Subject: [PATCH 02/34] Write sessions description --- nodeJS/authentication/sessions.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index ba7cf3d360d..6d16a9d0664 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -12,6 +12,18 @@ This section contains a general overview of topics that you will learn in this l - Use a database as a session store. - Explain how and why passwords are hashed before being stored. +### Persisting logins + +The basic login process (with a username and password) is rather straightforward. The user can submit a form with their username and password, then the server can check the database if that username/password combo exists. If it does, great - it knows who the requester is and can continue with the request as appropriate. If it does not exist, it does not know who the requester is so it can end the request there and then. + +But if someone does successfully "log in", how does the server recognise that the next request that user sends is coming from them? Without a system to persist the login, it'd just be a plain ol' request like any other and could have come from anyone. To handle this, we will use **sessions**. + +### Sessions + +When a user successfully logs in, we can store (serialize) some information about that user, such as their user ID in a database table, somewhere server-side like another database table or some dedicated session store. That session will have its own ID and may also have an expiry time. We can then store that session's ID in a cookie (it doesn't need anything else stored in it) and send it back to the user in the server response. The client now has that cookie with the session ID and can then attach it to any future requests. The server can then check the database for a valid session with the same ID it found in the cookie. If there is a matching session, great - it can extract the serialized user information (deserialize) and continue with the request now it knows who made it. If there is no matching or valid session, like with logging in, we don't know who the user is so we can end the request there. + +This is exactly like having a name badge or access pass at work or some event. The cookie is like an access pass which you (the client) give to a machine or security (the server) and it checks who you are and if you're allowed in or not. You can go home and come back the next day, reusing that pass as many times as you need. + ### Assignment
From 4d23f47e8695638cdba59ff4df44f9df5cf052a9 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sat, 9 Nov 2024 16:00:29 +0000 Subject: [PATCH 03/34] Rearrange section content 'Persisting logins' was a glorified intro anyway --- nodeJS/authentication/sessions.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index 6d16a9d0664..2dbc48ee64c 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -2,6 +2,10 @@ Now that we've been introduced to cookies and their various uses and properties, let's use them to help us implement something. We want to allow someone to log in once and let the server "remember" them, automatically recognising any future requests from them. +The basic login process (with a username and password) is rather straightforward. The user submits a form with their username and password, then the server checks the database if that username/password combo exists. If it does, great - it knows who the requester is and can continue with the rest of the request. If it does not exist, it does not know who the requester is so it can end the request there and then. + +So if someone does successfully "log in", how does the server recognize that the next request that user sends is coming from them? Without a system to persist the login, it'd just be a plain ol' request like any other and could have come from anyone. To handle this, we will use **sessions**. + ### Lesson overview This section contains a general overview of topics that you will learn in this lesson. @@ -12,17 +16,13 @@ This section contains a general overview of topics that you will learn in this l - Use a database as a session store. - Explain how and why passwords are hashed before being stored. -### Persisting logins - -The basic login process (with a username and password) is rather straightforward. The user can submit a form with their username and password, then the server can check the database if that username/password combo exists. If it does, great - it knows who the requester is and can continue with the request as appropriate. If it does not exist, it does not know who the requester is so it can end the request there and then. - -But if someone does successfully "log in", how does the server recognise that the next request that user sends is coming from them? Without a system to persist the login, it'd just be a plain ol' request like any other and could have come from anyone. To handle this, we will use **sessions**. - ### Sessions -When a user successfully logs in, we can store (serialize) some information about that user, such as their user ID in a database table, somewhere server-side like another database table or some dedicated session store. That session will have its own ID and may also have an expiry time. We can then store that session's ID in a cookie (it doesn't need anything else stored in it) and send it back to the user in the server response. The client now has that cookie with the session ID and can then attach it to any future requests. The server can then check the database for a valid session with the same ID it found in the cookie. If there is a matching session, great - it can extract the serialized user information (deserialize) and continue with the request now it knows who made it. If there is no matching or valid session, like with logging in, we don't know who the user is so we can end the request there. +When a user successfully logs in, we can store (serialize) some information about that user, such as their user ID in a database table, somewhere server-side like another database table. That data will have its own ID and may also have an expiry time. We can then store that session's ID in a cookie (it doesn't need anything else stored in it) and send it back to the user in the server response. + +The client now has that cookie with the session ID and can then attach it to any future requests. The server can then check the database for a valid session with the same ID it found in the cookie. If there is a matching session, great - it can extract the serialized user information (deserialize) and continue with the request now it knows who made it. If there is no matching or valid session, like with logging in, we don't know who the user is so we can end the request there. -This is exactly like having a name badge or access pass at work or some event. The cookie is like an access pass which you (the client) give to a machine or security (the server) and it checks who you are and if you're allowed in or not. You can go home and come back the next day, reusing that pass as many times as you need. +This is exactly like having a name badge or access pass at work or some event. The cookie is like an access pass which you (the client) give to a machine or security (the server) and it checks who you are and if you're allowed in or not. You can go home and come back the next day, reusing that pass as many times as you need so long as you still have it and your details are still in the system. ### Assignment From a2d7e44fe162d325ec6d90294e80b2f87240326f Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sat, 9 Nov 2024 16:44:05 +0000 Subject: [PATCH 04/34] Add example app setup instructions --- nodeJS/authentication/sessions.md | 129 ++++++++++++++++++++++++++++++ 1 file changed, 129 insertions(+) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index 2dbc48ee64c..453dbf08a4f 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -24,6 +24,135 @@ The client now has that cookie with the session ID and can then attach it to any This is exactly like having a name badge or access pass at work or some event. The cookie is like an access pass which you (the client) give to a machine or security (the server) and it checks who you are and if you're allowed in or not. You can go home and come back the next day, reusing that pass as many times as you need so long as you still have it and your details are still in the system. +### Implementing sessions + +Let's use [express-session](https://expressjs.com/en/resources/middleware/session.html) to implement a basic session authentication system. For the purpose of streamlining our example, we'll put all of the JavaScript in `app.js` and start with hardcoding a few things. When it comes to your own projects, separate different parts like routes, controllers etc. to their own files and folders as you'll have done before. The same can be done for any config for auth features. + +#### Setup + +First, make a new database within `psql`. We will start by setting up a `users` table: + +```sql +CREATE TABLE users ( + id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY, + username VARCHAR ( 255 ), + password VARCHAR ( 255 ) +); +``` + +Now we'll set up a minimal express app. We'll need the following dependencies installed first: + +```bash +npm install express express-session pg ejs +``` + +Now our `app.js`: + +```javascript +// app.js +const path = require("node:path"); +const { Pool } = require("pg"); +const express = require("express"); +const session = require("express-session"); + +const pool = new Pool({ + // add your db configuration +}); + +const app = express(); +app.set("views", path.join(__dirname, "views")); +app.set("view engine", "ejs"); + +app.use(session({ + secret: "we will explain and change this secret later", + resave: false, + saveUninitialized: false, + cookie: { + httpOnly: true, + maxAge: 2 * 24 * 60 * 60 * 1000, // 2 days + }, +})); +app.use(express.urlencoded({ extended: false })); + +app.get("/", (req, res) => res.render("index")); + +app.listen(3000, () => { + console.log("App listening on port 3000!"); +}); +``` + +And our initial `index.ejs` view: + +```ejs + + + + + + Home + + +

Hello world!

+ + +``` + +### Creating users + +
+ +#### Critical: Validate your user input + +For the sake of brevity, we're omitting the server-side validation step in this example. **Do not omit server-side validation in your projects.** + +
+ +We will need a way for a user to create an account, so let's create a sign up form view: + +```ejs + + + + + + Sign up + + +

Sign Up

+
+ + + + + +
+ + +``` + +And corresponding `GET` and `POST` routes: + +```javascript +app.get("/sign-up", (req, res) => { + res.render("signUp"); +}); + +// or use express-async-handler to ditch the try/catch +app.post("/sign-up", async (req, res, next) => { + try { + await pool.query( + "INSERT INTO users (username, password) VALUES ($1, $2)", + [req.body.username, req.body.password], + ); + res.redirect("/"); + } catch(err) { + return next(err); + } +}); +``` + +Remember, we're omitting the validation step as well as storing the raw password - this is of course unsafe but is just for demonstration purposes. We have also not implemented a way to prevent duplicate usernames, so that's something for you to handle yourself in your projects. Password hashing will be introduced later in this lesson. For now, you should be able to serve your app and visit `/sign-up`, submit the form and be redirected to the home view. Open your database in `psql` and query the users table to see your first user! + ### Assignment
From 657de493d22452169b6f65039f551462f2b2fb32 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sat, 9 Nov 2024 20:56:05 +0000 Subject: [PATCH 05/34] Start login example Only up to the original login process --- nodeJS/authentication/sessions.md | 103 ++++++++++++++++++++++++++++-- 1 file changed, 98 insertions(+), 5 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index 453dbf08a4f..f76cbbc3068 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -110,7 +110,7 @@ For the sake of brevity, we're omitting the server-side validation step in this We will need a way for a user to create an account, so let's create a sign up form view: ```ejs - + @@ -118,7 +118,7 @@ We will need a way for a user to create an account, so let's create a sign up fo Sign up -

Sign Up

+

Sign up

@@ -133,12 +133,12 @@ We will need a way for a user to create an account, so let's create a sign up fo And corresponding `GET` and `POST` routes: ```javascript -app.get("/sign-up", (req, res) => { - res.render("signUp"); +app.get("/signup", (req, res) => { + res.render("signup"); }); // or use express-async-handler to ditch the try/catch -app.post("/sign-up", async (req, res, next) => { +app.post("/signup", async (req, res, next) => { try { await pool.query( "INSERT INTO users (username, password) VALUES ($1, $2)", @@ -153,6 +153,99 @@ app.post("/sign-up", async (req, res, next) => { Remember, we're omitting the validation step as well as storing the raw password - this is of course unsafe but is just for demonstration purposes. We have also not implemented a way to prevent duplicate usernames, so that's something for you to handle yourself in your projects. Password hashing will be introduced later in this lesson. For now, you should be able to serve your app and visit `/sign-up`, submit the form and be redirected to the home view. Open your database in `psql` and query the users table to see your first user! +### Logging in + +Now we have the ability to put users in our database, let's allow them to log in to see a special greeting instead of the generic "Hello world!". We will need the following steps to occur: + +1. Check if the submitted username and password match a user in our users table in our database. +1. If no match is found, end the request there, rejecting the login. Otherwise, serialize the user's ID to a new session. +1. Set a cookie with that session's ID. +1. Respond to the request with the cookie. + +We'll need to start with the login logic itself, so let's create login routes. Remember we're doing everything together for demonstration purposes only; organise and extract code as you see fit. + +```javascript +app.get("/login", (req, res) => { + res.render("login"); +}); + +app.post("/login", async (req, res, next) => { + try { + const { rows } = await pool.query( + "SELECT * FROM users WHERE username = $1", + [req.body.username], + ); + const user = rows[0]; + + if (user?.password === req.body.password) { + req.session.userId = user.id; + res.redirect("/"); + } else { + res.render("login", { error: "Incorrect username or password" }); + } + } catch(err) { + next(err); + } +}); +``` + +What's going on here? First we have our route for rendering the login page. In our `POST` route, we query our db for the submitted ujername. If the username exists *and* the submitted password matches, we serialize the user ID to the session data then redirect to the homepage (if you've never seen `?.` before, check out [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)). Otherwise if no matching username/password combo, we rerender the login page with an error message. Note that we cannot serialize the user ID to `req.session.id` because [`req.session.id` is already used for the session's own ID](http://expressjs.com/en/resources/middleware/session.html#reqsessionid). + +We only need to serialize the user ID as that will never change for a user (unlike a username which could be changed by a user if that feature is implemented). It's the only user info we'll need as later we'll write a middleware that deserializes the user ID from a matching session then query the db with that ID to grab any other user info. This middleware can then be added to the start of any routes that need authenticating. + +
+ +#### Warning: Use generic login error messages + +While it may seem like better UX to inform which field is incorrect when logging in with incorrect credentials, it is better to be vague. If you misspell the username but it happens to match a different user's username, you may not realize if the error message implies only the password is incorrect. It also makes it less likely that malicious people realise a username exists and they only have to guess the password. + +
+ +Let's create the login page: + +```ejs + + + + + + Login + + +

Please log in

+ + + + + + +
+ <% if (locals.error) { %> +

<%= error %>

+ <% } %> + + +``` + +And edit the homepage to show a personalized greeting with a logout button (which we will implement later): + +```ejs + + + + + + Home + + +

Hello, <%= username %>!

+
+ +
+ + +``` + ### Assignment
From 002785719387a5cfe0ddffe389d0347acc666025 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sat, 9 Nov 2024 21:26:58 +0000 Subject: [PATCH 06/34] Handle general authentication middleware --- nodeJS/authentication/sessions.md | 41 +++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index f76cbbc3068..a9cb7516b6e 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -246,6 +246,47 @@ And edit the homepage to show a personalized greeting with a logout button (whic ``` +### Handling post-login requests + +Express-session does some magic for us by automatically checking if an incoming request has a session cookie attached. If so, it will check if there is a valid session that matches the ID and populate `req.session` with any matching session info. If no session cookie or match, a fresh session will be loaded (which will only be saved at the end of the request cycle if it has been modified by us). + +As of now, our `GET /` route will always display the homepage and will crash if someone has not yet logged in! There would not be a cookie and therefore no session to deserialize. We can write a middleware that checks the current loaded session and if it has a user ID in it, we can use it to query the db and grab any user info we need, then continue to the homepage. Otherwise, the user is not authenticated and we can redirect to the login page. + +```javascript +async function checkAuthenticated(req, res, next) { + try { + if (!req.session.userId) { + res.redirect("/login"); + } else { + const { rows } = await pool.query( + "SELECT * FROM users WHERE id = $1", + [req.session.userId], + ); + // add the user details we need to req so we can access it in the next middleware + req.user = { + id: rows[0].id, + username: rows[0].username, + }; + next(); + } + } catch (err) { + next(err); + } +} +``` + +Plonk it in our `GET /` route as the first middleware and there we go! + +```javascript +app.get("/", checkAuthenticated, (req, res) => { + res.render("index", { username: req.user.username }); +}); +``` + +If you serve your app and go to the site on localhost, you should be redirected to `/login` straightaway since you won't yet have a session cookie. You should already have a user in your database, so log in with your credentials and see your personalized greeting! + +We can add our `checkAuthenticated` middleware to any routes that we need authenticate, or even as router-level middleware for example. You can of course customize this function however you wish - the code shown here is just for our example app. + ### Assignment
From ca2d4f91fc56364d192f2976097f4c51370adcd9 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sun, 10 Nov 2024 19:41:01 +0000 Subject: [PATCH 07/34] Rephrase validation feedback warning verbiage --- nodeJS/authentication/sessions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index a9cb7516b6e..312809a1724 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -197,7 +197,7 @@ We only need to serialize the user ID as that will never change for a user (unli #### Warning: Use generic login error messages -While it may seem like better UX to inform which field is incorrect when logging in with incorrect credentials, it is better to be vague. If you misspell the username but it happens to match a different user's username, you may not realize if the error message implies only the password is incorrect. It also makes it less likely that malicious people realise a username exists and they only have to guess the password. +Don't specify which form fields are incorrect when providing validation feedback. Providing specific feedback can allow attackers to target accounts if they know a specific username exists, for example. It also means if you misspell your username but it happens to match someone else's username, you're less likely to be misled into thinking the you entered your username correctly.
From 0cbc9975a0ebe8b773d27214fdb89f3f0ed864e6 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sun, 10 Nov 2024 20:14:30 +0000 Subject: [PATCH 08/34] Explain session options in app.js Reordered setup code blocks for easier content flow. Decided to introduce the session store and secret explanations here instead of their own sections later on (not really needed there). --- nodeJS/authentication/sessions.md | 50 +++++++++++++++++++------------ 1 file changed, 31 insertions(+), 19 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index 312809a1724..4d5725382d6 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -43,17 +43,35 @@ CREATE TABLE users ( Now we'll set up a minimal express app. We'll need the following dependencies installed first: ```bash -npm install express express-session pg ejs +npm install express express-session ejs dotenv pg connect-pg-simple ``` -Now our `app.js`: +Now our initial `index.ejs` view: + +```ejs + + + + + + Home + + +

Hello world!

+ + +``` + +And our `app.js`: ```javascript // app.js +require("dotenv").config(); const path = require("node:path"); const { Pool } = require("pg"); const express = require("express"); const session = require("express-session"); +const pgSession = require("connect-pg-simple")(session); const pool = new Pool({ // add your db configuration @@ -64,38 +82,32 @@ app.set("views", path.join(__dirname, "views")); app.set("view engine", "ejs"); app.use(session({ - secret: "we will explain and change this secret later", + secret: process.env.SESSION_SECRET, resave: false, saveUninitialized: false, cookie: { httpOnly: true, maxAge: 2 * 24 * 60 * 60 * 1000, // 2 days }, + store: new pgSession({ pool: pool }), })); app.use(express.urlencoded({ extended: false })); app.get("/", (req, res) => res.render("index")); -app.listen(3000, () => { - console.log("App listening on port 3000!"); +const PORT = process.env.PORT || 3000; +app.listen(PORT, () => { + console.log(`App listening on port ${PORT}!`); }); ``` -And our initial `index.ejs` view: +Let's talk about our `session` config, which we set for our whole app to use on every request. First, we set a **session secret**, which we set in our `.env` file since it's, well, a secret. This should be some string that's hard to guess - you can use a random key generator online for this - and is used by express-session alongside the session ID to generate a hash and sign the resulting session cookie. Then if an attacker tries to tamper with a session cookie, the signature would no longer match and the server can invalidate it. -```ejs - - - - - - Home - - -

Hello world!

- - -``` +We also turn off `resave` and `saveUninitialized`, which means express-session will only save a session to our database or overwrite an existing one if it gets modified as part of the request. No need to save anything that hasn't been changed. + +We then pass in options for the session cookies that will be created. In the example, we make it inaccessible to JavaScript on the front-end and set a 2-day expiry. You can always use environment variables to set or conditionally set values (e.g. `httpOnly: process.env.NODE_ENV === "prod"` can allow you to access session cookies via front-end JavaScript in development but prevent it in production). + +Lastly, we use the [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) library to make express-session store session data in our database inside a new table. Without this, sessions would be stored in memory by default which would not persist through any server restarts! ### Creating users From d7675468fa1b52d3f135032a87572b603a62f54f Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 13 Nov 2024 02:28:41 +0000 Subject: [PATCH 09/34] Format code blocks for less horizontal scrolling --- nodeJS/authentication/sessions.md | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index 4d5725382d6..53d7c50277d 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -133,7 +133,7 @@ We will need a way for a user to create an account, so let's create a sign up fo

Sign up

- + @@ -193,7 +193,9 @@ app.post("/login", async (req, res, next) => { req.session.userId = user.id; res.redirect("/"); } else { - res.render("login", { error: "Incorrect username or password" }); + res.render("login", { + error: "Incorrect username or password", + }); } } catch(err) { next(err); @@ -227,7 +229,7 @@ Let's create the login page:

Please log in

- + @@ -274,7 +276,8 @@ async function checkAuthenticated(req, res, next) { "SELECT * FROM users WHERE id = $1", [req.session.userId], ); - // add the user details we need to req so we can access it in the next middleware + // add the user details we need to req + // so we can access it in the next middleware req.user = { id: rows[0].id, username: rows[0].username, From 79ef33ef82565d72755965eef7494a4c3bbfd0f7 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 13 Nov 2024 03:04:36 +0000 Subject: [PATCH 10/34] Rearrange session options description New order allows for a more natural way of explaining how express-session populates req.session. That can be explained at the start and options explained to reflect that, instead of explaining options in a bit of a black hole then provide the context later in the lesson. --- nodeJS/authentication/sessions.md | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index 53d7c50277d..759a20c0491 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -82,18 +82,20 @@ app.set("views", path.join(__dirname, "views")); app.set("view engine", "ejs"); app.use(session({ - secret: process.env.SESSION_SECRET, + store: new pgSession({ pool: pool }), resave: false, saveUninitialized: false, + secret: process.env.SESSION_SECRET, cookie: { httpOnly: true, maxAge: 2 * 24 * 60 * 60 * 1000, // 2 days }, - store: new pgSession({ pool: pool }), })); app.use(express.urlencoded({ extended: false })); -app.get("/", (req, res) => res.render("index")); +app.get("/", (req, res) => { + res.render("index"); +}); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { @@ -101,13 +103,13 @@ app.listen(PORT, () => { }); ``` -Let's talk about our `session` config, which we set for our whole app to use on every request. First, we set a **session secret**, which we set in our `.env` file since it's, well, a secret. This should be some string that's hard to guess - you can use a random key generator online for this - and is used by express-session alongside the session ID to generate a hash and sign the resulting session cookie. Then if an attacker tries to tamper with a session cookie, the signature would no longer match and the server can invalidate it. +Let's talk about our session config which we apply to every incoming request (by mounting it on `app`). Firstly, we use the [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) library to make express-session store session data in our database (creating a "session" table if it does not already exist). Without this, sessions would be stored in memory by default which would not persist through any server restarts! -We also turn off `resave` and `saveUninitialized`, which means express-session will only save a session to our database or overwrite an existing one if it gets modified as part of the request. No need to save anything that hasn't been changed. +Then we turn off `resave` and `saveUninitialized`. For every request, express-session will automatically check if a valid session cookie is attached. If a valid session cookie is attached then it deserializes the session data, populating `req.session` with it - we will make use of this later. If no valid session cookie is attached to the request, it will instead create a brand new session object in `req.session`. At the end of the request-response cycle, if the session object has been modified in any way, the session will be saved to the database. Turning off `resave` and `saveUninitialized` makes sure we don't save any unmodified session objects to the database. -We then pass in options for the session cookies that will be created. In the example, we make it inaccessible to JavaScript on the front-end and set a 2-day expiry. You can always use environment variables to set or conditionally set values (e.g. `httpOnly: process.env.NODE_ENV === "prod"` can allow you to access session cookies via front-end JavaScript in development but prevent it in production). +We then set a **session secret**, which we define in our `.env` file since it's, well, a secret. This should be some string that's hard to guess - you can use a random key generator online for this - and is used by express-session alongside the session ID to generate a hash and sign the resulting session cookie. Then if an attacker tries to tamper with a session cookie, the signature would no longer match and the server can invalidate it. -Lastly, we use the [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) library to make express-session store session data in our database inside a new table. Without this, sessions would be stored in memory by default which would not persist through any server restarts! +Lastly, we pass in options for the cookies that will be created by express-session. In our example, we make it inaccessible to JavaScript on the front-end and set a 2-day expiry. You can always use environment variables to set values or even conditionally set them (e.g. `httpOnly: process.env.NODE_ENV === "prod"` can allow you to access session cookies via front-end JavaScript in development but prevent it in production). ### Creating users @@ -163,7 +165,7 @@ app.post("/signup", async (req, res, next) => { }); ``` -Remember, we're omitting the validation step as well as storing the raw password - this is of course unsafe but is just for demonstration purposes. We have also not implemented a way to prevent duplicate usernames, so that's something for you to handle yourself in your projects. Password hashing will be introduced later in this lesson. For now, you should be able to serve your app and visit `/sign-up`, submit the form and be redirected to the home view. Open your database in `psql` and query the users table to see your first user! +Remember, we're omitting the validation step as well as storing the raw password - this is of course unsafe and is solely for demonstration purposes. Password hashing will be introduced later in this lesson. We have also not done anything to prevent duplicate usernames, so that's something for you to handle yourself in your projects. For now, you should be able to serve your app and visit `/sign-up`, submit the form and be redirected to the home view. Open your database in `psql` and query the users table to see your first user! ### Logging in @@ -262,9 +264,7 @@ And edit the homepage to show a personalized greeting with a logout button (whic ### Handling post-login requests -Express-session does some magic for us by automatically checking if an incoming request has a session cookie attached. If so, it will check if there is a valid session that matches the ID and populate `req.session` with any matching session info. If no session cookie or match, a fresh session will be loaded (which will only be saved at the end of the request cycle if it has been modified by us). - -As of now, our `GET /` route will always display the homepage and will crash if someone has not yet logged in! There would not be a cookie and therefore no session to deserialize. We can write a middleware that checks the current loaded session and if it has a user ID in it, we can use it to query the db and grab any user info we need, then continue to the homepage. Otherwise, the user is not authenticated and we can redirect to the login page. +As of now, our `GET /` route will always display the homepage and will crash if someone has not yet logged in! There would not be a cookie and therefore no session to deserialize, so `req.session` would contain a fresh session object without any user properties. We can write a middleware that checks `req.session` and if it has a user ID in it, we can use it to query the db and grab any user info we need, then continue to the homepage. Otherwise, the user is not authenticated and we can redirect to the login page. ```javascript async function checkAuthenticated(req, res, next) { From 2c0da9b8c8360a1a5068843abf5bba4dfccb8b1d Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 13 Nov 2024 04:08:43 +0000 Subject: [PATCH 11/34] Add section on password hashing with argon2id --- nodeJS/authentication/sessions.md | 70 +++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index 759a20c0491..b8d4f966040 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -302,6 +302,76 @@ If you serve your app and go to the site on localhost, you should be redirected We can add our `checkAuthenticated` middleware to any routes that we need authenticate, or even as router-level middleware for example. You can of course customize this function however you wish - the code shown here is just for our example app. +### Storing passwords securely + +The most secure way to store passwords? Don't. Offloading that responsibility to others by letting users log in with their Facebook or Google accounts means you won't need to store passwords on your side at all. That being said, that's very much out of scope right now and not particularly helpful with understanding what generally goes on behind the scenes. + +By far the worst way we can store passwords is to just store them in plaintext, just as we've done in our example app earlier. Even if we encrypted the passwords, all an attacker would need is the key to decrypt all the passwords and let's face it. If someone has managed to gain access to your database, it probably won't be very hard for them to get the encryption key assuming they don't already have it. + +Remember [hash functions](https://www.theodinproject.com/lessons/javascript-hashmap-data-structure#what-is-a-hash-code) from the Hashmap lesson? We want to hash our passwords since they're one-way processes, then store the hash. We also want to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) the password when hashing to prevent identical passwords from being stored with identical hashes (after all, someone out there will use `Password123` as their password... tsk tsk). On top of all that, we also want the hash function to be purposely slow - not so slow that a normal user will be waiting ages just to log in but certainly slow enough to minimise the number of attempts an attacker might be able to do in a given amount of time. + +All of this can be done for us via the [argon2 npm package](https://www.npmjs.com/package/argon2) (which uses the Argon2id function by default as recommended by the Open Worldwide Application Security Project (OWASP)). Fortunately for us, using it doesn't require that much extra. Going back to our `POST /signup` middleware, let's hash our password before we store it: + +```bash +npm install argon2 +``` + +```javascript +const argon2 = require("argon2"); + +app.post("/signup", async (req, res, next) => { + try { + const hashedPassword = await argon2.hash(req.body.password); + await pool.query( + "INSERT INTO users (username, password) VALUES ($1, $2)", + [req.body.username, hashedPassword], + ); + res.redirect("/"); + } catch(err) { + return next(err); + } +}); +``` + +We don't need to set modify any of its options as the defaults are all perfectly sufficient for our use case, including the salt. Now in our `POST /login` middleware, we can also use argon2 to verify the submitted password against the stored salted hash: + +```javascript +app.post("/login", async (req, res, next) => { + try { + const { rows } = await pool.query( + "SELECT * FROM users WHERE username = $1", + [req.body.username], + ); + const user = rows[0]; + + // argon2.verify requires an argon2 hash to compare + // so we must early return if there is no matching user + if (!user) { + return res.render("login", { + error: "Incorrect username or password", + }); + } + + const isMatchingPassword = await argon2.verify( + user.password, + req.body.password + ); + if (isMatchingPassword) { + req.session.userId = user.id; + res.redirect("/"); + } else { + res.render("login", { + error: "Incorrect username or password", + }); + } + } catch(err) { + next(err); + } +}); +``` + +Now when a user signs up, their password is salted and hashed before storage which is then used to verify the password upon login. + ### Assignment
From e5bfc4543c5388d1993ee6ce04e7fde73c3fbbf0 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 13 Nov 2024 04:09:56 +0000 Subject: [PATCH 12/34] Fix signup path --- nodeJS/authentication/sessions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index b8d4f966040..e2c6126a9f2 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -133,7 +133,7 @@ We will need a way for a user to create an account, so let's create a sign up fo

Sign up

- + @@ -165,7 +165,7 @@ app.post("/signup", async (req, res, next) => { }); ``` -Remember, we're omitting the validation step as well as storing the raw password - this is of course unsafe and is solely for demonstration purposes. Password hashing will be introduced later in this lesson. We have also not done anything to prevent duplicate usernames, so that's something for you to handle yourself in your projects. For now, you should be able to serve your app and visit `/sign-up`, submit the form and be redirected to the home view. Open your database in `psql` and query the users table to see your first user! +Remember, we're omitting the validation step as well as storing the raw password - this is of course unsafe and is solely for demonstration purposes. Password hashing will be introduced later in this lesson. We have also not done anything to prevent duplicate usernames, so that's something for you to handle yourself in your projects. For now, you should be able to serve your app and visit `/signup`, submit the form and be redirected to the home view. Open your database in `psql` and query the users table to see your first user! ### Logging in @@ -344,7 +344,7 @@ app.post("/login", async (req, res, next) => { ); const user = rows[0]; - // argon2.verify requires an argon2 hash to compare + // argon2.verify requires an argon2 hash as the first argument // so we must early return if there is no matching user if (!user) { return res.render("login", { From 584b2a4a37cf55fa72709d88170defa7da1ed5e7 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 13 Nov 2024 14:11:24 +0000 Subject: [PATCH 13/34] Add section on logging out Made more sense to talk about logging out after logging in, then talk about password hashing afterwords. --- nodeJS/authentication/sessions.md | 34 ++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index e2c6126a9f2..758230d9e37 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -160,7 +160,7 @@ app.post("/signup", async (req, res, next) => { ); res.redirect("/"); } catch(err) { - return next(err); + next(err); } }); ``` @@ -302,15 +302,35 @@ If you serve your app and go to the site on localhost, you should be redirected We can add our `checkAuthenticated` middleware to any routes that we need authenticate, or even as router-level middleware for example. You can of course customize this function however you wish - the code shown here is just for our example app. +### Logging out + +A user is only "logged in" because their requests have a session cookie attached containing an ID matching a valid session in our db. So to "log out", we can just destroy the session in our database, which will automatically invalidate the client cookie since it will no longer match any sessions. + +To destroy the session, express-session gives us a lovely `req.session.destroy` function. We give it a callback run after the destruction occurs - if there was an error destroying the session, pass control to the error handler middleware, otherwise redirect to the login page. Since the session was destroyed, if a user tried to access the `GET /` route, they would not be authenticated and so will be redirected to the login page. + +```javascript +app.post("/logout", (req, res, next) => { + req.session.destroy((err) => { + if (err) { + next(err); + } else { + res.redirect("/login"); + } + }); +}); +``` + +We should have a working app that allows new users to sign up, log in and log out. Try it out before we move on to talk about password security. + ### Storing passwords securely -The most secure way to store passwords? Don't. Offloading that responsibility to others by letting users log in with their Facebook or Google accounts means you won't need to store passwords on your side at all. That being said, that's very much out of scope right now and not particularly helpful with understanding what generally goes on behind the scenes. +The most secure way to store passwords? Don't. Offloading that responsibility to other systems by letting users log in with their Facebook or Google accounts means you won't need to store passwords on your side at all. That being said, that's very much out of scope right now and doesn't really help us with learning these fundamental behind-the-scenes. -By far the worst way we can store passwords is to just store them in plaintext, just as we've done in our example app earlier. Even if we encrypted the passwords, all an attacker would need is the key to decrypt all the passwords and let's face it. If someone has managed to gain access to your database, it probably won't be very hard for them to get the encryption key assuming they don't already have it. +By far the worst way we can store passwords is to just store them in plaintext like we've done in our example app earlier. Even if we encrypted the passwords, all an attacker would need is the key to decrypt all the passwords. Let's face it, if someone managed to gain access to your database, it probably wouldn't be very hard for them to get the encryption key (assuming they don't already have it). -Remember [hash functions](https://www.theodinproject.com/lessons/javascript-hashmap-data-structure#what-is-a-hash-code) from the Hashmap lesson? We want to hash our passwords since they're one-way processes, then store the hash. We also want to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) the password when hashing to prevent identical passwords from being stored with identical hashes (after all, someone out there will use `Password123` as their password... tsk tsk). On top of all that, we also want the hash function to be purposely slow - not so slow that a normal user will be waiting ages just to log in but certainly slow enough to minimise the number of attempts an attacker might be able to do in a given amount of time. +Remember [hash functions](https://www.theodinproject.com/lessons/javascript-hashmap-data-structure#what-is-a-hash-code) from the Hashmap lesson? We want to hash our passwords then store the hash since hashes are one-way functions. We also want to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) the password when hashing to prevent identical passwords from being stored with identical hashes. On top of all that, we also want the hash function to be purposely slow - not so slow that a normal user will be waiting ages just to log in but certainly slow enough to minimize the number of attempts an attacker might be able to make in a given amount of time. -All of this can be done for us via the [argon2 npm package](https://www.npmjs.com/package/argon2) (which uses the Argon2id function by default as recommended by the Open Worldwide Application Security Project (OWASP)). Fortunately for us, using it doesn't require that much extra. Going back to our `POST /signup` middleware, let's hash our password before we store it: +All of this can be done for us via the [argon2 npm package](https://www.npmjs.com/package/argon2) (which uses the Argon2id function by default as recommended by the Open Worldwide Application Security Project (OWASP)). Fortunately for us, using it doesn't require that much extra work. Going back to our `POST /signup` middleware, let's hash our password before we store it: ```bash npm install argon2 @@ -328,7 +348,7 @@ app.post("/signup", async (req, res, next) => { ); res.redirect("/"); } catch(err) { - return next(err); + next(err); } }); ``` @@ -354,7 +374,7 @@ app.post("/login", async (req, res, next) => { const isMatchingPassword = await argon2.verify( user.password, - req.body.password + req.body.password, ); if (isMatchingPassword) { req.session.userId = user.id; From 1869145d5c1dc12c622d9105eec016f21f22f9b6 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 13 Nov 2024 14:58:23 +0000 Subject: [PATCH 14/34] Add knowledge check questions Use subsections for better content organisation and linking --- nodeJS/authentication/sessions.md | 33 ++++++++++++++++++++++++++----- 1 file changed, 28 insertions(+), 5 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index 758230d9e37..2a708dbba00 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -18,7 +18,7 @@ This section contains a general overview of topics that you will learn in this l ### Sessions -When a user successfully logs in, we can store (serialize) some information about that user, such as their user ID in a database table, somewhere server-side like another database table. That data will have its own ID and may also have an expiry time. We can then store that session's ID in a cookie (it doesn't need anything else stored in it) and send it back to the user in the server response. +A session is just information about a user's interaction with the site in a given time period and can be used to store a whole variety of data. For persisting logins, we can store (serialize) some information about that user, such as their user ID, in a database table. That data will have its own ID and may also have an expiry time. We can then store that session's ID in a cookie (it doesn't need anything else stored in it) and send it back to the user in the server response. The client now has that cookie with the session ID and can then attach it to any future requests. The server can then check the database for a valid session with the same ID it found in the cookie. If there is a matching session, great - it can extract the serialized user information (deserialize) and continue with the request now it knows who made it. If there is no matching or valid session, like with logging in, we don't know who the user is so we can end the request there. @@ -103,11 +103,19 @@ app.listen(PORT, () => { }); ``` +#### Session store + Let's talk about our session config which we apply to every incoming request (by mounting it on `app`). Firstly, we use the [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) library to make express-session store session data in our database (creating a "session" table if it does not already exist). Without this, sessions would be stored in memory by default which would not persist through any server restarts! +#### Prevent unnecessary session saving + Then we turn off `resave` and `saveUninitialized`. For every request, express-session will automatically check if a valid session cookie is attached. If a valid session cookie is attached then it deserializes the session data, populating `req.session` with it - we will make use of this later. If no valid session cookie is attached to the request, it will instead create a brand new session object in `req.session`. At the end of the request-response cycle, if the session object has been modified in any way, the session will be saved to the database. Turning off `resave` and `saveUninitialized` makes sure we don't save any unmodified session objects to the database. -We then set a **session secret**, which we define in our `.env` file since it's, well, a secret. This should be some string that's hard to guess - you can use a random key generator online for this - and is used by express-session alongside the session ID to generate a hash and sign the resulting session cookie. Then if an attacker tries to tamper with a session cookie, the signature would no longer match and the server can invalidate it. +#### Session secret + +We then set a **session secret** which we define in our `.env` file since it's, well, a secret. This should be some string that's hard to guess - you can use a random key generator online for this - and is used by express-session alongside the session ID to generate a hash and sign the resulting session cookie. Then if an attacker tries to tamper with a session cookie, the signature would no longer match and the server can invalidate it. + +#### Cookie options Lastly, we pass in options for the cookies that will be created by express-session. In our example, we make it inaccessible to JavaScript on the front-end and set a 2-day expiry. You can always use environment variables to set values or even conditionally set them (e.g. `httpOnly: process.env.NODE_ENV === "prod"` can allow you to access session cookies via front-end JavaScript in development but prevent it in production). @@ -205,9 +213,11 @@ app.post("/login", async (req, res, next) => { }); ``` -What's going on here? First we have our route for rendering the login page. In our `POST` route, we query our db for the submitted ujername. If the username exists *and* the submitted password matches, we serialize the user ID to the session data then redirect to the homepage (if you've never seen `?.` before, check out [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)). Otherwise if no matching username/password combo, we rerender the login page with an error message. Note that we cannot serialize the user ID to `req.session.id` because [`req.session.id` is already used for the session's own ID](http://expressjs.com/en/resources/middleware/session.html#reqsessionid). +What's going on here? First we have our route for rendering the login page. In our `POST` route, we query our db for the submitted username. If the username exists *and* the submitted password matches, we serialize the user ID to the session data then redirect to the homepage (if you've never seen `?.` before, check out [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)). Express-session automatically sets the cookie and attaches it to the response. -We only need to serialize the user ID as that will never change for a user (unlike a username which could be changed by a user if that feature is implemented). It's the only user info we'll need as later we'll write a middleware that deserializes the user ID from a matching session then query the db with that ID to grab any other user info. This middleware can then be added to the start of any routes that need authenticating. +If no matching username/password combo, we rerender the login page with an error message. Note that we cannot serialize the user ID to `req.session.id` because [`req.session.id` is already used for the session's own ID](http://expressjs.com/en/resources/middleware/session.html#reqsessionid). + +We only need to serialize the user ID as that will never change for a user (unlike a username which could be changed by a user if that feature is implemented). It's the only user info we'll need right now since later we'll write a middleware that uses the user ID to grab any other user info like the current username.
@@ -330,6 +340,8 @@ By far the worst way we can store passwords is to just store them in plaintext l Remember [hash functions](https://www.theodinproject.com/lessons/javascript-hashmap-data-structure#what-is-a-hash-code) from the Hashmap lesson? We want to hash our passwords then store the hash since hashes are one-way functions. We also want to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) the password when hashing to prevent identical passwords from being stored with identical hashes. On top of all that, we also want the hash function to be purposely slow - not so slow that a normal user will be waiting ages just to log in but certainly slow enough to minimize the number of attempts an attacker might be able to make in a given amount of time. +#### Argon2 + All of this can be done for us via the [argon2 npm package](https://www.npmjs.com/package/argon2) (which uses the Argon2id function by default as recommended by the Open Worldwide Application Security Project (OWASP)). Fortunately for us, using it doesn't require that much extra work. Going back to our `POST /signup` middleware, let's hash our password before we store it: ```bash @@ -396,13 +408,24 @@ Now when a user signs up, their password is salted and hashed before storage whi
+1. Bookmark the [express-session] docs if you haven't yet. +1. Watch this wonderful [video about password storage security](https://www.youtube.com/watch?v=qgpsIBLvrGY) from Studying With Alex. +
### Knowledge check The following questions are an opportunity to reflect on key topics in this lesson. If you can't answer a question, click on it to review the material, but keep in mind you are not expected to memorize or master this knowledge. -- [From inside to outside, what is the order of box-model properties?](#the-box-model) +- [What is a session?](#sessions) +- [What library can we use in Express to implement sessions?](#implementing-sessions) +- [Why do we need to set a session secret?](#session-secret) +- [How should the server respond if a user successfully logs in?](#logging-in) +- [After a user has logged in, how can the server recognize them for future requests?](#handling-post-login-requests) +- [What should the server do to "log a user out"?](#logging-out) +- [If we are to store passwords in our database, how can we ensure secure storage?](#storing-passwords-securely) +- [Should passwords be encrypted for storage and why/why not?](#storing-passwords-securely) +- [What library can we use to help us securely store passwords?](#argon2) ### Additional resources From 45bf391d161465f159f0c03d4cb1876cfab64c3a Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:06:51 +0000 Subject: [PATCH 15/34] Emphasise argon2 library defaults meet OWASP recommendations --- nodeJS/authentication/sessions.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index 2a708dbba00..b24c0a53ad9 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -342,7 +342,7 @@ Remember [hash functions](https://www.theodinproject.com/lessons/javascript-hash #### Argon2 -All of this can be done for us via the [argon2 npm package](https://www.npmjs.com/package/argon2) (which uses the Argon2id function by default as recommended by the Open Worldwide Application Security Project (OWASP)). Fortunately for us, using it doesn't require that much extra work. Going back to our `POST /signup` middleware, let's hash our password before we store it: +All of this can be done for us via the [argon2 npm package](https://www.npmjs.com/package/argon2). Fortunately for us, using it doesn't require that much extra work. Going back to our `POST /signup` middleware, let's hash our password before we store it: ```bash npm install argon2 @@ -365,7 +365,7 @@ app.post("/signup", async (req, res, next) => { }); ``` -We don't need to set modify any of its options as the defaults are all perfectly sufficient for our use case, including the salt. Now in our `POST /login` middleware, we can also use argon2 to verify the submitted password against the stored salted hash: +We don't need to set modify any of its options as the defaults all meet the [password storage recommendations set by OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction) (Open Worldwide Application Security Project). Now in our `POST /login` middleware, we can also use argon2 to verify the submitted password against the stored salted hash: ```javascript app.post("/login", async (req, res, next) => { From 03da52fdbe2ad08a10a92f831b368de88290978c Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:09:44 +0000 Subject: [PATCH 16/34] Add login/signup links in views for convenient navigation --- nodeJS/authentication/sessions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index b24c0a53ad9..bc3654925b0 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -26,7 +26,7 @@ This is exactly like having a name badge or access pass at work or some event. T ### Implementing sessions -Let's use [express-session](https://expressjs.com/en/resources/middleware/session.html) to implement a basic session authentication system. For the purpose of streamlining our example, we'll put all of the JavaScript in `app.js` and start with hardcoding a few things. When it comes to your own projects, separate different parts like routes, controllers etc. to their own files and folders as you'll have done before. The same can be done for any config for auth features. +Let's use [express-session](https://expressjs.com/en/resources/middleware/session.html) to implement a basic session authentication system - code along! For the purpose of streamlining our example, we'll put all of the JavaScript in `app.js` and start with hardcoding a few things. When it comes to your own projects, separate different parts like routes, controllers etc. to their own files and folders as you'll have done before. The same can be done for any config for auth features. #### Setup @@ -140,6 +140,7 @@ We will need a way for a user to create an account, so let's create a sign up fo Sign up + Log in

Sign up

@@ -238,6 +239,7 @@ Let's create the login page: Login + Sign up

Please log in

From 77d56e1fd839d8793f2e77bbbdae819c7b8d933a Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:33:11 +0000 Subject: [PATCH 17/34] Fix grammar and US/UK spelling --- nodeJS/authentication/sessions.md | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index bc3654925b0..37162a7cb2b 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -1,8 +1,8 @@ ### Introduction -Now that we've been introduced to cookies and their various uses and properties, let's use them to help us implement something. We want to allow someone to log in once and let the server "remember" them, automatically recognising any future requests from them. +Now that we've been introduced to cookies and their various uses and properties, let's use them to help us implement something. We want to allow someone to log in once and let the server "remember" them, automatically recognizing any future requests from them. -The basic login process (with a username and password) is rather straightforward. The user submits a form with their username and password, then the server checks the database if that username/password combo exists. If it does, great - it knows who the requester is and can continue with the rest of the request. If it does not exist, it does not know who the requester is so it can end the request there and then. +The basic login process (with a username and password) is rather straightforward. The user submits a form with their username and password, then the server checks the database to see if that username/password combo exists. If it does, great - it knows who the requester is and can continue with the rest of the request. If it does not exist, it does not know who the requester is, so it can end the request there and then. So if someone does successfully "log in", how does the server recognize that the next request that user sends is coming from them? Without a system to persist the login, it'd just be a plain ol' request like any other and could have come from anyone. To handle this, we will use **sessions**. @@ -20,13 +20,13 @@ This section contains a general overview of topics that you will learn in this l A session is just information about a user's interaction with the site in a given time period and can be used to store a whole variety of data. For persisting logins, we can store (serialize) some information about that user, such as their user ID, in a database table. That data will have its own ID and may also have an expiry time. We can then store that session's ID in a cookie (it doesn't need anything else stored in it) and send it back to the user in the server response. -The client now has that cookie with the session ID and can then attach it to any future requests. The server can then check the database for a valid session with the same ID it found in the cookie. If there is a matching session, great - it can extract the serialized user information (deserialize) and continue with the request now it knows who made it. If there is no matching or valid session, like with logging in, we don't know who the user is so we can end the request there. +The client now has that cookie with the session ID and can then attach it to any future requests. The server can then check the database for a valid session with the same ID it found in the cookie. If there is a matching session, great - it can extract the serialized user information (deserialize) and continue with the request now it knows who made it. If there is no matching or valid session, like with logging in, we don't know who the user is, so we can end the request there. This is exactly like having a name badge or access pass at work or some event. The cookie is like an access pass which you (the client) give to a machine or security (the server) and it checks who you are and if you're allowed in or not. You can go home and come back the next day, reusing that pass as many times as you need so long as you still have it and your details are still in the system. ### Implementing sessions -Let's use [express-session](https://expressjs.com/en/resources/middleware/session.html) to implement a basic session authentication system - code along! For the purpose of streamlining our example, we'll put all of the JavaScript in `app.js` and start with hardcoding a few things. When it comes to your own projects, separate different parts like routes, controllers etc. to their own files and folders as you'll have done before. The same can be done for any config for auth features. +Let's use [express-session](https://expressjs.com/en/resources/middleware/session.html) to implement a basic session authentication system - code along! For the purpose of streamlining our example, we'll put all of the JavaScript in `app.js` and start with hardcoding a few things. When it comes to your own projects, separate different parts like routes, controllers etc. to their own files and folders as you'll have done before. The same can be done for any configuration for authentication features. #### Setup @@ -40,7 +40,7 @@ CREATE TABLE users ( ); ``` -Now we'll set up a minimal express app. We'll need the following dependencies installed first: +Now we'll set up a minimal Express app. We'll need the following dependencies installed first: ```bash npm install express express-session ejs dotenv pg connect-pg-simple @@ -178,14 +178,14 @@ Remember, we're omitting the validation step as well as storing the raw password ### Logging in -Now we have the ability to put users in our database, let's allow them to log in to see a special greeting instead of the generic "Hello world!". We will need the following steps to occur: +Now that we have the ability to put users in our database, let's allow them to log in to see a special greeting instead of the generic "Hello world!". We will need the following steps to occur: 1. Check if the submitted username and password match a user in our users table in our database. 1. If no match is found, end the request there, rejecting the login. Otherwise, serialize the user's ID to a new session. 1. Set a cookie with that session's ID. 1. Respond to the request with the cookie. -We'll need to start with the login logic itself, so let's create login routes. Remember we're doing everything together for demonstration purposes only; organise and extract code as you see fit. +We'll need to start with the login logic itself, so let's create login routes. Remember we're doing everything together for demonstration purposes only; organize and extract code as you see fit. ```javascript app.get("/login", (req, res) => { @@ -216,15 +216,15 @@ app.post("/login", async (req, res, next) => { What's going on here? First we have our route for rendering the login page. In our `POST` route, we query our db for the submitted username. If the username exists *and* the submitted password matches, we serialize the user ID to the session data then redirect to the homepage (if you've never seen `?.` before, check out [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)). Express-session automatically sets the cookie and attaches it to the response. -If no matching username/password combo, we rerender the login page with an error message. Note that we cannot serialize the user ID to `req.session.id` because [`req.session.id` is already used for the session's own ID](http://expressjs.com/en/resources/middleware/session.html#reqsessionid). +If there is no matching username/password combo, we render the login page again with an error message. Note that we cannot serialize the user ID to `req.session.id` because [`req.session.id` is already used for the session's own ID](http://expressjs.com/en/resources/middleware/session.html#reqsessionid). -We only need to serialize the user ID as that will never change for a user (unlike a username which could be changed by a user if that feature is implemented). It's the only user info we'll need right now since later we'll write a middleware that uses the user ID to grab any other user info like the current username. +We only need to serialize the user ID as that will never change for a user (unlike a username, which could be changed by a user if that feature is implemented). It's the only user info we'll need right now since later we'll write a middleware that uses the user ID to grab any other user info like the current username.
#### Warning: Use generic login error messages -Don't specify which form fields are incorrect when providing validation feedback. Providing specific feedback can allow attackers to target accounts if they know a specific username exists, for example. It also means if you misspell your username but it happens to match someone else's username, you're less likely to be misled into thinking the you entered your username correctly. +Don't specify which form fields are incorrect when providing validation feedback. Providing specific feedback can allow attackers to target accounts if they know a specific username exists, for example. It also means if you misspell your username but it happens to match someone else's username, you're less likely to be misled into thinking you entered your username correctly.
@@ -318,7 +318,7 @@ We can add our `checkAuthenticated` middleware to any routes that we need authen A user is only "logged in" because their requests have a session cookie attached containing an ID matching a valid session in our db. So to "log out", we can just destroy the session in our database, which will automatically invalidate the client cookie since it will no longer match any sessions. -To destroy the session, express-session gives us a lovely `req.session.destroy` function. We give it a callback run after the destruction occurs - if there was an error destroying the session, pass control to the error handler middleware, otherwise redirect to the login page. Since the session was destroyed, if a user tried to access the `GET /` route, they would not be authenticated and so will be redirected to the login page. +To destroy the session, express-session gives us a lovely `req.session.destroy` function. We give it a callback run after the destruction occurs - if there was an error destroying the session, pass control to the error handler middleware, otherwise, redirect to the login page. Since the session was destroyed, if a user tried to access the `GET /` route, they would not be authenticated and so would be redirected to the login page. ```javascript app.post("/logout", (req, res, next) => { @@ -336,11 +336,11 @@ We should have a working app that allows new users to sign up, log in and log ou ### Storing passwords securely -The most secure way to store passwords? Don't. Offloading that responsibility to other systems by letting users log in with their Facebook or Google accounts means you won't need to store passwords on your side at all. That being said, that's very much out of scope right now and doesn't really help us with learning these fundamental behind-the-scenes. +The most secure way to store passwords? Don't. Offloading that responsibility to other systems by letting users log in with their Facebook or Google accounts means you won't need to store passwords on your side at all. That being said, that's very much out of scope right now and doesn't really help us with learning these fundamentals behind the scenes. By far the worst way we can store passwords is to just store them in plaintext like we've done in our example app earlier. Even if we encrypted the passwords, all an attacker would need is the key to decrypt all the passwords. Let's face it, if someone managed to gain access to your database, it probably wouldn't be very hard for them to get the encryption key (assuming they don't already have it). -Remember [hash functions](https://www.theodinproject.com/lessons/javascript-hashmap-data-structure#what-is-a-hash-code) from the Hashmap lesson? We want to hash our passwords then store the hash since hashes are one-way functions. We also want to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) the password when hashing to prevent identical passwords from being stored with identical hashes. On top of all that, we also want the hash function to be purposely slow - not so slow that a normal user will be waiting ages just to log in but certainly slow enough to minimize the number of attempts an attacker might be able to make in a given amount of time. +Remember [hash functions](https://www.theodinproject.com/lessons/javascript-hashmap-data-structure#what-is-a-hash-code) from the Hashmap lesson? We want to hash our passwords, then store the hash since hashes are one-way functions. We also want to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) the password when hashing to prevent identical passwords from being stored with identical hashes. On top of all that, we also want the hash function to be purposely slow - not so slow that a normal user will be waiting ages just to log in but certainly slow enough to minimize the number of attempts an attacker might be able to make in a given amount of time. #### Argon2 @@ -367,7 +367,7 @@ app.post("/signup", async (req, res, next) => { }); ``` -We don't need to set modify any of its options as the defaults all meet the [password storage recommendations set by OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction) (Open Worldwide Application Security Project). Now in our `POST /login` middleware, we can also use argon2 to verify the submitted password against the stored salted hash: +We don't need to modify any of its options, as the defaults all meet the [password storage recommendations set by OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction) (Open Worldwide Application Security Project). Now in our `POST /login` middleware, we can also use argon2 to verify the submitted password against the stored salted hash: ```javascript app.post("/login", async (req, res, next) => { @@ -404,7 +404,7 @@ app.post("/login", async (req, res, next) => { }); ``` -Now when a user signs up, their password is salted and hashed before storage which is then used to verify the password upon login. +Now, when a user signs up, their password is salted and hashed before storage, which is then used to verify the password upon login. ### Assignment From 973251c4de0fecf826e5e3f250e8248f7962c3e4 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 13 Nov 2024 15:47:16 +0000 Subject: [PATCH 18/34] Rename lesson file "Session-based authentication" is a more appropriate title for the lesson than just "Sessions", given that sessions are not exclusively used for auth purposes. --- .../{sessions.md => session_based_authentication.md} | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename nodeJS/authentication/{sessions.md => session_based_authentication.md} (99%) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/session_based_authentication.md similarity index 99% rename from nodeJS/authentication/sessions.md rename to nodeJS/authentication/session_based_authentication.md index 37162a7cb2b..d1349966cc8 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/session_based_authentication.md @@ -310,7 +310,7 @@ app.get("/", checkAuthenticated, (req, res) => { }); ``` -If you serve your app and go to the site on localhost, you should be redirected to `/login` straightaway since you won't yet have a session cookie. You should already have a user in your database, so log in with your credentials and see your personalized greeting! +If you serve your app and go to the site on localhost, you should be redirected to `/login` straight away since you won't yet have a session cookie. You should already have a user in your database, so log in with your credentials and see your personalized greeting! We can add our `checkAuthenticated` middleware to any routes that we need authenticate, or even as router-level middleware for example. You can of course customize this function however you wish - the code shown here is just for our example app. From b9b0b1eafff5024732707d37309b8fa32faa9999 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 13 Nov 2024 16:20:45 +0000 Subject: [PATCH 19/34] Demonstrate NODE_ENV conditional cookie options Makes sense to showcase them directly rather than just via text example after the fact --- nodeJS/authentication/session_based_authentication.md | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/nodeJS/authentication/session_based_authentication.md b/nodeJS/authentication/session_based_authentication.md index d1349966cc8..a62248f5a5a 100644 --- a/nodeJS/authentication/session_based_authentication.md +++ b/nodeJS/authentication/session_based_authentication.md @@ -87,7 +87,8 @@ app.use(session({ saveUninitialized: false, secret: process.env.SESSION_SECRET, cookie: { - httpOnly: true, + httpOnly: process.env.NODE_ENV === 'prod', + secure: process.env.NODE_ENV === 'prod', maxAge: 2 * 24 * 60 * 60 * 1000, // 2 days }, })); @@ -117,7 +118,7 @@ We then set a **session secret** which we define in our `.env` file since it's, #### Cookie options -Lastly, we pass in options for the cookies that will be created by express-session. In our example, we make it inaccessible to JavaScript on the front-end and set a 2-day expiry. You can always use environment variables to set values or even conditionally set them (e.g. `httpOnly: process.env.NODE_ENV === "prod"` can allow you to access session cookies via front-end JavaScript in development but prevent it in production). +Lastly, we pass in options for the cookies that will be created by express-session. In our example, we set a 2-day expiry and conditionally set the `httpOnly` and `secure` properties so that they're only true when in production. This is so that when we're developing locally with localhost, we can still access the cookie via `document.cookie` on the front end if necessary, and still allow the cookie to be set over HTTP (we only want to limit the cookie to HTTPS connections when deployed). ### Creating users From 9c5f8a2d13e8927b7e7aaca2aa27599a5c59c571 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sun, 17 Nov 2024 16:50:03 +0000 Subject: [PATCH 20/34] Force hash verification to run every POST /login Prevent timing attack --- .../session_based_authentication.md | 30 ++++++++++++------- 1 file changed, 19 insertions(+), 11 deletions(-) diff --git a/nodeJS/authentication/session_based_authentication.md b/nodeJS/authentication/session_based_authentication.md index a62248f5a5a..0e5f82b7c6a 100644 --- a/nodeJS/authentication/session_based_authentication.md +++ b/nodeJS/authentication/session_based_authentication.md @@ -368,7 +368,7 @@ app.post("/signup", async (req, res, next) => { }); ``` -We don't need to modify any of its options, as the defaults all meet the [password storage recommendations set by OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction) (Open Worldwide Application Security Project). Now in our `POST /login` middleware, we can also use argon2 to verify the submitted password against the stored salted hash: +We don't need to modify any of its options, as the defaults all meet the [password storage recommendations set by OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction) (Open Worldwide Application Security Project). Now in our `POST /login` middleware, we can also use argon2 to verify the submitted password against the stored salted hash. ```javascript app.post("/login", async (req, res, next) => { @@ -379,19 +379,15 @@ app.post("/login", async (req, res, next) => { ); const user = rows[0]; - // argon2.verify requires an argon2 hash as the first argument - // so we must early return if there is no matching user - if (!user) { - return res.render("login", { - error: "Incorrect username or password", - }); - } - + // argon2.verify requires an argon2 hash as its first arg + // so we can't just pass in `undefined` if no user exists. + // The hash itself doesn't matter as long as it's a valid argon2 hash + // since this is to prevent timing attacks if no user is found const isMatchingPassword = await argon2.verify( - user.password, + user?.password ?? process.env.FALLBACK_HASH, req.body.password, ); - if (isMatchingPassword) { + if (user && isMatchingPassword) { req.session.userId = user.id; res.redirect("/"); } else { @@ -407,6 +403,18 @@ app.post("/login", async (req, res, next) => { Now, when a user signs up, their password is salted and hashed before storage, which is then used to verify the password upon login. +
+ +#### Warning: Timing attacks + +Why does the `POST /login` middleware force `argon2.verify` to run even when no user is found in our database? Why can't we just early return if no user found? + +Just like with [using generic login error messages](#warning-use-generic-login-error-messages), we don't want to reveal that a username is valid and only the corresponding password is incorrect. If no user is found and we return early, then the server will respond quicker than if it had to verify the password against a hash (`argon2.verify` is already designed to account for timing attacks). Attackers could use this timing difference to determine whether a username exists or not, allowing them to focus their efforts on certain usernames - a timing attack. + +You don't need to know all the details of specific attack techniques but in this case, it doesn't take much to ensure that the same process always runs regardless of whether a user exists or not. + +
+ ### Assignment
From db5787aa786b093af0419a07ee25df5beffead59 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sun, 17 Nov 2024 17:06:01 +0000 Subject: [PATCH 21/34] Rename heading Prevent confusion with POST as an HTTP verb --- nodeJS/authentication/session_based_authentication.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nodeJS/authentication/session_based_authentication.md b/nodeJS/authentication/session_based_authentication.md index 0e5f82b7c6a..655900a67ae 100644 --- a/nodeJS/authentication/session_based_authentication.md +++ b/nodeJS/authentication/session_based_authentication.md @@ -275,7 +275,7 @@ And edit the homepage to show a personalized greeting with a logout button (whic ``` -### Handling post-login requests +### Handling requests after login As of now, our `GET /` route will always display the homepage and will crash if someone has not yet logged in! There would not be a cookie and therefore no session to deserialize, so `req.session` would contain a fresh session object without any user properties. We can write a middleware that checks `req.session` and if it has a user ID in it, we can use it to query the db and grab any user info we need, then continue to the homepage. Otherwise, the user is not authenticated and we can redirect to the login page. @@ -368,7 +368,7 @@ app.post("/signup", async (req, res, next) => { }); ``` -We don't need to modify any of its options, as the defaults all meet the [password storage recommendations set by OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction) (Open Worldwide Application Security Project). Now in our `POST /login` middleware, we can also use argon2 to verify the submitted password against the stored salted hash. +We don't need to modify any of its options, as the defaults all meet the [password storage recommendations set by OWASP](https://cheatsheetseries.owasp.org/cheatsheets/Password_Storage_Cheat_Sheet.html#introduction) (Open Worldwide Application Security Project). Now in our `POST /login` middleware, we can also use argon2 to verify the submitted password against the stored salted hash: ```javascript app.post("/login", async (req, res, next) => { @@ -432,7 +432,7 @@ The following questions are an opportunity to reflect on key topics in this less - [What library can we use in Express to implement sessions?](#implementing-sessions) - [Why do we need to set a session secret?](#session-secret) - [How should the server respond if a user successfully logs in?](#logging-in) -- [After a user has logged in, how can the server recognize them for future requests?](#handling-post-login-requests) +- [After a user has logged in, how can the server recognize them for future requests?](#handling-requests-after-login) - [What should the server do to "log a user out"?](#logging-out) - [If we are to store passwords in our database, how can we ensure secure storage?](#storing-passwords-securely) - [Should passwords be encrypted for storage and why/why not?](#storing-passwords-securely) From 9a8148cbc9ba6a8204ed135693d4f9793aa38b78 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Tue, 19 Nov 2024 12:44:59 +0000 Subject: [PATCH 22/34] Improve wording for salt explanation --- nodeJS/authentication/session_based_authentication.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodeJS/authentication/session_based_authentication.md b/nodeJS/authentication/session_based_authentication.md index 655900a67ae..e0e2b395c14 100644 --- a/nodeJS/authentication/session_based_authentication.md +++ b/nodeJS/authentication/session_based_authentication.md @@ -341,7 +341,7 @@ The most secure way to store passwords? Don't. Offloading that responsibility to By far the worst way we can store passwords is to just store them in plaintext like we've done in our example app earlier. Even if we encrypted the passwords, all an attacker would need is the key to decrypt all the passwords. Let's face it, if someone managed to gain access to your database, it probably wouldn't be very hard for them to get the encryption key (assuming they don't already have it). -Remember [hash functions](https://www.theodinproject.com/lessons/javascript-hashmap-data-structure#what-is-a-hash-code) from the Hashmap lesson? We want to hash our passwords, then store the hash since hashes are one-way functions. We also want to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) the password when hashing to prevent identical passwords from being stored with identical hashes. On top of all that, we also want the hash function to be purposely slow - not so slow that a normal user will be waiting ages just to log in but certainly slow enough to minimize the number of attempts an attacker might be able to make in a given amount of time. +Remember [hash functions](https://www.theodinproject.com/lessons/javascript-hashmap-data-structure#what-is-a-hash-code) from the Hashmap lesson? We want to hash our passwords, then store the hash since hashes are one-way functions. We also want to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) the password when hashing so that the identical passwords will produce a different hash each time, preventing attackers from comparing hashes against precomputed hashes of common passwords (often referred to as "rainbow tables"). On top of all that, we also want the hash function to be purposely slow - not so slow that a normal user will be waiting ages just to log in but certainly slow enough to minimize the number of attempts an attacker might be able to make in a given amount of time. #### Argon2 From 93dea16bacf420a0f0753e27c11540079b581ae8 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sun, 16 Mar 2025 19:18:46 +0000 Subject: [PATCH 23/34] Amend verbiage as per review comments Co-authored-by: 01zulfi <85733202+01zulfi@users.noreply.github.com> --- .../authentication/session_based_authentication.md | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/nodeJS/authentication/session_based_authentication.md b/nodeJS/authentication/session_based_authentication.md index e0e2b395c14..f5306bc9480 100644 --- a/nodeJS/authentication/session_based_authentication.md +++ b/nodeJS/authentication/session_based_authentication.md @@ -1,10 +1,10 @@ ### Introduction -Now that we've been introduced to cookies and their various uses and properties, let's use them to help us implement something. We want to allow someone to log in once and let the server "remember" them, automatically recognizing any future requests from them. +Now that we've been introduced to cookies and their various uses and properties, let's use them to help us implement authentication. We want to allow someone to log in once and let the server "remember" them, automatically recognizing any future requests from them. -The basic login process (with a username and password) is rather straightforward. The user submits a form with their username and password, then the server checks the database to see if that username/password combo exists. If it does, great - it knows who the requester is and can continue with the rest of the request. If it does not exist, it does not know who the requester is, so it can end the request there and then. +The basic login process (with a username and password) is rather straightforward. The user submits a form with their username and password, then the server checks the database to see if that username/password combo exists. If it does, great - it knows who the requester is and can continue with the rest of the request. If it does not exist, it does not know who the requester is, so it can unauthorize the request. -So if someone does successfully "log in", how does the server recognize that the next request that user sends is coming from them? Without a system to persist the login, it'd just be a plain ol' request like any other and could have come from anyone. To handle this, we will use **sessions**. +So if someone does successfully "log in", how does the server recognize that the next request that user sends is coming from them? Without a system to persist the login, it'd just be a plain ol' request like any other and could have come from anyone. We *could* send the username and password with every request and authenticate that every time, but as you can imagine, this can be quite cumbersome. To handle this better, we will use **sessions**. ### Lesson overview @@ -20,7 +20,7 @@ This section contains a general overview of topics that you will learn in this l A session is just information about a user's interaction with the site in a given time period and can be used to store a whole variety of data. For persisting logins, we can store (serialize) some information about that user, such as their user ID, in a database table. That data will have its own ID and may also have an expiry time. We can then store that session's ID in a cookie (it doesn't need anything else stored in it) and send it back to the user in the server response. -The client now has that cookie with the session ID and can then attach it to any future requests. The server can then check the database for a valid session with the same ID it found in the cookie. If there is a matching session, great - it can extract the serialized user information (deserialize) and continue with the request now it knows who made it. If there is no matching or valid session, like with logging in, we don't know who the user is, so we can end the request there. +The client now has that cookie with the session ID and can then attach it to any future requests. The server can then check the database for a valid session with the same ID it found in the cookie. If there is a matching session, great - it can extract the serialized user information (deserialize) and continue with the request now it knows who made it. If there is no matching or valid session, like with logging in, we don't know who the user is, so we can unauthorize the request. This is exactly like having a name badge or access pass at work or some event. The cookie is like an access pass which you (the client) give to a machine or security (the server) and it checks who you are and if you're allowed in or not. You can go home and come back the next day, reusing that pass as many times as you need so long as you still have it and your details are still in the system. @@ -106,7 +106,7 @@ app.listen(PORT, () => { #### Session store -Let's talk about our session config which we apply to every incoming request (by mounting it on `app`). Firstly, we use the [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) library to make express-session store session data in our database (creating a "session" table if it does not already exist). Without this, sessions would be stored in memory by default which would not persist through any server restarts! +Let's talk about our session config which we apply to every incoming request (by mounting it on `app`). Firstly, we use the [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) library to make express-session store session data in our database (creating a "session" table if it does not already exist). #### Prevent unnecessary session saving @@ -419,7 +419,7 @@ You don't need to know all the details of specific attack techniques but in this
-1. Bookmark the [express-session] docs if you haven't yet. +1. Bookmark the [express-session](https://expressjs.com/en/resources/middleware/session.html) docs if you haven't yet. 1. Watch this wonderful [video about password storage security](https://www.youtube.com/watch?v=qgpsIBLvrGY) from Studying With Alex.
From 96f4078cc2a98a1a0d445b923e3e83d539890206 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sun, 16 Mar 2025 19:27:38 +0000 Subject: [PATCH 24/34] Make connect-pg-simple create session table if missing Link to connect-pg-simple's SQL queries for this may be useful for learners to look at. --- nodeJS/authentication/session_based_authentication.md | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/nodeJS/authentication/session_based_authentication.md b/nodeJS/authentication/session_based_authentication.md index f5306bc9480..24785f577e0 100644 --- a/nodeJS/authentication/session_based_authentication.md +++ b/nodeJS/authentication/session_based_authentication.md @@ -82,7 +82,10 @@ app.set("views", path.join(__dirname, "views")); app.set("view engine", "ejs"); app.use(session({ - store: new pgSession({ pool: pool }), + store: new pgSession({ + pool: pool, + createTableIfMissing: true, + }), resave: false, saveUninitialized: false, secret: process.env.SESSION_SECRET, @@ -106,7 +109,7 @@ app.listen(PORT, () => { #### Session store -Let's talk about our session config which we apply to every incoming request (by mounting it on `app`). Firstly, we use the [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) library to make express-session store session data in our database (creating a "session" table if it does not already exist). +Let's talk about our session config which we apply to every incoming request (by mounting it on `app`). Firstly, we use the [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) library to make express-session store session data in a "session" table in our database, creating the table if it does not already exist. If you look inside your database in psql, you'll be able to see what the session table looks like. You can also have a look at the [SQL queries for creating the session table](https://github.com/voxpelli/node-connect-pg-simple/blob/HEAD/table.sql). #### Prevent unnecessary session saving From f93b3154c6bf563a3de6e3226366dea688cd758a Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sun, 16 Mar 2025 19:45:41 +0000 Subject: [PATCH 25/34] Expand on serialising to and saving session data Explicit `.save()` call required as per docs to force saving the session before a redirect potentially triggers a page load: https://expressjs.com/en/resources/middleware/session.html#:~:text=Session.save(callback) --- .../session_based_authentication.md | 21 ++++++++++++++++--- 1 file changed, 18 insertions(+), 3 deletions(-) diff --git a/nodeJS/authentication/session_based_authentication.md b/nodeJS/authentication/session_based_authentication.md index 24785f577e0..c58f3301d12 100644 --- a/nodeJS/authentication/session_based_authentication.md +++ b/nodeJS/authentication/session_based_authentication.md @@ -198,15 +198,24 @@ app.get("/login", (req, res) => { app.post("/login", async (req, res, next) => { try { + // query for a matching username const { rows } = await pool.query( "SELECT * FROM users WHERE username = $1", [req.body.username], ); const user = rows[0]; + // if the user exists and the password matches... if (user?.password === req.body.password) { + // serialize the user ID in the session object req.session.userId = user.id; - res.redirect("/"); + req.session.save((err) => { + if (err) { + next(err); + } else { + res.redirect("/"); + } + }); } else { res.render("login", { error: "Incorrect username or password", @@ -218,7 +227,7 @@ app.post("/login", async (req, res, next) => { }); ``` -What's going on here? First we have our route for rendering the login page. In our `POST` route, we query our db for the submitted username. If the username exists *and* the submitted password matches, we serialize the user ID to the session data then redirect to the homepage (if you've never seen `?.` before, check out [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)). Express-session automatically sets the cookie and attaches it to the response. +What's going on here? First we have our route for rendering the login page. In our `POST` route, we query our db for the submitted username. If the username exists *and* the submitted password matches, we serialize the user ID to the session data, save the session, then redirect to the homepage (if you've never seen `?.` before, check out [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)). Express-session automatically sets the cookie and attaches it to the response. If there is no matching username/password combo, we render the login page again with an error message. Note that we cannot serialize the user ID to `req.session.id` because [`req.session.id` is already used for the session's own ID](http://expressjs.com/en/resources/middleware/session.html#reqsessionid). @@ -392,7 +401,13 @@ app.post("/login", async (req, res, next) => { ); if (user && isMatchingPassword) { req.session.userId = user.id; - res.redirect("/"); + req.session.save((err) => { + if (err) { + next(err); + } else { + res.redirect("/"); + } + }); } else { res.render("login", { error: "Incorrect username or password", From ddb093fb1e4148313ada31171b8ec6ad77a10ea6 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Sun, 16 Mar 2025 19:48:11 +0000 Subject: [PATCH 26/34] Remove note box heading prefixes Prefixes only necessary if https://github.com/TheOdinProject/curriculum/pull/28372 gets merged. Until then, they are not necessary. --- nodeJS/authentication/session_based_authentication.md | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/nodeJS/authentication/session_based_authentication.md b/nodeJS/authentication/session_based_authentication.md index c58f3301d12..ba5d6586162 100644 --- a/nodeJS/authentication/session_based_authentication.md +++ b/nodeJS/authentication/session_based_authentication.md @@ -127,7 +127,7 @@ Lastly, we pass in options for the cookies that will be created by express-sessi
-#### Critical: Validate your user input +#### Validate your user input For the sake of brevity, we're omitting the server-side validation step in this example. **Do not omit server-side validation in your projects.** @@ -235,7 +235,7 @@ We only need to serialize the user ID as that will never change for a user (unli
-#### Warning: Use generic login error messages +#### Use generic login error messages Don't specify which form fields are incorrect when providing validation feedback. Providing specific feedback can allow attackers to target accounts if they know a specific username exists, for example. It also means if you misspell your username but it happens to match someone else's username, you're less likely to be misled into thinking you entered your username correctly. @@ -423,11 +423,11 @@ Now, when a user signs up, their password is salted and hashed before storage, w
-#### Warning: Timing attacks +#### Timing attacks Why does the `POST /login` middleware force `argon2.verify` to run even when no user is found in our database? Why can't we just early return if no user found? -Just like with [using generic login error messages](#warning-use-generic-login-error-messages), we don't want to reveal that a username is valid and only the corresponding password is incorrect. If no user is found and we return early, then the server will respond quicker than if it had to verify the password against a hash (`argon2.verify` is already designed to account for timing attacks). Attackers could use this timing difference to determine whether a username exists or not, allowing them to focus their efforts on certain usernames - a timing attack. +Just like with [using generic login error messages](#use-generic-login-error-messages), we don't want to reveal that a username is valid and only the corresponding password is incorrect. If no user is found and we return early, then the server will respond quicker than if it had to verify the password against a hash (`argon2.verify` is already designed to account for timing attacks). Attackers could use this timing difference to determine whether a username exists or not, allowing them to focus their efforts on certain usernames - a timing attack. You don't need to know all the details of specific attack techniques but in this case, it doesn't take much to ensure that the same process always runs regardless of whether a user exists or not. From ebbf606183b58e6bc04db4f1a2ec5e986db23fb5 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Tue, 18 Mar 2025 15:52:02 +0000 Subject: [PATCH 27/34] Streamline verbiage --- nodeJS/authentication/session_based_authentication.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nodeJS/authentication/session_based_authentication.md b/nodeJS/authentication/session_based_authentication.md index ba5d6586162..fea0582bff9 100644 --- a/nodeJS/authentication/session_based_authentication.md +++ b/nodeJS/authentication/session_based_authentication.md @@ -207,7 +207,7 @@ app.post("/login", async (req, res, next) => { // if the user exists and the password matches... if (user?.password === req.body.password) { - // serialize the user ID in the session object + // serialize the user ID in the session object so it can be retrieved later req.session.userId = user.id; req.session.save((err) => { if (err) { @@ -227,7 +227,7 @@ app.post("/login", async (req, res, next) => { }); ``` -What's going on here? First we have our route for rendering the login page. In our `POST` route, we query our db for the submitted username. If the username exists *and* the submitted password matches, we serialize the user ID to the session data, save the session, then redirect to the homepage (if you've never seen `?.` before, check out [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)). Express-session automatically sets the cookie and attaches it to the response. +What's going on here? First we have our route for rendering the login page. In our `POST` route, we query our db for the submitted username. If the username exists *and* the submitted password matches, we serialize the user ID to the session data, save the session, then redirect to the homepage (if you've never seen `?.` before, check out [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)). We do this so we can retrieve the user ID from the session at a later point, such as in a new request. Express-session will then automatically set the cookie and attaches it to the response. If there is no matching username/password combo, we render the login page again with an error message. Note that we cannot serialize the user ID to `req.session.id` because [`req.session.id` is already used for the session's own ID](http://expressjs.com/en/resources/middleware/session.html#reqsessionid). @@ -353,7 +353,7 @@ The most secure way to store passwords? Don't. Offloading that responsibility to By far the worst way we can store passwords is to just store them in plaintext like we've done in our example app earlier. Even if we encrypted the passwords, all an attacker would need is the key to decrypt all the passwords. Let's face it, if someone managed to gain access to your database, it probably wouldn't be very hard for them to get the encryption key (assuming they don't already have it). -Remember [hash functions](https://www.theodinproject.com/lessons/javascript-hashmap-data-structure#what-is-a-hash-code) from the Hashmap lesson? We want to hash our passwords, then store the hash since hashes are one-way functions. We also want to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) the password when hashing so that the identical passwords will produce a different hash each time, preventing attackers from comparing hashes against precomputed hashes of common passwords (often referred to as "rainbow tables"). On top of all that, we also want the hash function to be purposely slow - not so slow that a normal user will be waiting ages just to log in but certainly slow enough to minimize the number of attempts an attacker might be able to make in a given amount of time. +Remember [hash functions](https://www.theodinproject.com/lessons/javascript-hashmap-data-structure#what-is-a-hash-code) from the Hashmap lesson? We want to hash our passwords, then store the hash since hashes are one-way functions. We also want to [salt](https://en.wikipedia.org/wiki/Salt_(cryptography)) the password when hashing so that the identical passwords will produce a different hash each time, preventing attackers from comparing hashes against precomputed hashes of common passwords (often referred to as "rainbow tables"). There are many more things to account for and that's why we have purpose-built algorithms for hashing passwords, such as argon2. #### Argon2 From 624a15d321467cc6758ad203adb4e4e504eea133 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Tue, 15 Apr 2025 23:49:18 +0100 Subject: [PATCH 28/34] Rename lesson file IMHO, pairs a little better with a 'JSON Web Tokens' lesson since it doesn't follow the '*-based authentication' pattern --- .../{session_based_authentication.md => sessions.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename nodeJS/authentication/{session_based_authentication.md => sessions.md} (100%) diff --git a/nodeJS/authentication/session_based_authentication.md b/nodeJS/authentication/sessions.md similarity index 100% rename from nodeJS/authentication/session_based_authentication.md rename to nodeJS/authentication/sessions.md From 165df1013533d4ebec7e4dcfb65394cab98f7d1f Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Thu, 17 Apr 2025 23:35:54 +0100 Subject: [PATCH 29/34] Use full word 'production' Quotes changed to double for lesson consistency --- nodeJS/authentication/sessions.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index fea0582bff9..f4e3d3c46a0 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -13,7 +13,6 @@ This section contains a general overview of topics that you will learn in this l - Describe what sessions are. - Explain how sessions and cookies can be used together to persist logins. - Implement authentication with sessions. -- Use a database as a session store. - Explain how and why passwords are hashed before being stored. ### Sessions @@ -77,6 +76,7 @@ const pool = new Pool({ // add your db configuration }); +const isProduction = process.env.NODE_ENV === "production"; const app = express(); app.set("views", path.join(__dirname, "views")); app.set("view engine", "ejs"); @@ -90,8 +90,8 @@ app.use(session({ saveUninitialized: false, secret: process.env.SESSION_SECRET, cookie: { - httpOnly: process.env.NODE_ENV === 'prod', - secure: process.env.NODE_ENV === 'prod', + httpOnly: isProduction, + secure: isProduction, maxAge: 2 * 24 * 60 * 60 * 1000, // 2 days }, })); From 338923d32272b3c6df9ffddce401584d807bbcb9 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Mon, 21 Apr 2025 10:18:18 +0100 Subject: [PATCH 30/34] Clarify purpose and mechanism behind saving userId to session --- nodeJS/authentication/sessions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index f4e3d3c46a0..8e5411ea31c 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -227,7 +227,7 @@ app.post("/login", async (req, res, next) => { }); ``` -What's going on here? First we have our route for rendering the login page. In our `POST` route, we query our db for the submitted username. If the username exists *and* the submitted password matches, we serialize the user ID to the session data, save the session, then redirect to the homepage (if you've never seen `?.` before, check out [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)). We do this so we can retrieve the user ID from the session at a later point, such as in a new request. Express-session will then automatically set the cookie and attaches it to the response. +What's going on here? First we have our route for rendering the login page. In our `POST` route, we query our db for the submitted username. If the username exists *and* the submitted password matches, we serialize the user ID to the session data, save the session to the database, then redirect to the homepage (if you've never seen `?.` before, check out [optional chaining](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Optional_chaining)). Express-session will then automatically set the cookie and attach it to the response. For subsequent requests, this session (which will be retrieved from the session ID in the cookie) will contain the user ID, which we can use to authenticate. If there is no matching username/password combo, we render the login page again with an error message. Note that we cannot serialize the user ID to `req.session.id` because [`req.session.id` is already used for the session's own ID](http://expressjs.com/en/resources/middleware/session.html#reqsessionid). From 6a6a247b7c50d095bd66099d6d41a54c0523e8b5 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Mon, 21 Apr 2025 11:46:56 +0100 Subject: [PATCH 31/34] Expand on description of session table --- nodeJS/authentication/sessions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index 8e5411ea31c..d818ff95742 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -109,7 +109,9 @@ app.listen(PORT, () => { #### Session store -Let's talk about our session config which we apply to every incoming request (by mounting it on `app`). Firstly, we use the [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) library to make express-session store session data in a "session" table in our database, creating the table if it does not already exist. If you look inside your database in psql, you'll be able to see what the session table looks like. You can also have a look at the [SQL queries for creating the session table](https://github.com/voxpelli/node-connect-pg-simple/blob/HEAD/table.sql). +Let's talk about our session config which we apply to every incoming request (by mounting it on `app`). Firstly, we use the [connect-pg-simple](https://www.npmjs.com/package/connect-pg-simple) library to make express-session store session data in a "session" table in our database, creating the table if it does not already exist. Have a look at the [SQL queries for creating the session table](https://github.com/voxpelli/node-connect-pg-simple/blob/HEAD/table.sql), then go and look inside your database in psql to see what the session table looks like. + +Inside the session table, you should see three columns: "sid", "sess" and "expire". These contain the session's ID, the data stored in the session, and its expiry time respectively. #### Prevent unnecessary session saving From 7048919ec17a759bc9c6fca01545c4bdbdd29f47 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Mon, 21 Apr 2025 15:12:45 +0100 Subject: [PATCH 32/34] Add comment clarifying checkAuthenticated steps else block not necessary with early return --- nodeJS/authentication/sessions.md | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index d818ff95742..20cbc37805e 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -298,19 +298,21 @@ async function checkAuthenticated(req, res, next) { try { if (!req.session.userId) { res.redirect("/login"); - } else { - const { rows } = await pool.query( - "SELECT * FROM users WHERE id = $1", - [req.session.userId], - ); - // add the user details we need to req - // so we can access it in the next middleware - req.user = { - id: rows[0].id, - username: rows[0].username, - }; - next(); + return; } + + // if there is a user ID in the session, grab that matching user + const { rows } = await pool.query( + "SELECT * FROM users WHERE id = $1", + [req.session.userId], + ); + // add the user details we need to req + // so we can access it in the next middleware + req.user = { + id: rows[0].id, + username: rows[0].username, + }; + next(); } catch (err) { next(err); } From ae48efd8b0309f711c6ef90c56b1feeb59ecd909 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Mon, 21 Apr 2025 15:17:17 +0100 Subject: [PATCH 33/34] Remove try/catch due to new Express v5 async behaviour Effectively built-in express-async-handler behaviour now. https://expressjs.com/en/guide/migrating-5.html#rejected-promises --- nodeJS/authentication/sessions.md | 173 +++++++++++++----------------- 1 file changed, 76 insertions(+), 97 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index 20cbc37805e..afc584fc6ee 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -166,17 +166,12 @@ app.get("/signup", (req, res) => { res.render("signup"); }); -// or use express-async-handler to ditch the try/catch app.post("/signup", async (req, res, next) => { - try { - await pool.query( - "INSERT INTO users (username, password) VALUES ($1, $2)", - [req.body.username, req.body.password], - ); - res.redirect("/"); - } catch(err) { - next(err); - } + await pool.query( + "INSERT INTO users (username, password) VALUES ($1, $2)", + [req.body.username, req.body.password], + ); + res.redirect("/"); }); ``` @@ -199,32 +194,28 @@ app.get("/login", (req, res) => { }); app.post("/login", async (req, res, next) => { - try { - // query for a matching username - const { rows } = await pool.query( - "SELECT * FROM users WHERE username = $1", - [req.body.username], - ); - const user = rows[0]; - - // if the user exists and the password matches... - if (user?.password === req.body.password) { - // serialize the user ID in the session object so it can be retrieved later - req.session.userId = user.id; - req.session.save((err) => { - if (err) { - next(err); - } else { - res.redirect("/"); - } - }); - } else { - res.render("login", { - error: "Incorrect username or password", - }); - } - } catch(err) { - next(err); + // query for a matching username + const { rows } = await pool.query( + "SELECT * FROM users WHERE username = $1", + [req.body.username], + ); + const user = rows[0]; + + // if the user exists and the password matches... + if (user?.password === req.body.password) { + // serialize the user ID in the session object so it can be retrieved later + req.session.userId = user.id; + req.session.save((err) => { + if (err) { + next(err); + } else { + res.redirect("/"); + } + }); + } else { + res.render("login", { + error: "Incorrect username or password", + }); } }); ``` @@ -295,27 +286,23 @@ As of now, our `GET /` route will always display the homepage and will crash if ```javascript async function checkAuthenticated(req, res, next) { - try { - if (!req.session.userId) { - res.redirect("/login"); - return; - } - - // if there is a user ID in the session, grab that matching user - const { rows } = await pool.query( - "SELECT * FROM users WHERE id = $1", - [req.session.userId], - ); - // add the user details we need to req - // so we can access it in the next middleware - req.user = { - id: rows[0].id, - username: rows[0].username, - }; - next(); - } catch (err) { - next(err); + if (!req.session.userId) { + res.redirect("/login"); + return; } + + // if there is a user ID in the session, grab that matching user + const { rows } = await pool.query( + "SELECT * FROM users WHERE id = $1", + [req.session.userId], + ); + // add the user details we need to req + // so we can access it in the next middleware + req.user = { + id: rows[0].id, + username: rows[0].username, + }; + next(); } ``` @@ -371,16 +358,12 @@ npm install argon2 const argon2 = require("argon2"); app.post("/signup", async (req, res, next) => { - try { - const hashedPassword = await argon2.hash(req.body.password); - await pool.query( - "INSERT INTO users (username, password) VALUES ($1, $2)", - [req.body.username, hashedPassword], - ); - res.redirect("/"); - } catch(err) { - next(err); - } + const hashedPassword = await argon2.hash(req.body.password); + await pool.query( + "INSERT INTO users (username, password) VALUES ($1, $2)", + [req.body.username, hashedPassword], + ); + res.redirect("/"); }); ``` @@ -388,37 +371,33 @@ We don't need to modify any of its options, as the defaults all meet the [passwo ```javascript app.post("/login", async (req, res, next) => { - try { - const { rows } = await pool.query( - "SELECT * FROM users WHERE username = $1", - [req.body.username], - ); - const user = rows[0]; - - // argon2.verify requires an argon2 hash as its first arg - // so we can't just pass in `undefined` if no user exists. - // The hash itself doesn't matter as long as it's a valid argon2 hash - // since this is to prevent timing attacks if no user is found - const isMatchingPassword = await argon2.verify( - user?.password ?? process.env.FALLBACK_HASH, - req.body.password, - ); - if (user && isMatchingPassword) { - req.session.userId = user.id; - req.session.save((err) => { - if (err) { - next(err); - } else { - res.redirect("/"); - } - }); - } else { - res.render("login", { - error: "Incorrect username or password", - }); - } - } catch(err) { - next(err); + const { rows } = await pool.query( + "SELECT * FROM users WHERE username = $1", + [req.body.username], + ); + const user = rows[0]; + + // argon2.verify requires an argon2 hash as its first arg + // so we can't just pass in `undefined` if no user exists. + // The hash itself doesn't matter as long as it's a valid argon2 hash + // since this is to prevent timing attacks if no user is found + const isMatchingPassword = await argon2.verify( + user?.password ?? process.env.FALLBACK_HASH, + req.body.password, + ); + if (user && isMatchingPassword) { + req.session.userId = user.id; + req.session.save((err) => { + if (err) { + next(err); + } else { + res.redirect("/"); + } + }); + } else { + res.render("login", { + error: "Incorrect username or password", + }); } }); ``` From 18a74a90e126cd3970fdcaa7b1f07b9fe612ab93 Mon Sep 17 00:00:00 2001 From: MaoShizhong <122839503+MaoShizhong@users.noreply.github.com> Date: Wed, 6 Aug 2025 19:39:33 +0100 Subject: [PATCH 34/34] Add note box about "session management" and "session" terminology "Session management" used in later lessons and linked resources to describe the more general concept of persisting user interaction data between requests, even when stateful sessions are not used. --- nodeJS/authentication/sessions.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/nodeJS/authentication/sessions.md b/nodeJS/authentication/sessions.md index afc584fc6ee..7aa5be25efc 100644 --- a/nodeJS/authentication/sessions.md +++ b/nodeJS/authentication/sessions.md @@ -11,13 +11,21 @@ So if someone does successfully "log in", how does the server recognize that the This section contains a general overview of topics that you will learn in this lesson. - Describe what sessions are. -- Explain how sessions and cookies can be used together to persist logins. +- Explain how sessions and cookies can be used together to for session management to persit logins. - Implement authentication with sessions. - Explain how and why passwords are hashed before being stored. ### Sessions -A session is just information about a user's interaction with the site in a given time period and can be used to store a whole variety of data. For persisting logins, we can store (serialize) some information about that user, such as their user ID, in a database table. That data will have its own ID and may also have an expiry time. We can then store that session's ID in a cookie (it doesn't need anything else stored in it) and send it back to the user in the server response. +
+ +#### "Session management" and "sessions" + +Note that from this lesson onwards, "session management" will refer to the general concept of persisting user interaction data between requests (like persisting a login), while "sessions" will refer specifically to the stateful solution discussed in this lesson. Later in the course, we will discuss other ways to handle session management that don't use stateful sessions. + +
+ +A session is just information about a user's interaction with the site in a given time period and can be used to store a whole variety of data. To persist logins, we can store (serialize) some information about that user, such as their user ID, in a database table. That data will have its own ID and may also have an expiry time. We can then store that session's ID in a cookie (it doesn't need anything else stored in it) and send it back to the user in the server response. The client now has that cookie with the session ID and can then attach it to any future requests. The server can then check the database for a valid session with the same ID it found in the cookie. If there is a matching session, great - it can extract the serialized user information (deserialize) and continue with the request now it knows who made it. If there is no matching or valid session, like with logging in, we don't know who the user is, so we can unauthorize the request.