Skip to main content

Authentication with JWT

Most web applications need to know who is making requests. Authentication verifies user identity, while authorization determines what they can access. In this chapter, we’ll implement a complete authentication system using industry-standard practices.

Authentication vs. Authorization

ConceptQuestion It AnswersExample
Authentication”Who are you?”Login with username/password
Authorization”What can you do?”Can this user delete posts?

Understanding JWT (JSON Web Tokens)

Traditional session-based authentication stores user data on the server. This works, but has challenges:
  • Scalability: Sessions must be shared across multiple servers
  • Mobile apps: Cookies don’t work well on native apps
  • Microservices: Each service needs access to session store
JWT solves these problems with stateless authentication. Instead of storing session data on the server, the server issues a signed token that the client stores and sends with each request.

How JWT Works

1. User logs in with credentials
2. Server verifies credentials
3. Server creates a signed JWT containing user info
4. Client stores the token (localStorage, cookie)
5. Client sends token with every request
6. Server verifies token signature and extracts user info

JWT Structure

A JWT consists of three parts separated by dots:
header.payload.signature
  • Header: Algorithm and token type
  • Payload: User data (claims) like user ID, role, expiration
  • Signature: Ensures the token hasn’t been tampered with
Never store sensitive information (passwords, credit cards) in the JWT payload. The payload is Base64-encoded, not encrypted—anyone can decode and read it.

Prerequisites

Install the necessary packages:
npm install jsonwebtoken bcryptjs
  • jsonwebtoken: To sign and verify tokens.
  • bcryptjs: To hash passwords securely.

1. User Registration (Hashing Passwords)

Never store passwords in plain text. Use bcryptjs to hash them.
const bcrypt = require('bcryptjs');

// ... inside your register route
const { username, password } = req.body;

// Check if user exists...

// Hash password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(password, salt);

// Save user with hashedPassword...

2. User Login (Comparing Passwords)

When a user logs in, compare the provided password with the hashed password in the database.
const validPassword = await bcrypt.compare(password, user.password);

if (!validPassword) {
  return res.status(400).json({ msg: 'Invalid credentials' });
}

3. Generating a JWT

If the password is valid, generate a token and send it to the client.
const jwt = require('jsonwebtoken');

const payload = {
  user: {
    id: user.id
  }
};

jwt.sign(
  payload,
  process.env.JWT_SECRET,
  { expiresIn: '1h' },
  (err, token) => {
    if (err) throw err;
    res.json({ token });
  }
);
Note: Store JWT_SECRET in your .env file.

4. Protecting Routes (Middleware)

Create middleware to verify the token on protected routes. middleware/auth.js
const jwt = require('jsonwebtoken');

module.exports = function(req, res, next) {
  // Get token from header
  const token = req.header('x-auth-token');

  // Check if not token
  if (!token) {
    return res.status(401).json({ msg: 'No token, authorization denied' });
  }

  // Verify token
  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded.user;
    next();
  } catch (err) {
    res.status(401).json({ msg: 'Token is not valid' });
  }
};

5. Using the Middleware

const auth = require('./middleware/auth');

// Protected route
app.get('/api/auth/user', auth, async (req, res) => {
  try {
    // Fetch user from DB using req.user.id
    // ...
    res.json(user);
  } catch (err) {
    res.status(500).send('Server Error');
  }
});

Summary

  • bcryptjs: Use hash() to encrypt passwords and compare() to validate
  • JWT: Use sign() to create tokens and verify() to validate
  • Middleware: Create custom middleware to protect private routes
  • Stateless: The server doesn’t store session data; the token contains necessary info

Access Tokens vs Refresh Tokens

For production applications, implement a dual-token system:
Token TypeLifetimePurpose
Access Token15 min - 1 hourAuthenticate API requests
Refresh Token7-30 daysGet new access tokens
// Generate both tokens
const generateTokens = (userId) => {
  const accessToken = jwt.sign(
    { userId },
    process.env.ACCESS_TOKEN_SECRET,
    { expiresIn: '15m' }
  );
  
  const refreshToken = jwt.sign(
    { userId },
    process.env.REFRESH_TOKEN_SECRET,
    { expiresIn: '7d' }
  );
  
  return { accessToken, refreshToken };
};

// Login endpoint
app.post('/api/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 { accessToken, refreshToken } = generateTokens(user._id);
  
  // Store refresh token in database or Redis
  await RefreshToken.create({ token: refreshToken, userId: user._id });
  
  // Send refresh token as httpOnly cookie
  res.cookie('refreshToken', refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
  });
  
  res.json({ accessToken });
});

// Refresh endpoint
app.post('/api/auth/refresh', async (req, res) => {
  const { refreshToken } = req.cookies;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'No refresh token' });
  }
  
  try {
    const decoded = jwt.verify(refreshToken, process.env.REFRESH_TOKEN_SECRET);
    
    // Check if refresh token exists in database
    const storedToken = await RefreshToken.findOne({ token: refreshToken });
    if (!storedToken) {
      return res.status(401).json({ error: 'Invalid refresh token' });
    }
    
    // Generate new access token
    const accessToken = jwt.sign(
      { userId: decoded.userId },
      process.env.ACCESS_TOKEN_SECRET,
      { expiresIn: '15m' }
    );
    
    res.json({ accessToken });
  } catch (error) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

// Logout endpoint
app.post('/api/auth/logout', async (req, res) => {
  const { refreshToken } = req.cookies;
  
  // Remove from database
  await RefreshToken.deleteOne({ token: refreshToken });
  
  // Clear cookie
  res.clearCookie('refreshToken');
  res.json({ message: 'Logged out successfully' });
});

Role-Based Access Control (RBAC)

// User model with roles
const UserSchema = new mongoose.Schema({
  email: String,
  password: String,
  role: {
    type: String,
    enum: ['user', 'admin', 'moderator'],
    default: 'user'
  }
});

// Role middleware
const authorize = (...roles) => {
  return (req, res, next) => {
    if (!roles.includes(req.user.role)) {
      return res.status(403).json({
        error: 'Not authorized to access this resource'
      });
    }
    next();
  };
};

// Usage
app.get('/api/admin/users', auth, authorize('admin'), (req, res) => {
  // Only admins can access
});

app.delete('/api/posts/:id', auth, authorize('admin', 'moderator'), (req, res) => {
  // Admins and moderators can delete
});

OAuth 2.0 with Passport.js

npm install passport passport-google-oauth20
const passport = require('passport');
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 {
      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 (error) {
      return done(error, null);
    }
  }
));

// Routes
app.get('/auth/google',
  passport.authenticate('google', { scope: ['profile', 'email'] })
);

app.get('/auth/google/callback',
  passport.authenticate('google', { session: false }),
  (req, res) => {
    const { accessToken, refreshToken } = generateTokens(req.user._id);
    res.redirect(`/auth-success?token=${accessToken}`);
  }
);

Password Reset Flow

const crypto = require('crypto');
const nodemailer = require('nodemailer');

// Request password reset
app.post('/api/auth/forgot-password', async (req, res) => {
  const user = await User.findOne({ email: req.body.email });
  
  if (!user) {
    // Don't reveal if email exists
    return res.json({ message: 'If email exists, reset link sent' });
  }
  
  // Generate reset token
  const resetToken = crypto.randomBytes(32).toString('hex');
  const hashedToken = crypto
    .createHash('sha256')
    .update(resetToken)
    .digest('hex');
  
  user.resetPasswordToken = hashedToken;
  user.resetPasswordExpires = Date.now() + 3600000; // 1 hour
  await user.save();
  
  // Send email
  const resetUrl = `${process.env.FRONTEND_URL}/reset-password/${resetToken}`;
  
  await transporter.sendMail({
    to: user.email,
    subject: 'Password Reset',
    html: `Click <a href="${resetUrl}">here</a> to reset your password.`
  });
  
  res.json({ message: 'If email exists, reset link sent' });
});

// Reset password
app.post('/api/auth/reset-password/:token', async (req, res) => {
  const hashedToken = crypto
    .createHash('sha256')
    .update(req.params.token)
    .digest('hex');
  
  const user = await User.findOne({
    resetPasswordToken: hashedToken,
    resetPasswordExpires: { $gt: Date.now() }
  });
  
  if (!user) {
    return res.status(400).json({ error: 'Invalid or expired token' });
  }
  
  user.password = await bcrypt.hash(req.body.password, 10);
  user.resetPasswordToken = undefined;
  user.resetPasswordExpires = undefined;
  await user.save();
  
  res.json({ message: 'Password reset successful' });
});

Security Best Practices

  1. Never store JWTs in localStorage - vulnerable to XSS attacks
  2. Use httpOnly cookies for refresh tokens
  3. Implement token blacklisting for logout
  4. Use short expiration for access tokens
  5. Rotate refresh tokens on each use
  6. Hash passwords with bcrypt (cost factor 10+)
  7. Validate password strength on registration