Authentication

Authentication & Authorization

Security, ORMs, & APIs

CSCI 4513

Week 15, Lecture 26

Today's Learning Objectives

Norse Mythology Connection

The Halls of Valhalla: Choosers and Warriors

In Asgard stands Valhalla, Odin's magnificent hall where only the worthy may enter. Valkyries choose the fallen warriors (authentication), grant them entry (authorization), and assign them roles (permissions)—just as we authenticate users, authorize access, and manage permissions.

The Tale:

Valhalla's golden shields gleam in the eternal light of Asgard. At its gates stand the Valkyries, Odin's shield-maidens, who have chosen the brave warriors from battlefields across Midgard. But entry is not automatic—each warrior must prove their identity and worthiness.

Norse Mythology Connection

The Authentication Process

The Valkyrie's Verification: First, the Valkyrie verifies the warrior's identity—did they truly fall in glorious battle? This is authentication (proving who you are). She checks their battle scars, their weapons, their stories against the records in Odin's great hall.

Granting Entry: Once verified, the warrior is granted entry through Valhalla's massive gates. This is authorization (what you're allowed to do). But not all warriors have the same privileges.

The Einherjar's Ranks: Some warriors become Einherjar—the honored dead who will fight at Ragnarök. These have special privileges: access to Odin's table, training with the gods, and knowledge of secret battle tactics. Others are guests, welcome but with limited access. This represents different authorization levels (regular users, members, admins).

Norse Mythology Connection

The Session Token

The Golden Armband: Each verified warrior receives a golden armband—a token of their authenticated status. This armband is their session token. When they approach the mead hall or the training grounds, guards check their armband rather than re-verifying their entire battle history every time.

Encrypted Secrets: Some warriors carry messages from Odin, sealed with magic runes that only certain recipients can read. These sealed messages are like JWT tokens—encoded information that proves authenticity without needing to query Odin every time.

Our Lesson: Today we build Valhalla. We create gates (login forms), station Valkyries (Passport.js), forge armbands (sessions), craft rune-sealed messages (JWTs), and organize the halls (authorization levels). By lesson's end, you'll guard your applications as fiercely as Valhalla guards its warriors.

Part 1: Authentication Basics

Building Secure User Systems

Authentication is crucial for modern web applications. We'll use Passport.js, an authentication middleware for Express, along with bcrypt for password security.

Authentication Overview

Key Concepts

Setting Up Dependencies

Required Packages

# Install required dependencies
npm install express express-session pg passport passport-local ejs bcryptjs

# express-session: Session management
# passport: Authentication middleware
# passport-local: Username/password strategy
# bcryptjs: Password hashing

Database Setup

Creating Users Table

-- Create users table in PostgreSQL
CREATE TABLE users (
   id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
   username VARCHAR ( 255 ),
   password VARCHAR ( 255 )
);
⚠️ Important: We'll start with plain text passwords for demonstration, but will add bcrypt hashing before finishing!

Basic Express Setup

app.js

const path = require("node:path");
const { Pool } = require("pg");
const express = require("express");
const session = require("express-session");
const passport = require("passport");
const LocalStrategy = require('passport-local').Strategy;

const pool = new Pool({
  host: "localhost",
  user: "your_user",
  database: "your_database",
  password: "your_password",
  port: 5432
});

const app = express();
app.set("views", path.join(__dirname, "views"));
app.set("view engine", "ejs");

app.use(session({
  secret: "cats",
  resave: false,
  saveUninitialized: false
}));
app.use(passport.session());
app.use(express.urlencoded({ extended: false }));

Understanding Middleware Order

Sequence Matters!

// 1. Session middleware FIRST
app.use(session({ secret: "cats", resave: false, saveUninitialized: false }));

// 2. Passport session SECOND (depends on express-session)
app.use(passport.session());

// 3. Body parser for form data
app.use(express.urlencoded({ extended: false }));

// 4. Routes come AFTER middleware setup
Why this order? Passport's session management depends on express-session being initialized first. Think of it like layers—each layer builds on the previous one.

Creating Users: Sign-Up Form

views/sign-up-form.ejs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Sign Up</title>
</head>
<body>
  <h1>Sign Up</h1>
  <form action="/sign-up" method="POST">
    <label for="username">Username</label>
    <input id="username" name="username"
           placeholder="username" type="text" />

    <label for="password">Password</label>
    <input id="password" name="password" type="password" />

    <button type="submit">Sign Up</button>
  </form>
</body>
</html>

Sign-Up Route

Creating New Users

// GET route to show form
app.get("/sign-up", (req, res) => res.render("sign-up-form"));

// POST route to create user
app.post("/sign-up", 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) {
    return next(err);
  }
});
🚨 WARNING: This stores passwords in PLAIN TEXT! Never do this in production. We'll fix this with bcrypt soon.

Passport.js Strategies

What are Strategies?

Strategies are authentication mechanisms. Passport.js has over 500 strategies:

  • LocalStrategy: Username & password (what we'll use)
  • OAuth: Google, Facebook, GitHub login
  • JWT: JSON Web Tokens for APIs
  • SAML: Enterprise single sign-on
  • ...and many more!

Each strategy defines how to verify credentials.

Setting Up LocalStrategy

Verifying Credentials

passport.use(
  new LocalStrategy(async (username, password, done) => {
    try {
      const { rows } = await pool.query(
        "SELECT * FROM users WHERE username = $1",
        [username]
      );
      const user = rows[0];

      if (!user) {
        return done(null, false, { message: "Incorrect username" });
      }
      if (user.password !== password) {
        return done(null, false, { message: "Incorrect password" });
      }
      return done(null, user);
    } catch(err) {
      return done(err);
    }
  })
);

Understanding the LocalStrategy

How the done() Callback Works

// done() has three parameters: done(error, user, info)

// 1. Authentication error (database error, etc.)
return done(err);  // Something went wrong

// 2. Authentication failed (wrong credentials)
return done(null, false, { message: "Incorrect username" });
// error: null, user: false, info: error message

// 3. Authentication successful
return done(null, user);
// error: null, user: user object
You don't call this function directly—Passport calls it when you use passport.authenticate()

Sessions and Serialization

Maintaining Logged-In State

The Problem: HTTP is stateless. Each request is independent. How do we remember that a user is logged in?

The Solution: Sessions and cookies!

  1. User logs in successfully
  2. Server creates a session and stores user ID
  3. Server sends session ID to browser as a cookie
  4. Browser sends cookie with every request
  5. Server reads cookie, retrieves session, knows who's logged in

Serialize User

Storing User in Session

passport.serializeUser((user, done) => {
  done(null, user.id);  // Store only user ID in session
});

// When user logs in:
// 1. LocalStrategy returns user object
// 2. serializeUser runs and stores user.id in session
// 3. Session data saved, cookie sent to browser
Why only the ID? Sessions should be small. We can retrieve the full user data later using the ID.

Deserialize User

Retrieving User from Session

passport.deserializeUser(async (id, done) => {
  try {
    const { rows } = await pool.query(
      "SELECT * FROM users WHERE id = $1",
      [id]
    );
    const user = rows[0];
    done(null, user);  // Attaches user to req.user
  } catch(err) {
    done(err);
  }
});

// On subsequent requests:
// 1. Browser sends cookie with session ID
// 2. Server reads session, gets user ID
// 3. deserializeUser runs, queries database for user
// 4. User object attached to req.user
// 5. You can access req.user in your routes!

The Session Flow

Putting It All Together

Login (First Request):

User submits credentials
  → LocalStrategy verifies username/password
  → Returns user object
  → serializeUser stores user.id in session
  → Session cookie sent to browser

Subsequent Requests:

Browser sends session cookie
  → Server reads session ID from cookie
  → Server retrieves user ID from session
  → deserializeUser queries database for user
  → User object attached to req.user
  → Route handler has access to req.user

Login Form

Updating index.ejs

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>Home</title>
</head>
<body>
  <h1>please log in</h1>
  <form action="/log-in" method="POST">
    <label for="username">Username</label>
    <input id="username" name="username"
           placeholder="username" type="text" />

    <label for="password">Password</label>
    <input id="password" name="password" type="password" />

    <button type="submit">Log In</button>
  </form>
</body>
</html>

Login Route

The Magic of passport.authenticate()

app.post(
  "/log-in",
  passport.authenticate("local", {
    successRedirect: "/",
    failureRedirect: "/"
  })
);

// This ONE line does SO MUCH:
// 1. Reads username/password from req.body
// 2. Runs LocalStrategy to verify credentials
// 3. Calls serializeUser to store user ID in session
// 4. Creates session cookie
// 5. Redirects based on success/failure
Note: Passport looks for username and password fields in the form by default.

Checking if User is Logged In

Using req.user in Routes

app.get("/", (req, res) => {
  res.render("index", { user: req.user });
  // req.user will be:
  // - undefined if not logged in
  // - user object if logged in
});

Updated views/index.ejs:

<% if (locals.user) {%>
  <h1>WELCOME BACK <%= user.username %></h1>
  <a href="/log-out">LOG OUT</a>
<% } else { %>
  <h1>please log in</h1>
  <!-- login form here -->
<%}%>

Logout Route

Destroying the Session

app.get("/log-out", (req, res, next) => {
  req.logout((err) => {
    if (err) {
      return next(err);
    }
    res.redirect("/");
  });
});

// req.logout() is provided by Passport
// It destroys the session and clears the cookie

Using res.locals for Current User

Custom Middleware

// Add this middleware AFTER passport initialization
app.use((req, res, next) => {
  res.locals.currentUser = req.user;
  next();
});

// Now in ALL views, you can access currentUser
// WITHOUT passing it manually to each render()!

In views:

<% if (currentUser) { %>
  <p>Welcome, <%= currentUser.username %>!</p>
<% } %>

Password Security with bcrypt

Why Hash Passwords?

NEVER store passwords in plain text!

If your database is compromised (SQL injection, hacked server, rogue employee, backup theft), attackers get all passwords. Users often reuse passwords across sites, so one breach compromises their email, bank, and more.

What is Hashing?

One-Way Transformation

Hashing is a one-way function that transforms input into a fixed-size pseudo-random output.

password123 → hash function → $2a$10$N9qo8uLO...
mySecurePass → hash function → $2a$10$K5yP9xT2...

Key Properties:

  • One-way: Can't reverse the hash to get original password
  • Deterministic: Same input always produces same output
  • Unique: Different inputs produce different outputs

Salting

Protecting Against Rainbow Tables

Salt: Random data added to password before hashing

User A: password123 + salt_abc → hash_xyz
User B: password123 + salt_def → hash_uvw

Even if two users have the same password, their hashes are different! This prevents:

  • Rainbow tables: Pre-computed hash databases
  • Duplicate detection: Can't tell if users share passwords

bcrypt automatically generates and stores salt within the hash!

Installing bcryptjs

Hashing Passwords

npm install bcryptjs

# Note: There's also 'bcrypt' (written in C++)
# bcryptjs is pure JavaScript, easier to install
# bcrypt is faster, but bcryptjs works everywhere

Updated Sign-Up Route:

const bcrypt = require("bcryptjs");

app.post("/sign-up", async (req, res, next) => {
  try {
    // Hash password with salt rounds = 10
    const hashedPassword = await bcrypt.hash(req.body.password, 10);

    await pool.query(
      "INSERT INTO users (username, password) VALUES ($1, $2)",
      [req.body.username, hashedPassword]
    );
    res.redirect("/");
  } catch (err) {
    return next(err);
  }
});

Understanding Salt Rounds

The Second Argument to bcrypt.hash()

const hashedPassword = await bcrypt.hash(password, 10);
//                                                      ^^
//                                              salt rounds

Salt rounds = 10 means bcrypt will perform 2^10 = 1,024 iterations

  • Higher rounds = more secure (slower to crack)
  • Higher rounds = slower to hash (affects UX)
  • 10 rounds is a good default (as of 2025)
  • Increase over time as computers get faster

Verifying Hashed Passwords

bcrypt.compare()

passport.use(
  new LocalStrategy(async (username, password, done) => {
    try {
      const { rows } = await pool.query(
        "SELECT * FROM users WHERE username = $1",
        [username]
      );
      const user = rows[0];

      if (!user) {
        return done(null, false, { message: "Incorrect username" });
      }

      // Compare plain text password with hashed password
      const match = await bcrypt.compare(password, user.password);
      if (!match) {
        return done(null, false, { message: "Incorrect password" });
      }

      return done(null, user);
    } catch(err) {
      return done(err);
    }
  })
);

How bcrypt.compare() Works

Under the Hood

await bcrypt.compare(plainTextPassword, hashedPasswordFromDB)
// Returns: true or false

What it does:

  1. Extracts salt from stored hash (it's embedded in there!)
  2. Hashes the plain text password using same salt
  3. Compares the two hashes
  4. Returns true if they match, false otherwise

You don't need to store or manage salt separately—bcrypt handles it!

Authentication Basics: Summary

What We've Learned

Part 2: Members Only Project

In-Class Lab

Build an exclusive clubhouse where members can write anonymous posts. Outsiders see posts but not authors. Members see who wrote what. Admins can delete posts.

Members Only: Requirements

Core Features

Members Only: Database Design

Tables to Create

-- Users table
CREATE TABLE users (
  id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  first_name VARCHAR(255) NOT NULL,
  last_name VARCHAR(255) NOT NULL,
  username VARCHAR(255) UNIQUE NOT NULL,
  password VARCHAR(255) NOT NULL,
  is_member BOOLEAN DEFAULT FALSE,
  is_admin BOOLEAN DEFAULT FALSE
);

-- Messages table
CREATE TABLE messages (
  id INTEGER PRIMARY KEY GENERATED ALWAYS AS IDENTITY,
  title VARCHAR(255) NOT NULL,
  text TEXT NOT NULL,
  timestamp TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  user_id INTEGER REFERENCES users(id) ON DELETE CASCADE
);

Members Only: Implementation Tips

Getting Started

Members Only: Testing Checklist

Verify All Features

Part 3: Prisma ORM

Object-Relational Mapping

Tired of writing raw SQL? ORMs let you interact with databases using JavaScript objects and methods instead of SQL strings.

Challenges with Raw SQL

Why Use an ORM?

1. So Much More Code

// Need to write queries for EVERY operation
await pool.query("SELECT * FROM books");
await pool.query("SELECT * FROM users");
await pool.query("INSERT INTO books (title, author) VALUES ($1, $2)", [title, author]);
await pool.query("UPDATE books SET title = $1 WHERE id = $2", [newTitle, id]);
// ... and so on for every table and operation

2. No Type Safety

What columns does the books table have? You have to check the database or documentation.

3. Error-Prone

Typos in column names, table names, or SQL syntax won't be caught until runtime.

Challenges with Raw SQL (cont.)

More Pain Points

4. No Central Schema

Database structure is only in the database. To understand tables and relations, you must log into the database or check separate documentation.

5. Manual Migrations

When database structure changes, you write migration scripts by hand. Errors can corrupt data or break production.

6. Repetitive Patterns

Every project needs CRUD operations. You keep rewriting the same patterns: getAll, getById, create, update, delete...

Introducing Prisma ORM

Modern Database Toolkit

Prisma is a next-generation ORM for Node.js and TypeScript (but works with JavaScript too!)

Three Main Components:

  • Prisma Schema: Define your database structure in code
  • Prisma Client: Auto-generated query builder
  • Prisma Migrate: Database migration system

Why Prisma? Popular, well-documented, active community, works with PostgreSQL, MySQL, SQLite, MongoDB, and more.

Prisma Schema

Database Models in Code

// prisma/schema.prisma
model Message {
  id        Int      @id @default(autoincrement())
  content   String   @db.VarChar(255)
  createdAt DateTime @default(now())
  author    User     @relation(fields: [authorId], references: [id])
  authorId  Int
}

model User {
  id       Int       @id @default(autoincrement())
  username String    @unique
  messages Message[]
}
This schema file lives in your codebase! You can see table structure without logging into the database.

Understanding the Schema

Prisma Schema Language

model Message {
  id        Int      @id @default(autoincrement())
  //        ^^^      ^^^
  //        type     attributes

  content   String   @db.VarChar(255)
  //                 ^^^^^^^^^^^^^^^^
  //                 database-specific type

  createdAt DateTime @default(now())
  //                 ^^^^^^^^^^^^^^
  //                 default value

  author    User     @relation(fields: [authorId], references: [id])
  //        ^^^^     ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  //        type     relation definition

  authorId  Int      // Foreign key
}

Prisma Client

Auto-Generated Query Builder

// Install Prisma Client
npm install @prisma/client

// Generate client from schema
npx prisma generate

// Use the client
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();

// Create a message
await prisma.message.create({
  data: {
    content: 'Hello, world!',
    authorId: 1
  }
});

// Get all messages
const messages = await prisma.message.findMany();

Prisma Client CRUD Examples

Common Operations

// CREATE
const newUser = await prisma.user.create({
  data: { username: 'alice', email: 'alice@example.com' }
});

// READ (single)
const user = await prisma.user.findUnique({
  where: { id: 1 }
});

// READ (multiple with filter)
const users = await prisma.user.findMany({
  where: { username: { contains: 'bob' } }
});

// UPDATE
const updated = await prisma.user.update({
  where: { id: 1 },
  data: { username: 'alice_updated' }
});

// DELETE
await prisma.user.delete({
  where: { id: 1 }
});

Advanced Queries with Prisma

Relations, Sorting, Pagination

// Include related data
const userWithMessages = await prisma.user.findUnique({
  where: { id: 1 },
  include: { messages: true }  // Include all user's messages
});

// Filtering and sorting
const recentMessages = await prisma.message.findMany({
  where: {
    createdAt: { gte: new Date('2025-01-01') }
  },
  orderBy: { createdAt: 'desc' },
  take: 10  // Limit to 10 results
});

// Pagination
const page2 = await prisma.message.findMany({
  skip: 10,   // Skip first 10
  take: 10    // Take next 10
});

Raw SQL with Prisma

When You Need It

// Sometimes you need raw SQL for complex queries
const result = await prisma.$queryRaw`
  SELECT u.username, COUNT(m.id) as message_count
  FROM users u
  LEFT JOIN messages m ON u.id = m.user_id
  GROUP BY u.id
  HAVING COUNT(m.id) > 5
`;

// Execute raw SQL (for INSERT, UPDATE, DELETE)
await prisma.$executeRaw`
  UPDATE users SET last_login = NOW() WHERE id = ${userId}
`;
Prisma Client supports raw SQL when you need it, but most operations can be done with the query builder.

Prisma Migrate

Database Migrations

# Create a migration (after changing schema)
npx prisma migrate dev --name add_user_email

# Apply migrations to production
npx prisma migrate deploy

# Reset database (development only!)
npx prisma migrate reset

How it works:

  1. You update the schema file
  2. Run prisma migrate dev
  3. Prisma generates SQL migration file
  4. Migration is applied to database
  5. Migration is tracked in prisma/migrations/ folder

Setting Up Prisma with JavaScript

Important Modifications

⚠️ Prisma recently moved to TypeScript-first. To use with JavaScript, follow these modifications:

  1. Skip TypeScript dependencies (typescript, tsx, @types/node)
  2. Use --generator-provider prisma-client-js when initializing
  3. Use .js file extensions instead of .ts
  4. Add .js extensions to import statements

Prisma Initialization (JavaScript)

Modified Commands

# Initialize Prisma with JavaScript
npx prisma init --datasource-provider postgresql \
  --output ../generated/prisma \
  --generator-provider prisma-client-js

# Create lib/prisma.js (not .ts!)
# File: lib/prisma.js
import { PrismaClient } from '../generated/prisma/client.js';
//                                                      ^^^^ add .js extension

export const prisma = new PrismaClient();

# In your routes (script.js)
import { prisma } from './lib/prisma.js';  // Add .js extension

# Run scripts
node script.js  // Not tsx or ts-node

Prisma Limitations

Things to Know

Identity Columns:

PostgreSQL recommends IDENTITY columns (SQL standard) over SERIAL types. Prisma doesn't support IDENTITY and uses SERIAL instead.

// In Prisma schema:
id Int @id @default(autoincrement())

// Creates in PostgreSQL:
id SERIAL PRIMARY KEY

// PostgreSQL prefers:
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY

This won't affect your curriculum projects, but good to be aware of!

Prisma ORM: Summary

Key Takeaways

Part 4: API Basics

Building RESTful APIs

Modern web development separates frontend and backend. APIs allow them to communicate using JSON instead of HTML.

The Jamstack Pattern

Separating Frontend and Backend

Traditional: One server handles database, business logic, AND view templates

Modern (Jamstack): Backend server (API) + Separate frontend(s)

Benefits:

  • Modularity: Separate business logic from view logic
  • Multiple Frontends: One API → website, mobile app, desktop app
  • Team Division: Backend and frontend teams work independently
  • Technology Freedom: Use React, Vue, Svelte, etc. for frontend

JSON APIs

Speaking the Language of APIs

// Instead of res.render() or res.send(html)
app.get("/api/posts", (req, res) => {
  res.json([
    { id: 1, title: "Hello World", author: "Alice" },
    { id: 2, title: "API Basics", author: "Bob" }
  ]);
});

// Frontend fetches this:
fetch("/api/posts")
  .then(res => res.json())
  .then(posts => {
    // Display posts in React, Vue, vanilla JS, etc.
  });
That's it! res.json() instead of res.render(). Your Express app is now an API!

What is REST?

Representational State Transfer

REST is an architectural style for designing APIs. It's a set of conventions for organizing API endpoints.

Key Principles:

  • Resource-based: URLs represent resources (nouns), not actions (verbs)
  • HTTP Verbs: Use GET, POST, PUT, DELETE for actions
  • Stateless: Each request is independent
  • Predictable: Follow conventions for easier integration

Following REST makes your API intuitive and maintainable!

HTTP Verbs / Methods

CRUD Operations

HTTP Verb CRUD Action Example
POST Create Create new resource POST /posts
GET Read Retrieve resource(s) GET /posts/:id
PUT Update Update entire resource PUT /posts/:id
PATCH Update Update partial resource PATCH /posts/:id
DELETE Delete Delete resource DELETE /posts/:id

RESTful Naming Conventions

Resource-Based URLs

âś… Good (RESTful):

GET    /posts              → Get all posts
GET    /posts/:id         → Get single post
POST   /posts             → Create new post
PUT    /posts/:id         → Update post
DELETE /posts/:id         → Delete post

GET    /posts/:id/comments        → Get comments for post
POST   /posts/:id/comments        → Add comment to post
DELETE /posts/:id/comments/:cid   → Delete specific comment

❌ Bad (Not RESTful):

GET /getAllPosts
GET /getPostById?id=123
POST /createNewPost
POST /savePostInDatabase

RESTful API Example

Blog Post API

const express = require("express");
const app = express();

app.use(express.json());  // Parse JSON bodies

// GET all posts
app.get("/api/posts", async (req, res) => {
  const posts = await prisma.post.findMany();
  res.json(posts);
});

// GET single post
app.get("/api/posts/:id", async (req, res) => {
  const post = await prisma.post.findUnique({
    where: { id: parseInt(req.params.id) }
  });
  res.json(post);
});

// CREATE new post
app.post("/api/posts", async (req, res) => {
  const newPost = await prisma.post.create({
    data: req.body
  });
  res.status(201).json(newPost);
});

CORS: Same-Origin Policy

The Problem

Same-Origin Policy: Browsers block requests from one origin to another for security.

Frontend at: https://mywebsite.com
API at:      https://api.mywebsite.com

Browser says: "Different origins! BLOCKED!"

Origins are different if:

  • Different domain (example.com vs api.example.com)
  • Different protocol (http vs https)
  • Different port (localhost:3000 vs localhost:4000)

CORS: The Solution

Cross-Origin Resource Sharing

# Install CORS middleware
npm install cors
const cors = require("cors");

// Allow ALL origins (development)
app.use(cors());

// Or allow specific origin (production)
app.use(cors({
  origin: "https://mywebsite.com"
}));

// Or multiple origins
app.use(cors({
  origin: ["https://mywebsite.com", "https://app.mywebsite.com"]
}));

CORS Headers

What CORS Middleware Does

CORS middleware adds headers to responses:

Access-Control-Allow-Origin: https://mywebsite.com
Access-Control-Allow-Methods: GET, POST, PUT, DELETE
Access-Control-Allow-Headers: Content-Type, Authorization

These headers tell the browser: "It's OK! I allow this origin to access me."

API Basics: Summary

Key Takeaways

Part 5: API Security

JSON Web Tokens (JWT)

When frontend and backend are separate, we can't use session cookies the same way. Enter JWT tokens—a secure way to authenticate API requests.

Session vs Token Authentication

Two Approaches

Session-Based (What we learned earlier):

  1. User logs in → Server creates session → Stores in database
  2. Server sends session ID cookie to browser
  3. Browser sends cookie with each request
  4. Server looks up session in database

Token-Based (JWT):

  1. User logs in → Server creates signed token
  2. Server sends token to client
  3. Client stores token (localStorage, sessionStorage)
  4. Client sends token in Authorization header
  5. Server verifies token signature (no database lookup!)

What is a JWT?

JSON Web Token Structure

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VySWQiOjEyMywiaWF0IjoxNjQwOTk1MjAwfQ.3c5_Xd8kF2pN7vQ8sT1uZ9yW0aB6cE4fG7hI8jK9lM2

Three parts separated by dots:

  1. Header: eyJhbGciOi... (algorithm & type)
  2. Payload: eyJ1c2VySW... (user data)
  3. Signature: 3c5_Xd8kF... (cryptographic signature)

Base64 encoded (not encrypted!) but signed to prevent tampering

JWT Payload Example

Decoded Token

// Header
{
  "alg": "HS256",  // Algorithm: HMAC SHA256
  "typ": "JWT"     // Type: JSON Web Token
}

// Payload (your data)
{
  "userId": 123,
  "username": "alice",
  "iat": 1640995200,  // Issued At timestamp
  "exp": 1641081600   // Expiration timestamp
}

// Signature = hash(header + payload + secret)
// Ensures nobody tampered with the token
⚠️ Important: JWTs are NOT encrypted! Don't store sensitive data (passwords, credit cards) in them.

Creating JWTs

Using jsonwebtoken Library

npm install jsonwebtoken
const jwt = require("jsonwebtoken");

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

  // Verify credentials (check database, bcrypt.compare, etc.)
  const user = await verifyCredentials(username, password);

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

  // Create JWT
  const token = jwt.sign(
    { userId: user.id, username: user.username },  // Payload
    process.env.JWT_SECRET,                        // Secret key
    { expiresIn: "1h" }                            // Expires in 1 hour
  );

  res.json({ token });
});

Verifying JWTs

Middleware to Protect Routes

const jwt = require("jsonwebtoken");

// Middleware to verify JWT
function authenticateToken(req, res, next) {
  // Get token from Authorization header
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];  // "Bearer TOKEN"

  if (!token) {
    return res.status(401).json({ error: "No token provided" });
  }

  jwt.verify(token, process.env.JWT_SECRET, (err, user) => {
    if (err) {
      return res.status(403).json({ error: "Invalid token" });
    }
    req.user = user;  // Attach decoded payload to req.user
    next();
  });
}

// Protected route
app.get("/api/protected", authenticateToken, (req, res) => {
  res.json({ message: "You are authenticated!", user: req.user });
});

Authorization Header

How Clients Send Tokens

// Client-side (fetch API)
const token = localStorage.getItem('token');

fetch('/api/protected', {
  headers: {
    'Authorization': `Bearer ${token}`
  }
})
.then(res => res.json())
.then(data => console.log(data));

Authorization Header Format:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

"Bearer" is the authentication scheme. The token follows after a space.

Token Expiration

Security Best Practice

// Create token with expiration
const token = jwt.sign(
  { userId: user.id },
  process.env.JWT_SECRET,
  { expiresIn: "1h" }  // Options: "1h", "7d", "30m", "2 days"
);

// When token expires, jwt.verify() will fail
jwt.verify(token, secret, (err, user) => {
  if (err) {
    // Could be expired OR invalid signature
    return res.status(403).json({ error: "Token expired or invalid" });
  }
  // Token is valid
});
Why expire tokens? If a token is stolen, the attacker has limited time to use it. Shorter expiration = more secure but less convenient.

Refresh Tokens (Advanced)

Long-Term Authentication

The Problem: Short-lived tokens (1 hour) mean users must re-login frequently.

The Solution: Two tokens!

  • Access Token: Short-lived (15 min), sent with every request
  • Refresh Token: Long-lived (7 days), stored securely, used to get new access tokens

When access token expires, client uses refresh token to get a new one. No re-login needed!

(This is more advanced—focus on basic JWTs for now)

JWT with Passport.js

Combining Tools

npm install passport-jwt
const JwtStrategy = require('passport-jwt').Strategy;
const ExtractJwt = require('passport-jwt').ExtractJwt;

passport.use(new JwtStrategy({
    jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
    secretOrKey: process.env.JWT_SECRET
  },
  async (payload, done) => {
    try {
      const user = await prisma.user.findUnique({
        where: { id: payload.userId }
      });
      if (user) {
        return done(null, user);
      }
      return done(null, false);
    } catch (err) {
      return done(err, false);
    }
  }
));

// Protected route
app.get("/api/protected",
  passport.authenticate('jwt', { session: false }),
  (req, res) => {
    res.json({ user: req.user });
  }
);

API Security: Summary

Key Takeaways

Part 6: Projects

File Uploader & Blog API

Two major projects to practice everything we've learned: authentication, file handling, APIs, and more!

Project 1: File Uploader

Build a Google Drive Clone

File Uploader: Tech Stack

Key Libraries

# Core dependencies
npm install express prisma @prisma/client passport passport-local \
  express-session bcryptjs ejs

# File upload
npm install multer

# Cloud storage (choose one)
npm install cloudinary  # Cloudinary
# OR
npm install @supabase/supabase-js  # Supabase

# Session store
npm install @quixo3/prisma-session-store

Project 2: Blog API

Backend + Two Frontends

Three Separate Apps:

  1. Backend (REST API):
    • Posts and comments models
    • JWT authentication
    • RESTful endpoints
    • CORS enabled
  2. Public Frontend:
    • Read posts and comments
    • Leave comments
    • No authentication required
  3. Admin Frontend:
    • Login required (JWT)
    • Create, edit, delete posts
    • Publish/unpublish posts
    • Manage comments

Blog API: Database Design

Models to Consider

model User {
  id       Int      @id @default(autoincrement())
  username String   @unique
  password String   // Hashed with bcrypt
  isAuthor Boolean  @default(false)
  posts    Post[]
}

model Post {
  id          Int       @id @default(autoincrement())
  title       String
  content     Text
  published   Boolean   @default(false)
  createdAt   DateTime  @default(now())
  updatedAt   DateTime  @updatedAt
  author      User      @relation(fields: [authorId], references: [id])
  authorId    Int
  comments    Comment[]
}

model Comment {
  id        Int      @id @default(autoincrement())
  text      String
  username  String?  // Optional username
  createdAt DateTime @default(now())
  post      Post     @relation(fields: [postId], references: [id])
  postId    Int
}

Blog API: Testing with Postman

API Development Tools

Before building frontends, test your API endpoints:

  • Postman: Popular GUI for API testing
  • curl: Command-line HTTP client
  • Browser: Works for GET requests
  • VS Code REST Client: Test in your editor

Test all CRUD operations, authentication, and error handling before writing frontend code!

Blog API: Deployment

Three Separate Deployments

Backend API: Deploy to Railway, Render, or similar (from Deployment lesson)

Public Frontend: Deploy to Netlify, Vercel, GitHub Pages, or Cloudflare Pages

Admin Frontend: Same options as public frontend

Don't forget: Configure CORS to allow your frontend domains!

Part 7: Your Portfolio & Career

Beyond the Classroom

This is our last content lecture! Let's talk about turning your projects into portfolio pieces and getting noticed by employers.

Finish The Odin Project

Complete Your Learning Journey

Many topics we've covered come from The Odin Project curriculum. I encourage you to complete the full program:

  • Foundations: HTML, CSS, JavaScript basics
  • Full Stack JavaScript: React, Node, databases (what we're doing now!)
  • Advanced Topics: Testing, performance, deployment, and more

Why? It's free, comprehensive, project-based, and respected in the industry. Employers recognize Odin Project graduates.

đź”— https://www.theodinproject.com

Polish Your Portfolio Projects

From Class Project to Portfolio Piece

How Employers Find You

The Power of Deployed Projects

From personal experience: Most headhunter pings I get come from:

  1. Client Recommendations: People I've worked with
  2. Live Projects: Software floating around on the internet
  3. GitHub Activity: Contributions, projects, code quality

Resumes and LinkedIn are great, but when people want to see what you can do, they look at your portfolio.

A deployed project shows: technical skills, follow-through, real-world problem solving, and that you care about your craft.

Review: Authentication & Authorization

Core Concepts

Review: Prisma ORM

Database Management

Review: RESTful APIs

Building Modern Backends

Review: JWT Security

Token-Based Authentication

Review: Projects

What We're Building

Members Only (In-Class Lab):

  • Authentication with Passport.js
  • Authorization levels (public, member, admin)
  • Secret passcodes
  • Message board

File Uploader (Homework):

  • Multer for file uploads
  • Cloud storage (Cloudinary/Supabase)
  • Folder management

Blog API (Homework):

  • REST API backend
  • JWT authentication
  • Two separate frontends

Next Steps

After Today

Final Thoughts

From Valhalla to Your Career

Just as the Valkyries choose warriors worthy of Valhalla, your authentication systems choose users worthy of access. You've learned to guard the gates, verify identities, and grant permissions.

These aren't just class projects—they're the foundation of modern web development. Every major application uses these patterns.

Build. Deploy. Share. Repeat. Your portfolio is your greatest asset. Make it legendary.

Questions?

Authentication, APIs, & Beyond

Let's discuss:

  • Passport.js and authentication strategies
  • bcrypt and password security
  • Prisma ORM setup and usage
  • REST API design patterns
  • JWT tokens and API security
  • Project requirements and ideas