Skip to content
Draft
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
64 changes: 64 additions & 0 deletions courses/backend/node/module-materials/examples/auth-jwt.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
import express from "express";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";

const JWT_SECRET = process.env.JWT_SECRET || "development-secret";

// Example in-memory "database" for teaching purposes only
const users = [
{
id: 1,
username: "alice",
// bcrypt.hashSync("password123", 10)
password_hash: "$2b$10$exampleexampleexampleexampleexampleexa",
},
];

function getUserByUsername(username) {
return users.find((user) => user.username === username) ?? null;
}

const app = express();
app.use(express.json());

app.post("/login", async (req, res) => {
const { username, password } = req.body;

const user = getUserByUsername(username);
if (!user) {
return res.status(401).json({ error: "Invalid credentials" });
}

const isMatch = await bcrypt.compare(password, user.password_hash);
if (!isMatch) {
return res.status(401).json({ error: "Invalid credentials" });
}

const token = jwt.sign({ userId: user.id }, JWT_SECRET, { expiresIn: "1h" });
res.json({ token });
});

function requireJwtAuth(req, res, next) {
const authHeader = req.headers.authorization;
const token = authHeader?.split(" ")[1];
if (!token) {
return res.status(401).json({ error: "No token provided" });
}

try {
const decoded = jwt.verify(token, JWT_SECRET);
req.user = { id: decoded.userId };
next();
} catch (err) {
return res.status(401).json({ error: "Invalid or expired token" });
}
}

app.get("/protected", requireJwtAuth, (req, res) => {
res.json({ data: "Top secret snippets", userId: req.user.id });
});

app.listen(3001, () => {
console.log("> Ready on http://localhost:3001 (JWT auth example)");
});

Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import express from "express";
import bcrypt from "bcrypt";

// Example in-memory "database" for teaching purposes only
const users = [
{
id: 1,
username: "alice",
// bcrypt.hashSync("password123", 10)
password_hash: "$2b$10$exampleexampleexampleexampleexampleexa",
},
];

function getUserByUsername(username) {
return users.find((user) => user.username === username) ?? null;
}

const app = express();
app.use(express.json());

app.post("/login", async (req, res) => {
const { username, password } = req.body;

const user = getUserByUsername(username);
if (!user) {
return res.status(401).json({ error: "Invalid credentials" });
}

const isMatch = await bcrypt.compare(password, user.password_hash);
if (!isMatch) {
return res.status(401).json({ error: "Invalid credentials" });
}

res.json({ message: "Login successful", userId: user.id });
});

app.listen(3000, () => {
console.log("> Ready on http://localhost:3000 (bcrypt login example)");
});

60 changes: 60 additions & 0 deletions courses/backend/node/module-materials/examples/auth-sessions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
import express from "express";
import session from "express-session";

// Example in-memory "database" for teaching purposes only
const users = [
{
id: 1,
username: "alice",
password: "password123",
},
];

function getUserByUsername(username) {
return users.find((user) => user.username === username) ?? null;
}

const app = express();
app.use(express.json());

app.use(
session({
secret: process.env.SESSION_SECRET || "development-session-secret",
resave: false,
saveUninitialized: false,
}),
);

app.post("/login-session", (req, res) => {
const { username, password } = req.body;

const user = getUserByUsername(username);
if (!user || user.password !== password) {
return res.status(401).json({ error: "Invalid credentials" });
}

req.session.userId = user.id;
res.json({ message: "Logged in with session" });
});

function requireSessionAuth(req, res, next) {
if (req.session.userId) {
return next();
}
res.status(401).json({ error: "Not authenticated" });
}

app.get("/protected-session", requireSessionAuth, (req, res) => {
res.json({ data: "Session-protected snippets", userId: req.session.userId });
});

app.post("/logout-session", (req, res) => {
req.session.destroy(() => {
res.json({ message: "Logged out" });
});
});

app.listen(3002, () => {
console.log("> Ready on http://localhost:3002 (session auth example)");
});

21 changes: 21 additions & 0 deletions courses/backend/node/week3/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@

# Node (Week 3) – API Security & Authentication

In this session we will focus on securing our existing Snippets API. We will explore different ways of authenticating users and protecting API endpoints, and compare their trade-offs so you can choose the right approach for different scenarios.

## Contents

- [Preparation](./preparation.md)
- [Session Plan](./session-plan.md) (for mentors)
- [Assignment](./assignment.md)

## Session Learning goals

By the end of this session, you will be able to:

- [ ] Explain why storing plaintext passwords is insecure and how hashing (e.g. with bcrypt) improves security.
- [ ] Implement a basic login flow for the Snippets API using securely stored passwords.
- [ ] Protect Snippets API endpoints using JWT-based stateless authentication.
- [ ] Protect Snippets API endpoints using session-based authentication with cookies.
- [ ] Describe when to use database-stored tokens and API keys, and understand their trade-offs.
- [ ] Compare the strengths and weaknesses of credentials-only, DB tokens, JWT, sessions, and API keys for different use cases.
159 changes: 159 additions & 0 deletions courses/backend/node/week3/assignment.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,159 @@
# Assignment

In this assignment, you will extend the **Snippets API** to support multiple authentication mechanisms and reflect on their trade-offs.

You will:

- Solidify the secure password and login flow you implemented in class.
- Add database-stored tokens on top of the existing login logic.
- Deepen either your JWT or session-based authentication (or both, if you have time).
- Add a simple API-key-protected machine-style endpoint.

## Setup

1. Go to/create a `node/week3` directory in your `hyf-assignment` repo.
2. Copy or link your existing Snippets API code from week 2 (or the provided starter), so that you have:
- A working database connection and Knex setup.
- Existing snippets-related endpoints under `/api/snippets`.
3. Make sure your database contains the required tables for the Snippets API and that you can run it locally without errors.
4. Add (or confirm) a `users` table to your database with at least:
- `id`
- `username` (unique)
- `password_hash`
5. Ensure you have environment variables (or config values) for:
- `JWT_SECRET`
- `SESSION_SECRET`
- `API_KEY` (for the machine-style endpoint).

> If you are missing any of these pieces, revisit the week 3 session materials and implement the in-class steps first before moving on.

---

## Part 1 – Solidify in-class implementation

Start from the state of your Snippets API at the end of the week 3 session.

Your goals:

- Make sure the secure `/login` endpoint using bcrypt is correctly implemented.
- Make sure at least one auth mechanism (JWT or sessions) from the session is working reliably.

### Requirements

- Verify that:
- `users` table exists and contains at least one user with a hashed password.
- `/login`:
- Looks up the user by username (or email).
- Uses `bcrypt.compare` to check the password.
- Returns appropriate HTTP status codes on success (`200`) and failure (`401`).
- If using **JWT**:
- The login route issues a JWT signed with `JWT_SECRET`.
- A middleware verifies the token and attaches user info to `req.user`.
- If using **sessions**:
- Session middleware is configured with `SESSION_SECRET`.
- Login sets `req.session.userId`.
- A middleware checks for `req.session.userId` and rejects unauthenticated requests.
- Add or update error handling so that:
- You do not leak sensitive details in responses.
- You log enough server-side information to debug issues.

Document briefly (e.g. in comments or a short `AUTH_NOTES.md`) which auth mechanism you have working at this stage.

---

## Part 2 – Database-stored tokens

Next, add **database-stored tokens** to your Snippets API, in addition to your existing mechanism.

### Requirements

1. Create a `tokens` table with at least:
- `id` (primary key)
- `user_id` (foreign key to `users.id`)
- `token` (unique string)
- `created_at` (timestamp)
- `expires_at` (timestamp, optional)
2. Implement a `/login-token` route that:
- Reuses your secure username/password check.
- Generates a random token (for example using `crypto.randomBytes`).
- Stores the token and user ID in the `tokens` table.
- Returns the token to the client in JSON.
3. Implement `authToken` middleware that:
- Reads the `Authorization` header (`Bearer <token>`).
- Looks up the token in the `tokens` table.
- (Optionally) checks `expires_at`.
- Attaches the user to `req.user` or returns `401` on failure.
4. Protect at least **two existing Snippets API endpoints** using `authToken`, for example:
- `POST /api/snippets`
- `DELETE /api/snippets/:id`
5. Implement a `/logout-token` route that:
- Deletes or invalidates the token record from the `tokens` table.

---

## Part 3 – Deepen JWT or sessions (or both)

Choose **at least one** mechanism to deepen: **JWT** or **sessions**.

### Option A – Deepen JWT

If you choose JWT, extend your implementation by:

- Improving error handling (e.g. distinguish between missing, invalid, and expired tokens in a safe way).
- Adding at least one of:
- Short-lived access tokens plus a simple refresh token flow.
- Role-based checks in middleware (e.g. only certain users can delete snippets).

### Option B – Deepen sessions

If you choose sessions, extend your implementation by:

- Improving session configuration (cookie options, lifetime, etc.).
- Ensuring proper session destruction on logout.
- Considering what would be needed to run the app on multiple servers (e.g. shared session storage) and documenting your thoughts.

### Requirements

- Clearly document (in code comments or `AUTH_NOTES.md`) how to:
- Obtain credentials and log in.
- Use the improved mechanism (which headers/cookies are expected).
- Log out or otherwise invalidate access.

You may work on both JWT and sessions if you have time, but it is acceptable to focus deeply on one.

---

## Part 4 – API-key-protected machine endpoint

Finally, add a simple **API-key-protected endpoint** intended for machine-to-machine use.

### Requirements

1. Choose or create a route that makes sense for a machine client, for example:
- `GET /api/metrics`
- `GET /api/health`
- `GET /api/snippets/export`
2. Introduce an environment variable such as `API_KEY`.
3. Implement middleware (e.g. `requireApiKey`) that:
- Reads the `x-api-key` header.
- Compares it with `API_KEY`.
- Returns `401` on missing/incorrect keys.
4. Protect your machine-style route with this middleware.

### Optional stretch: basic rate limiting

If you have time, add a very simple in-memory rate-limiting mechanism (for example, per API key) and document the limitations of such an approach.

---

## Reflection

Add a short reflection section to your repository (for example in `AUTH_NOTES.md` or at the bottom of this file) where you answer the following questions in a few sentences or bullet points each:

1. Which auth mechanism would you choose for:
- A SPA web app with many users?
- A microservice-to-microservice communication scenario?
- An internal admin tool used by a small team?
2. Why would you **not** use the other mechanisms in those scenarios?
3. What is one security improvement you would like to make next if you had more time?

15 changes: 15 additions & 0 deletions courses/backend/node/week3/preparation.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# Preparation

//TODO: link resources

- Make sure you can run the **Snippets API** locally // TODO: after steamlining week 1 and 2
- 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.

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