Class 32: User Authentication & Authorization (Basics)
Building web applications often involves managing users and controlling access to certain features or data. This requires understanding two crucial concepts: Authentication and Authorization.
Authentication vs. Authorization
-
Authentication: "Who are you?"
The process of verifying the identity of a user, process, or device. It's about proving that you are who you say you are.
Common methods: Username/password, biometric scans, multi-factor authentication (MFA).
-
Authorization: "What are you allowed to do?"
The process of determining what an authenticated user is allowed to access or perform. It's about granting or denying permissions.
Example: An admin user can delete posts, but a regular user can only view them.

Common Authentication Methods
-
Session-based authentication (brief overview of
cookies):
Traditional method where the server creates a session for the user after successful login and stores a session ID in a cookie on the client's browser. For subsequent requests, the browser sends this cookie, and the server uses the session ID to identify the user.
Pros: Simple to implement for basic web apps. Cons: Requires server-side storage for sessions, less scalable for distributed systems, not ideal for mobile apps.
-
Token-based authentication (focus on JWT):
A more modern and scalable approach. After successful login, the server issues a cryptographic token (e.g., JWT) to the client. The client stores this token (e.g., in local storage) and sends it with every subsequent request in the
Authorization
header. The server then verifies the token's validity.Pros: Stateless (no server-side session storage), highly scalable, works well with APIs and mobile apps.
Secure Password Storage
Never store plaintext passwords in your database! If your database is compromised, all user passwords would be exposed, leading to severe security breaches.
-
Hashing:
A one-way function that transforms a password into a fixed-size string of characters (a hash). It's "one-way" because it's computationally infeasible to reverse the process and get the original password from its hash.
-
Salting:
A random string of data (a "salt") is added to the password before hashing. This prevents "rainbow table" attacks, where pre-computed hashes are used to crack passwords. Even if two users have the same password, their hashes will be different because of different salts.
-
Using
bcrypt.js
:bcrypt
is a popular and secure hashing algorithm. Thebcryptjs
library is a pure JavaScript implementation.npm install bcryptjs
// Example of hashing and comparing with bcryptjs const bcrypt = require('bcryptjs'); async function hashPassword(password) { const salt = await bcrypt.genSalt(10); // Generate a salt with 10 rounds const hashedPassword = await bcrypt.hash(password, salt); console.log('Hashed Password:', hashedPassword); return hashedPassword; } async function comparePassword(password, hashedPassword) { const isMatch = await bcrypt.compare(password, hashedPassword); console.log('Password Match:', isMatch); return isMatch; } // Usage: // hashPassword('mysecretpassword'); // comparePassword('mysecretpassword', '$2a$10$abcdefghijklmnopqrstuvw.xyzABCDEFG.HIJKLMNOPQRSTUVW');

Introduction to JSON Web Tokens (JWT)
A JSON Web Token (JWT) is a compact, URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed.

-
Components of a JWT: A JWT consists of
three parts, separated by dots (
.
):- Header: Contains the type of token (JWT) and the signing algorithm (e.g., HS256).
-
Payload: Contains the "claims"
(statements about an entity, typically the user, and
additional data). Common claims include
iss
(issuer),exp
(expiration time),sub
(subject), and custom data likeuserId
orrole
. - Signature: Created by taking the encoded header, the encoded payload, a secret key, and the algorithm specified in the header. This signature is used to verify that the sender of the JWT is who it says it is and to ensure the message hasn't been tampered with.
- Statelessness: JWTs are stateless because all necessary information about the user (claims) is contained within the token itself. The server doesn't need to store session data.
- Benefits: Scalability, mobile-friendly, widely adopted, can be used across different domains.
npm install jsonwebtoken
Implementing User Registration (Signup)
We'll create a simple user model and an endpoint for user registration.
1. User Model (models/User.js
):
// models/User.js
const mongoose = require('mongoose');
const bcrypt = require('bcryptjs');
const userSchema = mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
trim: true
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
match: [/^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$/, 'Please enter a valid email address']
},
password: {
type: String,
required: true,
minlength: [6, 'Password must be at least 6 characters long']
},
role: { // For authorization later
type: String,
enum: ['user', 'admin'],
default: 'user'
}
}, {
timestamps: true
});
// Hash password before saving
userSchema.pre('save', async function(next) {
if (!this.isModified('password')) { // Only hash if password was modified
next();
}
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
next();
});
// Method to compare entered password with hashed password
userSchema.methods.matchPassword = async function(enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
module.exports = mongoose.model('User', userSchema);
2. Registration Endpoint (server.js
):
// server.js (main file)
require('dotenv').config();
const express = require('express');
const connectDB = require('./config/db');
const User = require('./models/User'); // Import User model
const app = express();
const port = process.env.PORT || 3000;
connectDB();
app.use(express.json());
// POST /api/register - User Registration
app.post('/api/register', async (req, res) => {
const { username, email, password } = req.body;
try {
// Check if user already exists
const userExists = await User.findOne({ email });
if (userExists) {
return res.status(400).json({ message: 'User with that email already exists.' });
}
// Create new user (password hashing handled by pre-save hook in User model)
const user = await User.create({
username,
email,
password,
});
res.status(201).json({
message: 'User registered successfully!',
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
if (error.name === 'ValidationError') {
const messages = Object.values(error.errors).map(val => val.message);
return res.status(400).json({ message: 'Validation failed', errors: messages });
}
console.error('Error during registration:', error);
res.status(500).json({ message: 'Internal server error.' });
}
});
// ... other routes and app.listen() ...
Test with Postman/Insomnia:
-
POST
tohttp://localhost:3000/api/register
-
Body (JSON):
{"username": "testuser", "email": "test@example.com", "password": "password123"}
Implementing User Login (Signin)
After registration, users need to log in. Upon successful login, we'll generate and send a JWT.
1. JWT Generation Function:
Add a method to your User model to generate a JWT.
// models/User.js (add this method to userSchema)
const jwt = require('jsonwebtoken'); // npm install jsonwebtoken
userSchema.methods.getSignedJwtToken = function() {
return jwt.sign({ id: this._id, role: this.role }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRE
});
};
Add these to your .env
file:
# .env
JWT_SECRET=supersecretjwtkeythatshouldbeverylongandrandom
JWT_EXPIRE=1h # e.g., 1 hour
2. Login Endpoint (server.js
):
// server.js (main file)
// ... (imports and setup) ...
const jwt = require('jsonwebtoken'); // Import jsonwebtoken
// POST /api/login - User Login
app.post('/api/login', async (req, res) => {
const { email, password } = req.body;
// Basic validation
if (!email || !password) {
return res.status(400).json({ message: 'Please enter email and password.' });
}
try {
// Check for user by email
const user = await User.findOne({ email }).select('+password'); // Select password explicitly
if (!user) {
return res.status(401).json({ message: 'Invalid credentials.' });
}
// Check if password matches
const isMatch = await user.matchPassword(password);
if (!isMatch) {
return res.status(401).json({ message: 'Invalid credentials.' });
}
// If credentials match, generate JWT
const token = user.getSignedJwtToken();
res.status(200).json({
success: true,
token,
user: {
id: user._id,
username: user.username,
email: user.email,
role: user.role
}
});
} catch (error) {
console.error('Error during login:', error);
res.status(500).json({ message: 'Internal server error.' });
}
});
// ... other routes and app.listen() ...
Test with Postman/Insomnia:
-
POST
tohttp://localhost:3000/api/login
-
Body (JSON):
{"email": "test@example.com", "password": "password123"}
-
You should receive a
200 OK
response with a JWT.
Protecting Routes with JWT (Basic Middleware)
Now that users can get a JWT, we need a way to protect certain API routes so that only authenticated users can access them. We'll create an Express middleware for this.
1. Authentication Middleware (middleware/auth.js
):
// middleware/auth.js
const jwt = require('jsonwebtoken');
const User = require('../models/User'); // Import your User model
const protect = async (req, res, next) => {
let token;
// Check if Authorization header exists and starts with 'Bearer'
if (req.headers.authorization && req.headers.authorization.startsWith('Bearer')) {
// Extract the token
token = req.headers.authorization.split(' ')[1];
}
// Make sure token exists
if (!token) {
return res.status(401).json({ message: 'Not authorized to access this route. No token provided.' });
}
try {
// Verify token
const decoded = jwt.verify(token, process.env.JWT_SECRET);
console.log('Decoded JWT:', decoded);
// Attach user to the request object (excluding password)
req.user = await User.findById(decoded.id).select('-password');
if (!req.user) {
return res.status(401).json({ message: 'Not authorized, user not found.' });
}
next(); // Proceed to the next middleware/route handler
} catch (error) {
console.error('Token verification failed:', error);
return res.status(403).json({ message: 'Not authorized, token failed or expired.' });
}
};
module.exports = protect;
2. Protecting a Route (server.js
):
// server.js (main file)
// ... (imports and setup) ...
const protect = require('./middleware/auth'); // Import the protect middleware
// Example protected route: Get user profile
app.get('/api/profile', protect, async (req, res) => {
// If we reach here, the user is authenticated, and req.user contains their data
res.status(200).json({
success: true,
message: 'Welcome to your profile!',
user: req.user
});
});
// ... other routes and app.listen() ...
Test with Postman/Insomnia:
- First, perform a login request to get a JWT. Copy the token.
- Method:
GET
-
URL:
http://localhost:3000/api/profile
-
Headers: Add an
Authorization
header with valueBearer YOUR_JWT_TOKEN
(replaceYOUR_JWT_TOKEN
with the token you copied). -
Send the request. You should get a
200 OK
response with the user's profile. -
Try sending the request without the
Authorization
header, or with an invalid token, to see the401 Unauthorized
or403 Forbidden
responses.
You've now implemented basic user registration, login, and route protection using JWTs. This forms the backbone of secure user management in your applications. In the next class, we'll explore more advanced authentication topics, including refresh tokens and role-based access control.