Lesson 7 of 8
6 min read
Advanced Node.js

Authentication & JWT

Implement secure authentication with JWT, sessions, and OAuth

Authentication Methods

There are several ways to handle authentication in Node.js applications. The right choice depends on your use case: session-based for traditional web apps, JWT for APIs and SPAs, or OAuth for third-party authentication.

🔑 Authentication Methods Compared

Method Best For Storage
SessionsTraditional web appsServer (Redis)
JWTAPIs, SPAs, MobileClient (stateless)
OAuth 2.0Third-party loginProvider + tokens

Password Hashing with bcrypt

bcrypt is battle-tested and widely used. For new projects, consider Argon2 (winner of Password Hashing Competition) as an alternative: npm install argon2

const bcrypt = require('bcrypt');

// Hash a password
async function hashPassword(plainPassword) {
  const saltRounds = 12;  // Higher = more secure but slower
  const hashedPassword = await bcrypt.hash(plainPassword, saltRounds);
  return hashedPassword;
}

// Verify a password
async function verifyPassword(plainPassword, hashedPassword) {
  const isMatch = await bcrypt.compare(plainPassword, hashedPassword);
  return isMatch;
}

// Usage in registration
app.post('/register', async (req, res) => {
  const { email, password } = req.body;
  
  // Validate password strength
  if (password.length < 8) {
    return res.status(400).json({ error: 'Password too short' });
  }
  
  const hashedPassword = await hashPassword(password);
  
  await User.create({
    email,
    password: hashedPassword
  });
  
  res.status(201).json({ message: 'User created' });
});

// Usage in login
app.post('/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 verifyPassword(password, user.password);
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Create session or JWT here
});

JWT Authentication

const jwt = require('jsonwebtoken');
const express = require('express');
const app = express();

const JWT_SECRET = process.env.JWT_SECRET;
const JWT_EXPIRES_IN = '1h';
const REFRESH_TOKEN_EXPIRES_IN = '7d';

// Generate tokens
function generateTokens(user) {
  const accessToken = jwt.sign(
    { userId: user.id, email: user.email },
    JWT_SECRET,
    { expiresIn: JWT_EXPIRES_IN }
  );
  
  const refreshToken = jwt.sign(
    { userId: user.id, type: 'refresh' },
    JWT_SECRET,
    { expiresIn: REFRESH_TOKEN_EXPIRES_IN }
  );
  
  return { accessToken, refreshToken };
}

// Login endpoint
app.post('/api/login', async (req, res) => {
  const { email, password } = req.body;
  
  const user = await User.findOne({ email });
  const isValid = user && await bcrypt.compare(password, user.password);
  
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  const tokens = generateTokens(user);
  
  // Store refresh token in httpOnly cookie
  res.cookie('refreshToken', tokens.refreshToken, {
    httpOnly: true,
    secure: process.env.NODE_ENV === 'production',
    sameSite: 'strict',
    maxAge: 7 * 24 * 60 * 60 * 1000 // 7 days
  });
  
  res.json({ accessToken: tokens.accessToken });
});

// Auth middleware
function authenticate(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);
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expired' });
    }
    return res.status(401).json({ error: 'Invalid token' });
  }
}

// Protected route
app.get('/api/profile', authenticate, async (req, res) => {
  const user = await User.findById(req.user.userId);
  res.json({ user });
});

// Refresh token endpoint
app.post('/api/refresh', (req, res) => {
  const refreshToken = req.cookies.refreshToken;
  
  if (!refreshToken) {
    return res.status(401).json({ error: 'No refresh token' });
  }
  
  try {
    const decoded = jwt.verify(refreshToken, JWT_SECRET);
    
    if (decoded.type !== 'refresh') {
      throw new Error('Invalid token type');
    }
    
    const accessToken = jwt.sign(
      { userId: decoded.userId },
      JWT_SECRET,
      { expiresIn: JWT_EXPIRES_IN }
    );
    
    res.json({ accessToken });
  } catch (error) {
    res.status(401).json({ error: 'Invalid refresh token' });
  }
});

Session-Based Authentication

const session = require('express-session');
const RedisStore = require('connect-redis').default;
const { createClient } = require('redis');

// Redis client for session storage
const redisClient = createClient({ url: process.env.REDIS_URL });
redisClient.connect();

// Session middleware
app.use(session({
  store: new RedisStore({ client: redisClient }),
  secret: process.env.SESSION_SECRET,
  name: 'sid',
  resave: false,
  saveUninitialized: false,
  cookie: {
    secure: process.env.NODE_ENV === 'production',
    httpOnly: true,
    sameSite: 'lax',
    maxAge: 24 * 60 * 60 * 1000 // 24 hours
  }
}));

// Login
app.post('/login', async (req, res) => {
  const { email, password } = req.body;
  
  const user = await User.findOne({ email });
  const isValid = user && await bcrypt.compare(password, user.password);
  
  if (!isValid) {
    return res.status(401).json({ error: 'Invalid credentials' });
  }
  
  // Create session
  req.session.userId = user.id;
  req.session.email = user.email;
  
  res.json({ message: 'Logged in' });
});

// Auth middleware
function requireAuth(req, res, next) {
  if (!req.session.userId) {
    return res.status(401).json({ error: 'Not authenticated' });
  }
  next();
}

// Logout
app.post('/logout', (req, res) => {
  req.session.destroy((err) => {
    if (err) {
      return res.status(500).json({ error: 'Logout failed' });
    }
    res.clearCookie('sid');
    res.json({ message: 'Logged out' });
  });
});

OAuth 2.0 with Passport.js

const passport = require('passport');
const GoogleStrategy = require('passport-google-oauth20').Strategy;

// Configure Google OAuth
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
        });
      }
      
      done(null, user);
    } catch (error) {
      done(error, null);
    }
  }
));

// Serialize user for session
passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser(async (id, done) => {
  const user = await User.findById(id);
  done(null, user);
});

// Initialize passport
app.use(passport.initialize());
app.use(passport.session());

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

app.get('/auth/google/callback',
  passport.authenticate('google', { failureRedirect: '/login' }),
  (req, res) => {
    res.redirect('/dashboard');
  }
);

// Protected route
app.get('/dashboard', (req, res) => {
  if (!req.isAuthenticated()) {
    return res.redirect('/login');
  }
  res.render('dashboard', { user: req.user });
});

Role-Based Access Control (RBAC)

// Define roles and permissions
const ROLES = {
  admin: ['read', 'write', 'delete', 'manage_users'],
  editor: ['read', 'write'],
  viewer: ['read']
};

// Check permission middleware
function requirePermission(permission) {
  return (req, res, next) => {
    const userRole = req.user.role;
    const permissions = ROLES[userRole] || [];
    
    if (!permissions.includes(permission)) {
      return res.status(403).json({ error: 'Permission denied' });
    }
    
    next();
  };
}

// Usage
app.delete('/api/posts/:id', 
  authenticate,
  requirePermission('delete'),
  async (req, res) => {
    await Post.findByIdAndDelete(req.params.id);
    res.json({ message: 'Deleted' });
  }
);

// Resource-based authorization
async function requireOwnership(req, res, next) {
  const resource = await Post.findById(req.params.id);
  
  if (!resource) {
    return res.status(404).json({ error: 'Not found' });
  }
  
  if (resource.authorId !== req.user.userId && req.user.role !== 'admin') {
    return res.status(403).json({ error: 'Not authorized' });
  }
  
  req.resource = resource;
  next();
}

app.put('/api/posts/:id',
  authenticate,
  requireOwnership,
  async (req, res) => {
    // User owns this post or is admin
    req.resource.title = req.body.title;
    await req.resource.save();

  
    res.json(req.resource);
  }
);

💡 Best Practices

  • • Use bcrypt or argon2 for password hashing (never MD5/SHA1)
  • • Store JWT refresh tokens in httpOnly cookies
  • • Implement token rotation and revocation
  • • Use short expiry for access tokens (15min-1hr)
  • • Always validate and sanitize auth inputs
  • • Implement account lockout after failed attempts

Continue Learning