Appearance
Authentication in Express.js
Authentication verifies who a user is. This tutorial covers session-based authentication, JWT tokens, and OAuth with Passport.js.
Authentication vs Authorization
- Authentication: Verifying identity (Who are you?)
- Authorization: Verifying permissions (What can you do?)
Password Hashing
Never store plain-text passwords. Use bcrypt for secure hashing:
bash
npm install bcryptjavascript
const bcrypt = require('bcrypt');
// Hash password
const hashPassword = async (password) => {
const saltRounds = 10;
return await bcrypt.hash(password, saltRounds);
};
// Verify password
const verifyPassword = async (password, hash) => {
return await bcrypt.compare(password, hash);
};
// Usage
const hash = await hashPassword('mypassword123');
// $2b$10$N9qo8uLOickgx2ZMRZoMye...
const isValid = await verifyPassword('mypassword123', hash);
// trueSession-Based Authentication
Sessions store user data on the server, with a session ID in a cookie.
Setup
bash
npm install express-session connect-mongojavascript
const express = require('express');
const session = require('express-session');
const MongoStore = require('connect-mongo');
const app = express();
app.use(session({
secret: process.env.SESSION_SECRET,
resave: false,
saveUninitialized: false,
store: MongoStore.create({
mongoUrl: process.env.MONGODB_URI
}),
cookie: {
secure: process.env.NODE_ENV === 'production',
httpOnly: true,
maxAge: 24 * 60 * 60 * 1000 // 1 day
}
}));Implementation
javascript
const bcrypt = require('bcrypt');
// Register
app.post('/auth/register', async (req, res) => {
const { email, password, name } = req.body;
// Check if user exists
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'Email already registered' });
}
// Hash password and create user
const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({
email,
password: hashedPassword,
name
});
// Create session
req.session.userId = user.id;
res.status(201).json({
message: 'Registration successful',
user: { id: user.id, email: user.email, name: user.name }
});
});
// Login
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Create session
req.session.userId = user.id;
res.json({
message: 'Login successful',
user: { id: user.id, email: user.email, name: user.name }
});
});
// Logout
app.post('/auth/logout', (req, res) => {
req.session.destroy((err) => {
if (err) {
return res.status(500).json({ error: 'Logout failed' });
}
res.clearCookie('connect.sid');
res.json({ message: 'Logged out' });
});
});
// Auth middleware
const requireAuth = async (req, res, next) => {
if (!req.session.userId) {
return res.status(401).json({ error: 'Authentication required' });
}
const user = await User.findById(req.session.userId);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
req.user = user;
next();
};
// Protected route
app.get('/profile', requireAuth, (req, res) => {
res.json({ user: req.user });
});JWT Authentication
JSON Web Tokens are stateless and work well for APIs.
Setup
bash
npm install jsonwebtokenImplementation
javascript
const jwt = require('jsonwebtoken');
const bcrypt = require('bcrypt');
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '7d';
// Generate tokens
const generateTokens = (userId) => {
const accessToken = jwt.sign({ userId }, JWT_SECRET, { expiresIn: '15m' });
const refreshToken = jwt.sign({ userId }, JWT_SECRET, { expiresIn: '7d' });
return { accessToken, refreshToken };
};
// Register
app.post('/auth/register', async (req, res) => {
const { email, password, name } = req.body;
const existingUser = await User.findOne({ email });
if (existingUser) {
return res.status(400).json({ error: 'Email already registered' });
}
const hashedPassword = await bcrypt.hash(password, 10);
const user = await User.create({
email,
password: hashedPassword,
name
});
const tokens = generateTokens(user.id);
res.status(201).json({
message: 'Registration successful',
user: { id: user.id, email: user.email, name: user.name },
...tokens
});
});
// Login
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user || !(await bcrypt.compare(password, user.password))) {
return res.status(401).json({ error: 'Invalid credentials' });
}
const tokens = generateTokens(user.id);
res.json({
message: 'Login successful',
user: { id: user.id, email: user.email, name: user.name },
...tokens
});
});
// Refresh token
app.post('/auth/refresh', async (req, res) => {
const { refreshToken } = req.body;
try {
const decoded = jwt.verify(refreshToken, JWT_SECRET);
const tokens = generateTokens(decoded.userId);
res.json(tokens);
} catch (err) {
res.status(401).json({ error: 'Invalid refresh token' });
}
});
// Auth middleware
const authenticate = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith('Bearer ')) {
return res.status(401).json({ error: 'No token provided' });
}
const token = authHeader.split(' ')[1];
try {
const decoded = jwt.verify(token, JWT_SECRET);
const user = await User.findById(decoded.userId);
if (!user) {
return res.status(401).json({ error: 'User not found' });
}
req.user = user;
next();
} catch (err) {
if (err.name === 'TokenExpiredError') {
return res.status(401).json({ error: 'Token expired' });
}
res.status(401).json({ error: 'Invalid token' });
}
};
// Protected route
app.get('/profile', authenticate, (req, res) => {
res.json({ user: req.user });
});Passport.js
Passport is authentication middleware with 500+ strategies.
Setup
bash
npm install passport passport-local passport-jwtLocal Strategy (Username/Password)
javascript
const passport = require('passport');
const LocalStrategy = require('passport-local').Strategy;
const bcrypt = require('bcrypt');
passport.use(new LocalStrategy(
{ usernameField: 'email' },
async (email, password, done) => {
try {
const user = await User.findOne({ email });
if (!user) {
return done(null, false, { message: 'Invalid credentials' });
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
return done(null, false, { message: 'Invalid credentials' });
}
return done(null, user);
} catch (err) {
return done(err);
}
}
));
// Session serialization (for session-based auth)
passport.serializeUser((user, done) => {
done(null, user.id);
});
passport.deserializeUser(async (id, done) => {
try {
const user = await User.findById(id);
done(null, user);
} catch (err) {
done(err);
}
});
// Usage
app.use(passport.initialize());
app.use(passport.session()); // For session-based auth
app.post('/login', passport.authenticate('local'), (req, res) => {
res.json({ user: req.user });
});JWT Strategy
javascript
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 User.findById(payload.userId);
if (!user) {
return done(null, false);
}
return done(null, user);
} catch (err) {
return done(err);
}
}
));
// Usage
app.get('/profile',
passport.authenticate('jwt', { session: false }),
(req, res) => {
res.json({ user: req.user });
}
);OAuth (Google)
bash
npm install passport-google-oauth20javascript
const GoogleStrategy = require('passport-google-oauth20').Strategy;
passport.use(new GoogleStrategy(
{
clientID: process.env.GOOGLE_CLIENT_ID,
clientSecret: process.env.GOOGLE_CLIENT_SECRET,
callbackURL: '/auth/google/callback'
},
async (accessToken, refreshToken, profile, done) => {
try {
// Find or create user
let user = await User.findOne({ googleId: profile.id });
if (!user) {
user = await User.create({
googleId: profile.id,
email: profile.emails[0].value,
name: profile.displayName,
avatar: profile.photos[0]?.value
});
}
return done(null, user);
} catch (err) {
return done(err);
}
}
));
// Routes
app.get('/auth/google',
passport.authenticate('google', { scope: ['profile', 'email'] })
);
app.get('/auth/google/callback',
passport.authenticate('google', { failureRedirect: '/login' }),
(req, res) => {
// Generate JWT and redirect
const token = jwt.sign({ userId: req.user.id }, JWT_SECRET);
res.redirect(`/auth/success?token=${token}`);
}
);Role-Based Authorization
javascript
// Authorization middleware
const authorize = (...roles) => {
return (req, res, next) => {
if (!req.user) {
return res.status(401).json({ error: 'Authentication required' });
}
if (!roles.includes(req.user.role)) {
return res.status(403).json({ error: 'Insufficient permissions' });
}
next();
};
};
// Usage
app.get('/admin/users',
authenticate,
authorize('admin'),
async (req, res) => {
const users = await User.find();
res.json(users);
}
);
app.delete('/posts/:id',
authenticate,
authorize('admin', 'moderator'),
async (req, res) => {
await Post.findByIdAndDelete(req.params.id);
res.json({ message: 'Post deleted' });
}
);Security Best Practices
Rate Limiting
bash
npm install express-rate-limitjavascript
const rateLimit = require('express-rate-limit');
const loginLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 attempts
message: { error: 'Too many login attempts, try again later' }
});
app.post('/auth/login', loginLimiter, (req, res) => {
// Login logic
});Account Lockout
javascript
const MAX_LOGIN_ATTEMPTS = 5;
const LOCK_TIME = 15 * 60 * 1000; // 15 minutes
app.post('/auth/login', async (req, res) => {
const { email, password } = req.body;
const user = await User.findOne({ email });
if (!user) {
return res.status(401).json({ error: 'Invalid credentials' });
}
// Check if locked
if (user.lockUntil && user.lockUntil > Date.now()) {
const remaining = Math.ceil((user.lockUntil - Date.now()) / 1000 / 60);
return res.status(423).json({
error: `Account locked. Try again in ${remaining} minutes`
});
}
const isValid = await bcrypt.compare(password, user.password);
if (!isValid) {
user.loginAttempts = (user.loginAttempts || 0) + 1;
if (user.loginAttempts >= MAX_LOGIN_ATTEMPTS) {
user.lockUntil = Date.now() + LOCK_TIME;
}
await user.save();
return res.status(401).json({ error: 'Invalid credentials' });
}
// Reset on successful login
user.loginAttempts = 0;
user.lockUntil = null;
await user.save();
// Generate token and respond
});Secure Headers
javascript
const helmet = require('helmet');
app.use(helmet());Summary
In this tutorial, you learned:
- Password hashing with bcrypt
- Session-based authentication
- JWT token authentication
- Passport.js strategies
- OAuth integration
- Role-based authorization
- Security best practices
Next, we'll explore Database Integration for persisting data.