Authentication and Authorization
Understanding Authentication and Authorization in Node.js
Welcome to Day 13 of our Node.js blog series!๐
Today, we'll explore one of the most critical aspects of web application security: authentication and authorization. Authentication verifies the identity of users, while authorization determines their access rights. We'll discuss various authentication methods, focus on JWT (JSON Web Tokens) for secure and stateless authentication, and walk through implementing user registration and login. We'll also cover protecting routes with middleware to ensure only authorized users can access certain resources.
Introduction to Authentication Methods
Authentication is the process of verifying the identity of a user. There are several common methods:
Password-Based Authentication:
- The most common method, where users provide a username and password. The password is stored securely (hashed and salted) in the database.
Token-Based Authentication:
- After successfully logging in, the server issues a token (e.g., JWT) that the client stores and sends with each request. The server verifies the token's validity to authenticate the user.
OAuth:
- A popular protocol for third-party authentication, allowing users to log in using credentials from another service (e.g., Google, Facebook).
Multi-Factor Authentication (MFA):
- Adds an extra layer of security by requiring two or more verification factors (e.g., password and SMS code).
Biometric Authentication:
- Uses unique biological characteristics (e.g., fingerprints, facial recognition) for authentication.
Using JWT (JSON Web Tokens) for Authentication
JWT (JSON Web Tokens) is a compact, URL-safe token format that represents claims to be transferred between two parties. It is commonly used for stateless authentication in web applications.
Structure of a JWT
A JWT consists of three parts:
Header:
Contains the type of token (JWT) and the signing algorithm (e.g., HS256).
Example:
{"alg": "HS256", "typ": "JWT"}
Payload:
Contains the claims, which are statements about an entity (e.g., user) and additional data. Standard claims include
iss
(issuer),exp
(expiration time), andsub
(subject).Example:
{"sub": "1234567890", "name": "John Doe", "admin": true}
Signature:
Created by encoding the header and payload using a secret key with the specified algorithm. The signature ensures the token's integrity.
Example:
HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)
A complete JWT might look like this:
Copy codeeyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwicm9sZSI6ImFkbWluIn0.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Implementing JWT Authentication
Setup:
- Install the necessary packages:
npm install jsonwebtoken bcryptjs
jsonwebtoken
: A library for generating and verifying JWTs.bcryptjs
: A library for hashing passwords securely.
User Registration and Login:
User Model:
const mongoose = require('mongoose'); const bcrypt = require('bcryptjs'); const userSchema = new mongoose.Schema({ username: { type: String, required: true, unique: true }, password: { type: String, required: true } }); // Hash the password before saving the user userSchema.pre('save', async function(next) { if (!this.isModified('password')) return next(); try { const salt = await bcrypt.genSalt(10); this.password = await bcrypt.hash(this.password, salt); next(); } catch (err) { next(err); } }); userSchema.methods.comparePassword = function(password) { return bcrypt.compare(password, this.password); }; const User = mongoose.model('User', userSchema); module.exports = User;
User Registration:
const express = require('express'); const jwt = require('jsonwebtoken'); const User = require('./models/User'); // Import the User model const router = express.Router(); const secretKey = 'your_secret_key'; // Replace with your secret key router.post('/register', async (req, res) => { try { const { username, password } = req.body; let user = await User.findOne({ username }); if (user) return res.status(400).json({ message: 'User already exists' }); user = new User({ username, password }); await user.save(); const token = jwt.sign({ id: user._id, username: user.username }, secretKey, { expiresIn: '1h' }); res.json({ token }); } catch (err) { res.status(500).json({ message: err.message }); } }); router.post('/login', async (req, res) => { try { const { username, password } = req.body; const user = await User.findOne({ username }); if (!user) return res.status(400).json({ message: 'Invalid credentials' }); const isMatch = await user.comparePassword(password); if (!isMatch) return res.status(400).json({ message: 'Invalid credentials' }); const token = jwt.sign({ id: user._id, username: user.username }, secretKey, { expiresIn: '1h' }); res.json({ token }); } catch (err) { res.status(500).json({ message: err.message }); } }); module.exports = router;
Explanation:
The user registration endpoint (
/register
) checks if the username exists, hashes the password, saves the user, and returns a JWT.The login endpoint (
/login
) verifies the username and password, and if valid, returns a JWT.
Protecting Routes with Middleware:
Middleware functions can be used to protect routes by verifying the JWT.
Auth Middleware:
const jwt = require('jsonwebtoken'); const secretKey = 'your_secret_key'; // Replace with your secret key function authMiddleware(req, res, next) { const token = req.header('Authorization').replace('Bearer ', ''); if (!token) return res.status(401).json({ message: 'Access denied' }); try { const decoded = jwt.verify(token, secretKey); req.user = decoded; next(); } catch (err) { res.status(400).json({ message: 'Invalid token' }); } } module.exports = authMiddleware;
Protected Route Example:
const express = require('express'); const router = express.Router(); const authMiddleware = require('./middleware/auth'); router.get('/protected', authMiddleware, (req, res) => { res.json({ message: 'Welcome to the protected route', user: req.user }); }); module.exports = router;
Explanation:
The
authMiddleware
function checks for the presence of a token in theAuthorization
header, verifies it, and attaches the decoded user information to the request object.The
/protected
route is accessible only to authenticated users, as it requires a valid JWT.
Conclusion
In this post, we've covered the essentials of authentication and authorization in Node.js applications. We discussed various authentication methods and focused on JWT for secure, stateless authentication. We demonstrated how to implement user registration and login, and protect routes using middleware. These are foundational steps in securing your web application and ensuring that only authenticated and authorized users can access specific resources.
In our next post, we'll delve into more advanced topics, such as role-based access control (RBAC) and securing your application against common vulnerabilities. Stay tuned for more insights and practical examples!