Part 3
Authentication &
JWT Security
User Registration, Login & Protected Routes
Complete Step-by-Step Guide for Beginners
Backend Development Lab
Table of Contents
Table of Contents ...............................................................................................................................2
What You Will Learn ...........................................................................................................................4
STEP 1: Update Database Schema (Password Storage) .........................................................................5
Update Schema ....................................................................................................................................... 5
Run Migration.......................................................................................................................................... 5
STEP 1B: Handle Existing Users from Part 2 (Optional) .........................................................................6
Create the Utility Script ........................................................................................................................... 6
Usage ....................................................................................................................................................... 7
Alternative: Start Fresh (Delete Old Users) ............................................................................................. 7
STEP 2: Install Required Packages .......................................................................................................8
What Was Installed? ................................................................................................................................ 8
STEP 3: Environment Variables ...........................................................................................................9
Update .env ............................................................................................................................................. 9
STEP 4: JWT Utility Functions (Using 'sub') ........................................................................................ 10
Create lib/[Link] .....................................................................................................................................10
STEP 5: Authentication Middleware ..................................................................................................12
Create middleware/[Link]....................................................................................................................12
STEP 6: Registration Endpoint (Sign Up) ............................................................................................ 14
Create routes/[Link] ............................................................................................................................14
Update [Link] ........................................................................................................................................15
Test Registration ....................................................................................................................................16
STEP 7: Login Endpoint (Sign In) ........................................................................................................17
Update routes/[Link] ...........................................................................................................................17
Test Login ...............................................................................................................................................18
STEP 8: Protecting Routes ................................................................................................................. 19
Example 1: Protect User Routes ............................................................................................................19
Example 2: Protect Posts (Authors Only) ..............................................................................................20
STEP 9: Current User Endpoint (/me) ................................................................................................ 22
Add to routes/[Link] ............................................................................................................................22
STEP 10: Testing Authentication ........................................................................................................23
Complete Testing Flow ..........................................................................................................................23
Security Best Practices ..................................................................................................................... 25
1. Password Security..............................................................................................................................25
2. JWT Security ......................................................................................................................................25
3. Rate Limiting (Prevent Brute Force) ..................................................................................................25
4. CORS Configuration ...........................................................................................................................25
5. Token Blacklisting (True Logout) ........................................................................................................26
Troubleshooting Guide ..................................................................................................................... 27
Final Project Structure ...................................................................................................................... 28
Summary ...............................................................................................................................................28
Next Steps..............................................................................................................................................29
Part 3: Authentication & JWT Security | Backend Development Lab
What You Will Learn
In Part 2, you built a REST API with database persistence. But there's a critical problem:
anyone can access all data and perform any action. There's no way to identify who is making
requests or restrict access to authorized users.
In this tutorial, you will learn to:
• Securely store passwords using hashing (as passwordHash, never plain text)
• Handle existing database users who don't have passwords yet (from Part 2)
• Implement user registration (sign up) with validation
• Generate and validate JWT (JSON Web Tokens) using standard sub (subject) claims
• Create login/logout functionality
• Protect specific routes using authentication middleware
• Extract user identity from tokens using [Link]
• Implement role-based access control (instructor vs student)
Prerequisites:You must have completed Part 2 (PostgreSQL & Prisma) and have a working backend-
lab project with User model and database connection.
4 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
STEP 1: Update Database Schema (Password Storage)
We need to add a passwordHash field to store the hashed password securely.
Update Schema
Open prisma/[Link] and update the User model:
// File: prisma/[Link]
model User {
id Int @id @default(autoincrement())
email String @unique
name String
role String @default("student")
passwordHash String // NEW: Stores bcrypt hashed password
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
posts Post[]
@@map("users")
}
Naming Convention:We use passwordHash (not just password) to make it absolutely clear this is not
the plain text password. This is an industry best practice for schema clarity.
Run Migration
# Create migration for passwordHash field
npx prisma migrate dev --name add_password_hash
This adds the passwordHash column to your existing users table.
5 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
STEP 1B: Handle Existing Users from Part 2 (Optional)
Problem:If you have existing users in your database from Part 2, they don't have passwordHash
values yet (that column didn't exist). If you try to log in as them, it will fail.
Solution:We provide a utility script to set passwords for existing users without deleting them.
Create the Utility Script
File: scripts/[Link]
require("dotenv").config();
const bcrypt = require("bcryptjs");
const prisma = require("../lib/prisma");
async function main() {
const email = [Link][2];
const newPassword = [Link][3];
if (!email || !newPassword) {
[Link]("Usage: node scripts/[Link] <email>
<newPassword>");
[Link](1);
}
const passwordHash = await [Link](newPassword, 10);
const user = await [Link]({
where: { email },
data: { passwordHash },
select: { id: true, email: true, role: true },
});
[Link]("Password updated for:", user);
}
main()
6 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
.catch([Link])
.finally(() => prisma.$disconnect());
Usage
If you have existing users from Part 2 (e.g., alice@[Link]), run:
node scripts/[Link] alice@[Link] newpassword123
Then test login:
http POST localhost:3000/api/auth/login \
email="alice@[Link]" \
password="newpassword123"
Note:This script mimics a "password reset" workflow. In production, you'd send a secure reset link
via email. Here, we use a CLI script for development convenience.
Alternative: Start Fresh (Delete Old Users)
If you don't care about existing data, simply truncate the table:
-- In psql or pgAdmin
TRUNCATE TABLE users RESTART IDENTITY;
Then create new users via the /api/auth/register endpoint (see Step 6).
7 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
STEP 2: Install Required Packages
We need bcryptjs for password hashing and jsonwebtoken for JWT handling.
Note:We use bcryptjs (pure JavaScript) instead of bcrypt (C++ bindings) because it compiles easier on
Windows systems without requiring Python or Visual Studio Build Tools.
# In your backend-lab folder
npm install bcryptjs jsonwebtoken
npm install --save-dev @types/bcryptjs @types/jsonwebtoken
What Was Installed?
• bcryptjs: Hashes passwords securely using one-way encryption. Converts password123 into
$2a$10$N9qo8uLOickgx2ZMRZoMy...
• jsonwebtoken: Creates signed tokens that prove identity. Uses standard claims like sub
(subject) for user ID.
Why not just 'bcrypt'?bcrypt requires native compilation which often fails on student Windows
laptops. bcryptjs is slower but works everywhere without compilation.
8 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
STEP 3: Environment Variables
Create a strong secret for signing JWTs.
Update .env
Add to your .env file:
# File: .env
DATABASE_URL="postgresql://lab_user:your_password_here@localhost:5432/backe
nd_lab_db?schema=public"
PORT=3000
JWT_SECRET="your-super-secret-jwt-key-min-32-characters-long"
JWT_EXPIRES_IN="24h"
Security Warning:Use a long, random string for JWT_SECRET (at least 32 characters). Never commit
the real .env to GitHub (ensure it's in .gitignore). In production, generate this with
[Link](64).toString('hex')
9 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
STEP 4: JWT Utility Functions (Using 'sub')
Create a utility file for JWT operations. We use the standard sub (subject) claim to store the user ID,
following JWT specification (RFC 7519).
Create lib/[Link]
// File: lib/[Link]
const jwt = require('jsonwebtoken');
const JWT_SECRET = [Link].JWT_SECRET;
const JWT_EXPIRES_IN = [Link].JWT_EXPIRES_IN || '24h';
function signAccessToken(user) {
const payload = {
sub: [Link], // Subject: the user ID (standard JWT practice)
email: [Link], // Additional claims
role: [Link]
};
return [Link](payload, JWT_SECRET, {
expiresIn: JWT_EXPIRES_IN,
issuer: 'backend-lab-api',
audience: 'backend-lab-client'
});
}
function verifyAccessToken(token) {
try {
return [Link](token, JWT_SECRET, {
issuer: 'backend-lab-api',
audience: 'backend-lab-client'
});
} catch (error) {
throw new Error('Invalid or expired token');
10 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
}
}
[Link] = {
signAccessToken,
verifyAccessToken
};
Why 'sub'?The sub (subject) claim is the JWT standard way to identify the principal (user). Using sub
instead of custom userId makes your tokens compatible with standard JWT libraries and OpenID
Connect (OIDC) specifications.
11 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
STEP 5: Authentication Middleware
Create middleware that validates the JWT and attaches user info to the request.
Create middleware/[Link]
// File: middleware/[Link]
const { verifyAccessToken } = require('../lib/jwt');
const prisma = require('../lib/prisma');
async function requireAuth(req, res, next) {
try {
const authHeader = [Link]['authorization'];
if (!authHeader || ) {
return [Link](401).json({
error: 'Missing or invalid Authorization header. Format: Bearer
<token>'
});
}
const token = [Link]('Bearer '.length);
const payload = verifyAccessToken(token);
const user = await [Link]({
where: { id: [Link] },
select: { id: true, email: true, name: true, role: true }
});
if (!user) {
return [Link](401).json({ error: 'User no longer exists' });
}
[Link] = {
sub: [Link],
12 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
email: [Link],
role: [Link],
...user
};
next();
} catch (error) {
if ([Link] === 'Invalid or expired token') {
return [Link](401).json({ error: 'Invalid or expired token' });
}
[Link]('Auth middleware error:', error);
[Link](500).json({ error: 'Authentication failed' });
}
}
function requireRole(...allowedRoles) {
return (req, res, next) => {
if (![Link]) {
return [Link](401).json({ error: 'Not authenticated' });
}
if () {
return [Link](403).json({
error: `Access denied. Required role: ${[Link](' or ')}`
});
}
next();
};
}
[Link] = { requireAuth, requireRole };
13 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
STEP 6: Registration Endpoint (Sign Up)
Create authentication routes with secure password hashing.
Create routes/[Link]
// File: routes/[Link]
const express = require('express');
const router = [Link]();
const bcrypt = require('bcryptjs');
const prisma = require('../lib/prisma');
const { signAccessToken } = require('../lib/jwt');
const { requireAuth } = require('../middleware/auth');
const SALT_ROUNDS = 10;
[Link]('/register', async (req, res) => {
try {
const { email, name, password } = [Link];
if (!email || !name || !password) {
return [Link](400).json({
error: 'Email, name, and password are required'
});
}
if ([Link] < 6) {
return [Link](400).json({
error: 'Password must be at least 6 characters'
});
}
const existingUser = await [Link]({ where: { email }
});
if (existingUser) {
14 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
return [Link](409).json({ error: 'Email already registered' });
}
const passwordHash = await [Link](password, SALT_ROUNDS);
const user = await [Link]({
data: {
email,
name,
passwordHash,
role: 'student'
},
select: {
id: true, email: true, name: true, role: true, createdAt: true
}
});
const accessToken = signAccessToken(user);
[Link](201).json({
message: 'User registered successfully',
accessToken,
user
});
} catch (error) {
[Link]('Registration error:', error);
[Link](500).json({ error: 'Failed to register user' });
}
});
[Link] = router;
Update [Link]
// File: [Link]
15 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
// Add near the top:
const authRoutes = require('./routes/auth');
// Add after other routes:
[Link]('/api/auth', authRoutes);
Test Registration
http POST localhost:3000/api/auth/register \
email="alice@[Link]" \
name="Alice" \
password="secret123"
Expected Response:
{
"message": "User registered successfully",
"accessToken": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
"user": {
"id": 1,
"email": "alice@[Link]",
"name": "Alice",
"role": "student",
"createdAt": "2024-01-15T10:30:00.000Z"
}
}
Verify in pgAdmin:Open the users table. You should see passwordHash containing something like
$2a$10$... - never the plain text "secret123".
16 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
STEP 7: Login Endpoint (Sign In)
Add login to verify credentials and issue tokens with sub claim.
Update routes/[Link]
// File: routes/[Link] (add to existing file)
[Link]('/login', async (req, res) => {
try {
const { email, password } = [Link];
if (!email || !password) {
return [Link](400).json({
error: 'Email and password are required'
});
}
const user = await [Link]({ where: { email } });
if (!user) {
return [Link](401).json({ error: 'Invalid credentials' });
}
const isValidPassword = await [Link](password,
[Link]);
if (!isValidPassword) {
return [Link](401).json({ error: 'Invalid credentials' });
}
const accessToken = signAccessToken(user);
[Link]({
message: 'Login successful',
accessToken,
17 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
user: {
id: [Link],
email: [Link],
name: [Link],
role: [Link]
}
});
} catch (error) {
[Link]('Login error:', error);
[Link](500).json({ error: 'Failed to login' });
}
});
Test Login
http POST localhost:3000/api/auth/login \
email="alice@[Link]" \
password="secret123"
Expected Response:
{
"message": "Login successful",
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"user": {
"id": 1,
"email": "alice@[Link]",
"name": "Alice",
"role": "student"
}
}
Note:If you decode the token at [Link], you'll see "sub": 1 (the user ID), not "userId": 1. This is the
JWT standard.
18 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
STEP 8: Protecting Routes
Now protect existing routes using the sub claim from the token.
Example 1: Protect User Routes
Update routes/[Link]:
// File: routes/[Link]
const { requireAuth, requireRole } = require('../middleware/auth');
// Public: Anyone can list users
[Link]('/', async (req, res) => {
// ... existing code ...
});
// Protected: Only authenticated users can view specific user
[Link]('/:id', requireAuth, async (req, res) => {
// ... existing code ...
});
// Protected: Users can only update their own profile
[Link]('/:id', requireAuth, async (req, res) => {
try {
const targetUserId = parseInt([Link]);
const currentUserId = [Link]; // From JWT 'sub' claim
const currentUserRole = [Link];
if (currentUserId !== targetUserId && currentUserRole !== 'admin') {
return [Link](403).json({
error: 'You can only update your own profile'
});
}
// ... rest of code ...
} catch (error) {
19 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
// ... error handling ...
}
});
// Protected + Role-based: Only instructors/admins can delete
[Link]('/:id', requireAuth, requireRole('instructor', 'admin'),
async (req, res) => {
// ... delete code ...
});
Example 2: Protect Posts (Authors Only)
Update routes/[Link] to ensure only post authors can modify their posts:
// File: routes/[Link]
const { requireAuth } = require('../middleware/auth');
// Protected: Create post (uses [Link] as author)
[Link]('/', requireAuth, async (req, res) => {
try {
const { title, content } = [Link];
const authorId = [Link]; // From JWT
if (!title) {
return [Link](400).json({ error: 'Title is required' });
}
const post = await [Link]({
data: { title, content, authorId },
include: {
author: { select: { id: true, name: true } }
}
});
[Link](201).json(post);
} catch (error) {
20 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
[Link](500).json({ error: 'Failed to create post' });
}
});
// Protected: Only author can publish their post
[Link]('/:id/publish', requireAuth, async (req, res) => {
try {
const postId = parseInt([Link]);
const currentUserId = [Link];
const post = await [Link]({ where: { id: postId } });
if (!post) {
return [Link](404).json({ error: 'Post not found' });
}
if ([Link] !== currentUserId) {
return [Link](403).json({
error: 'You can only publish your own posts'
});
}
const updatedPost = await [Link]({
where: { id: postId },
data: { published: true },
include: { author: { select: { id: true, name: true } } }
});
[Link](updatedPost);
} catch (error) {
[Link](500).json({ error: 'Failed to publish post' });
}
});
21 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
STEP 9: Current User Endpoint (/me)
Add an endpoint to get the current logged-in user's info using the sub claim.
Add to routes/[Link]
// File: routes/[Link] (add before [Link])
[Link]('/me', requireAuth, async (req, res) => {
try {
const userId = [Link];
const user = await [Link]({
where: { id: userId },
select: {
id: true, email: true, name: true,
role: true, createdAt: true
}
});
if (!user) {
return [Link](404).json({ error: 'User not found' });
}
[Link]({ user });
} catch (error) {
[Link](500).json({ error: 'Failed to fetch user' });
}
});
[Link]('/logout', requireAuth, (req, res) => {
[Link]({ message: 'Logged out. Please delete your token client-side.'
});
});
22 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
STEP 10: Testing Authentication
Complete Testing Flow
1. Register (Create Account)
http POST localhost:3000/api/auth/register \
email="bob@[Link]" \
name="Bob" \
password="mypassword"
2. Verify Hash in Database
SELECT id, email, password_hash FROM users WHERE email = 'bob@[Link]';
Confirm password_hash looks like $2a$10$... (not "mypassword").
3. Login (Get Token)
http POST localhost:3000/api/auth/login \
email="bob@[Link]" \
password="mypassword"
Copy the accessToken!
4. Access Protected Route (Fail)
http GET localhost:3000/api/auth/me
Expected:401 Missing or invalid Authorization header
5. Access Protected Route (Success)
http GET localhost:3000/api/auth/me \
Authorization:"Bearer <token>"
6. Verify 'sub' Claim
Paste your token at [Link]. Verify payload contains:
{
"sub": 1,
"email": "bob@[Link]",
"role": "student",
"iat": 1705312000,
"exp": 1705398400
23 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
7. Test Authorization (Role Check)
http DELETE localhost:3000/api/users/1 \
Authorization:"Bearer <student_token>"
Expected:403 Access denied. Required role: instructor or admin
24 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
Security Best Practices
1. Password Security
• Minimum 8 characters (we used 6 for demo - increase for production)
• Check against common password lists (HaveIBeenPwned API)
• Never return passwordHash in any API response (always use select to exclude it)
2. JWT Security
• Short expiration: Use 15-60 minutes for access tokens, implement refresh tokens for longer
sessions
• HTTPS only: Never transmit JWT over HTTP in production
• Secure storage: Web - httpOnly cookies (not localStorage) to prevent XSS; Mobile -
Keychain/Keystore
3. Rate Limiting (Prevent Brute Force)
npm install express-rate-limit
const rateLimit = require('express-rate-limit');
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: 'Too many attempts, please try again later'
});
[Link]('/login', authLimiter, async (req, res) => { ... });
4. CORS Configuration
const cors = require('cors');
[Link](cors({
origin: [Link].CLIENT_URL || '[Link]
credentials: true
}));
25 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
5. Token Blacklisting (True Logout)
// On logout, add token to Redis blacklist
await [Link](`blacked:${token}`, tokenExpiryTime, 'true');
// In middleware, check if token is blacklisted
const isBlacklisted = await [Link](`blacked:${token}`);
if (isBlacklisted) return [Link](401).json({ error: 'Token revoked' });
26 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
Troubleshooting Guide
Error: "Invalid credentials" for existing Part 2 user
Fix:User exists but has no passwordHash (NULL in database). Fix: Use the set-password script
from Step 1B: node scripts/[Link] alice@[Link] newpassword123
Error: "Cannot read property 'sub' of undefined"
Fix:[Link] not set properly in middleware. Fix: Ensure requireAuth is called before your route
handler and that you're calling next() in the middleware.
Error: "Invalid or expired token"
Fix:Causes: Token expired (check exp claim in [Link]), Wrong JWT_SECRET (must match between
sign and verify), Token malformed (missing dots, wrong format)
Error: "jwt malformed"
Fix:Not extracting token correctly from "Bearer <token>" header. Fix: Ensure you're using
[Link]('Bearer '.length) or split(' ')[1].
Database Error: Column 'passwordHash' does not exist
Fix:You forgot to run npx prisma migrate dev --name add_password_hash or the migration didn't
apply. Check pgAdmin to confirm column exists.
Student can delete any user (Authorization not working)
Fix:Not checking [Link] against the target user ID. Fix: Ensure your code compares: if
([Link] !== targetId && [Link] !== 'admin') { return [Link](403).json({ error:
'Forbidden' }); }
Script Error: "Cannot find module '../lib/prisma'"
Fix:Running script from wrong directory. Fix: Run from project root: node scripts/[Link]
email@[Link] password (Not from inside the scripts folder)
27 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
Final Project Structure
backend-lab/
├── node_modules/
├── prisma/
│ ├── migrations/
│ │ └── 20240101000000_add_password_hash/
│ │ └── [Link]
│ └── [Link] # Updated with passwordHash
├── routes/
│ ├── [Link] # Now protected with requireAuth
│ ├── [Link] # Now protected
│ └── [Link] # NEW: Register/Login/Me
├── scripts/ # NEW: Utility scripts
│ └── [Link] # Set password for existing users
├── lib/
│ ├── [Link]
│ └── [Link] # NEW: signAccessToken, verifyAccessToken
├── middleware/
│ ├── [Link]
│ └── [Link] # NEW: requireAuth, requireRole
├── .env # Added JWT_SECRET, JWT_EXPIRES_IN
├── [Link] # Added authRoutes
└── [Link] # Added bcryptjs, jsonwebtoken
Summary
Congratulations! You now have production-ready authentication:
• Secure password storage using passwordHash with bcryptjs
• Migration utility ([Link]) for existing database users
• Standard JWT implementation using sub claim for user identification
• Protected routes with authentication middleware
• Authorization with role-based access control (student vs instructor)
28 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
Next Steps
0. Refresh Tokens: Implement short-lived access tokens (15m) with long-lived refresh tokens
1. Email Verification: Send verification emails on registration
2. Password Reset: Implement "Forgot Password" flow with secure tokens
3. OAuth: Add Google/GitHub login using [Link]
4. Audit Logging: Log all authentication attempts for security monitoring
29 / 30
Part 3: Authentication & JWT Security | Backend Development Lab
Backend Development Lab
Part 3: Authentication & JWT Security
Complete Step-by-Step Guide for Beginners
30 / 30