Authentication is a complex and vital topic in web development. It's complex because new security threats emerge daily, and it's crucial because most of our lives depend on online services and computer systems that hold sensitive data.
Understand the difference between authentication and authorization, and learn how to implement secure authentication systems.
Master password hashing, storage best practices, and protection against common security vulnerabilities.
Learn how to implement and manage user sessions securely using modern web technologies.
Authentication is the process by which our Web API verifies a client's identity that is trying to access a resource. This is different from authorization, which comes after authentication and determines what type of access a user should have.
Adding authentication to a Web API requires that an API can:
Proper authentication is difficult. There is a constant race between security experts with innovative ways to protect our information and attackers coming up with ways to circumvent those security measures.
The rule of thumb is: NEVER, EVER, under no circumstances, store user passwords in plain text. There are two main approaches to secure password storage:
If the database of users and keys is compromised with encryption, it's possible to decrypt the passwords to their original values. This is why cryptographic hashing is the preferred method for storing user passwords.
const bcrypt = require('bcryptjs'); // Hashing a password before saving to DB async function registerUser(user) { const rounds = 8; // complexity/work factor const hash = await bcrypt.hash(user.password, rounds); // Store user with hashed password return saveUser({ ...user, password: hash }); }
Password length alone is not enough to slow password guessing, but long passwords are generally better than short, complicated ones. It's a trade-off between convenience and security.
A common way that attackers circumvent hashing algorithms is by pre-calculating hashes for all possible character combinations using a rainbow table. To defend against this, we use:
Key Derivation Functions = [Hash] + [Time]
We use algorithms optimized for security rather than speed. This adds computational cost to password verification, making brute-force attacks much more time-consuming.
const bcrypt = require('bcryptjs'); async function loginUser(username, password) { // Get user from database const user = await getUserByUsername(username); if (!user) { return null; // User not found } // Compare provided password with stored hash const isValid = await bcrypt.compare(password, user.password); if (isValid) { return user; // Password matches } else { return null; // Invalid password } }
To restrict access to resources and allow access only for authenticated users, we can create middleware functions:
// Middleware to check if user is authenticated function restricted(req, res, next) { if (req.session && req.session.user) { next(); // User is authenticated, proceed } else { res.status(401).json({ message: 'You must be logged in to access this resource' }); } } // Example of a protected route app.get('/api/protected-resource', restricted, (req, res) => { res.json({ message: 'You have access to this protected resource', user: req.session.user }); }); // Role-based authorization middleware function checkRole(role) { return (req, res, next) => { if (req.session && req.session.user && req.session.user.role === role) { next(); } else { res.status(403).json({ message: 'Access forbidden: insufficient privileges' }); } }; } // Admin-only route app.get('/api/admin', restricted, checkRole('admin'), (req, res) => { res.json({ message: 'Admin dashboard data' }); });
Sessions are a way to persist authentication information across requests. After a user authenticates, the server creates a session and sends a session identifier to the client, which is then included in subsequent requests.
const express = require('express'); const session = require('express-session'); const app = express(); app.use(session({ name: 'session-cookie', secret: 'this should be stored in an env variable', cookie: { maxAge: 1000 * 60 * 60, // 1 hour in milliseconds secure: process.env.NODE_ENV === 'production', // use HTTPS in production httpOnly: true, // prevents client-side JS from reading the cookie }, resave: false, saveUninitialized: false, }));
Once we have session management set up, we can implement login and logout functionality:
// Login route app.post('/api/login', async (req, res) => { const { username, password } = req.body; try { const user = await loginUser(username, password); if (user) { // Create session and store user info req.session.user = { id: user.id, username: user.username, role: user.role }; res.status(200).json({ message: 'Logged in successfully', user: req.session.user }); } else { res.status(401).json({ message: 'Invalid credentials' }); } } catch (error) { res.status(500).json({ message: 'Server error during login' }); } }); // Logout route app.get('/api/logout', (req, res) => { // Destroy the session if (req.session) { req.session.destroy(err => { if (err) { res.status(500).json({ message: 'Logout failed' }); } else { res.status(200).json({ message: 'Logged out successfully' }); } }); } else { res.status(200).json({ message: 'No active session' }); } });
Build a secure authentication system using Node.js and Express
View Project