DEV Community

Sam Chen
Sam Chen

Posted on

Building a Secure Authentication System: A Practical Guide to JWT Implementation

A step-by-step guide to implementing JSON Web Tokens in your application with real-world security best practices


Introduction

Authentication is the gatekeeper of your application. Get it wrong, and you've handed the keys to the kingdom to anyone who asks. Get it right, and you've built a fortress that users can trust.

Today, we're building a secure JWT-based authentication system from the ground up. Whether you're protecting a REST API, a web application, or a mobile backend, this guide will show you how to implement industry-standard authentication without the headaches.


What We're Building

By the end of this tutorial, you'll have:

  • ✅ A working JWT authentication system
  • ✅ Refresh token rotation mechanism
  • ✅ Secure password hashing
  • ✅ Protection against common attack vectors
  • ✅ Production-ready error handling

Tech Stack: Node.js, Express, bcryptjs, jsonwebtoken, SQLite (easily adaptable to your DB)


Part 1: Understanding JWT (The 2-Minute Version)

A JWT token looks like gibberish, but it's actually three parts separated by dots:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
Enter fullscreen mode Exit fullscreen mode

Breaking it down:

  • Header: Algorithm and token type
  • Payload: Your actual data (user ID, email, permissions)
  • Signature: The secret sauce that proves it's legit

Important: The header and payload are just Base64-encoded, not encrypted. Never put passwords or credit card numbers in JWT payload!


Part 2: Project Setup

mkdir secure-auth-system
cd secure-auth-system
npm init -y
npm install express bcryptjs jsonwebtoken dotenv sqlite3 cors
npm install --save-dev nodemon
Enter fullscreen mode Exit fullscreen mode

Create your .env file:

PORT=3000
JWT_SECRET=your-super-secret-key-change-this-in-production
JWT_REFRESH_SECRET=your-refresh-secret-key
JWT_EXPIRES_IN=15m
JWT_REFRESH_EXPIRES_IN=7d
NODE_ENV=development
Enter fullscreen mode Exit fullscreen mode

⚠️ Security First: In production, use a strong random string (at least 32 characters) for your secrets. Never commit .env to Git!


Part 3: Database Setup

Create database.js:

const sqlite3 = require('sqlite3').verbose();
const path = require('path');

const db = new sqlite3.Database(path.join(__dirname, 'auth.db'));

db.serialize(() => {
  db.run(`
    CREATE TABLE IF NOT EXISTS users (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      email TEXT UNIQUE NOT NULL,
      password TEXT NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP
    )
  `);

  db.run(`
    CREATE TABLE IF NOT EXISTS refresh_tokens (
      id INTEGER PRIMARY KEY AUTOINCREMENT,
      user_id INTEGER NOT NULL,
      token TEXT NOT NULL,
      expires_at DATETIME NOT NULL,
      created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
      FOREIGN KEY(user_id) REFERENCES users(id) ON DELETE CASCADE
    )
  `);
});

module.exports = db;
Enter fullscreen mode Exit fullscreen mode

Why track refresh tokens? Token revocation. When a user logs out, you can blacklist their refresh token.


Part 4: Authentication Middleware

Create middleware/auth.js:

const jwt = require('jsonwebtoken');

const verifyToken = (req, res, next) => {
  const token = req.headers.authorization?.split(' ')[1];

  if (!token) {
    return res.status(401).json({ 
      error: 'No token provided' 
    });
  }

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.userId = decoded.userId;
    next();
  } catch (error) {
    // Different error for expired vs invalid token
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ 
        error: 'Token expired',
        code: 'TOKEN_EXPIRED' 
      });
    }

    return res.status(403).json({ 
      error: 'Invalid token' 
    });
  }
};

module.exports = { verifyToken };
Enter fullscreen mode Exit fullscreen mode

Pro tip: Return specific error codes so your frontend knows whether to refresh the token or send the user back to login.


Part 5: Core Authentication Logic

Create controllers/authController.js:


javascript
const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const db = require('../database');

const SALT_ROUNDS = 10;

// Generate both access and refresh tokens
const generateTokens = (userId) => {
  const accessToken = jwt.sign(
    { userId },
    process.env.JWT_SECRET,
    { expiresIn: process.env.JWT_EXPIRES_IN }
  );

  const refreshToken = jwt.sign(
    { userId },
    process.env.JWT_REFRESH_SECRET,
    { expiresIn: process.env.JWT_REFRESH_EXPIRES_IN }
  );

  return { accessToken, refreshToken };
};

// Register new user
exports.register = async (req, res) => {
  try {
    const { email, password } = req.body;

    // Validate input
    if (!email || !password) {
      return res.status(400).json({ 
        error: 'Email and password required' 
      });
    }

    // Password strength check
    if (password.length < 8) {
      return res.status(400).json({ 
        error: 'Password must be at least 8 characters' 
      });
    }

    // Hash password
    const hashedPassword = await bcrypt.hash(password, SALT_ROUNDS);

    // Insert user
    db.run(
      'INSERT INTO users (email, password) VALUES (?, ?)',
      [email, hashedPassword],
      function(err) {
        if (err) {
          if (err.message.includes('UNIQUE constraint failed')) {
            return res.status(409).json({ 
              error: 'Email already registered' 
            });
          }
          return res.status(500).json({ error: 'Registration failed' });
        }

        const { accessToken, refreshToken } = generateTokens(this.lastID);

        // Store refresh token
        db.run(
          'INSERT INTO refresh_tokens (user_id, token, expires_at) VALUES (?, ?, datetime(?, \'+7 days\'))',
          [this.lastID, refreshToken, 'now'],
          (err) => {
            if (err) {
              return res.status(500).json({ error: 'Failed to create session' });
            }

            res.status(201).json({
              message: 'User registered successfully',
              accessToken,
              refreshToken
            });
          }
        );
      }
    );
  } catch (error) {
    console.error('Register error:', error);
    res.status(500).json({ error: 'Server error' });
  }
};

// Login user
exports.login = async (req, res) => {
  try {
    const { email, password } = req.body;

    if (!email || !password) {
      return res.status(400).json({ 
        error: 'Email and password required' 
      });
    }

    // Find user
    db.get(
      'SELECT * FROM users WHERE email = ?',
      [email],
      async (err, user) => {
        if (err) {
          return res.status(500).json({ error: 'Database error' });
        }

        if (!user)
Enter fullscreen mode Exit fullscreen mode

Top comments (0)