-#### 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.