Avancé30 min de lecture

JWT Authentication

Implement stateless authentication with JSON Web Tokens — sign tokens on login, verify them on protected routes.

What is Authentication?

Authentication is the process of proving who you are. When you log into a website with your email and password, you are authenticating yourself. Authorization is what comes after authentication — it determines what you are allowed to do. A regular user can view posts, but only an admin can delete them. Authentication answers "who are you?" while authorization answers "what can you do?"

Traditionally, web applications used sessions for authentication. Here is how sessions work: the user logs in, the server creates a session object in memory (or a database), gives the user a session ID stored in a cookie, and on every subsequent request, the server looks up the session by that ID. This works, but it has drawbacks. The server must store session data for every logged-in user. If you have a million active users, that is a million sessions in memory. If you have multiple servers behind a load balancer, you need a shared session store (like Redis) so any server can look up any session. Scaling becomes complex.

The modern approach uses tokens — specifically JSON Web Tokens (JWTs). Instead of the server storing session data, the server creates a signed token containing the user's information and sends it to the client. The client stores the token (in localStorage or an httpOnly cookie) and sends it back with every request. The server verifies the token's signature to confirm it was not tampered with, and extracts the user data directly from the token. No server-side storage needed.

This is called stateless authentication. The server does not store any session state — all the information it needs is right there in the token. This makes scaling trivial: any server in your cluster can verify a token without consulting a shared session store. JWTs are the most popular token format for REST APIs and are used by companies like Auth0, Firebase, AWS Cognito, and countless others.

How JWT Works

A JWT is a long string made up of three parts separated by dots: xxxxx.yyyyy.zzzzz. Each part is Base64Url-encoded.

1. Header — A JSON object specifying the algorithm used to sign the token and the token type:

json
{ "alg": "HS256", "typ": "JWT" }

The most common algorithm is HS256 (HMAC with SHA-256). RS256 (RSA with SHA-256) is used when you need public/private key pairs — common in microservice architectures where multiple services need to verify tokens but only one service should create them.

2. Payload — A JSON object containing claims — data about the user and the token itself. There are three types of claims: registered claims (standard fields like iss for issuer, exp for expiration, sub for subject, iat for issued-at), public claims (custom fields you define like email, role, name), and private claims (agreed upon between parties). A typical payload looks like:

json
{ "sub": "user_123", "email": "alice@example.com", "role": "admin", "iat": 1700000000, "exp": 1700003600 }

Important: The payload is Base64-encoded, NOT encrypted. Anyone can decode and read it. Never put passwords, credit card numbers, or other sensitive data in the payload.

3. Signature — Created by taking the encoded header, the encoded payload, and a secret key, then applying the specified algorithm: HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret). The signature ensures the token was not tampered with. If someone changes a single character in the header or payload, the signature will not match, and the server will reject the token.

The complete flow:

  1. User sends email + password to /api/login
  2. Server verifies the credentials against the database
  3. Server creates a JWT with the user's id, email, and role
  4. Server sends the JWT back to the client
  5. Client stores the JWT (localStorage or httpOnly cookie)
  6. Client includes the JWT in the Authorization: Bearer <token> header on every request
  7. Server extracts the token, verifies the signature, and reads the payload
  8. If valid, the server processes the request with the user's identity

Creating & Signing Tokens

The jsonwebtoken package is the standard library for working with JWTs in Node.js. Install it with npm install jsonwebtoken.

Signing a token uses jwt.sign(payload, secret, options):

javascript
const jwt = require('jsonwebtoken');

const token = jwt.sign(
  { id: user._id, email: user.email, role: user.role },
  process.env.JWT_SECRET,
  { expiresIn: '7d' }
);

The three arguments are:

  • Payload: An object with the data you want to embed. Keep it small — this data is sent with every single HTTP request. Include only what you need: user ID, email, role. Do NOT include the entire user document.
  • Secret: A string used to sign the token. This must be a strong, random value stored as an environment variable. Never hardcode secrets in your source code. A good secret is at least 32 characters of random alphanumeric text. If someone discovers your secret, they can forge valid tokens for any user.
  • Options: Configuration object. The most important option is expiresIn, which accepts human-readable strings: '1h' (one hour), '7d' (seven days), '30m' (thirty minutes), '15s' (fifteen seconds).

Verifying a token uses jwt.verify(token, secret):

javascript
try {
  const decoded = jwt.verify(token, process.env.JWT_SECRET);
  console.log(decoded); // { id: '...', email: '...', role: '...', iat: ..., exp: ... }
} catch (error) {
  // Token is invalid or expired
  console.log('Invalid token:', error.message);
}

jwt.verify() throws an error if the token is invalid, expired, or was signed with a different secret. Always wrap it in a try/catch. The decoded object includes your original payload plus iat (issued at) and exp (expiration) timestamps automatically added by jsonwebtoken.

Complete Login & Signup Routes

javascript
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const User = require('./models/User');

const router = express.Router();
const JWT_SECRET = process.env.JWT_SECRET; // e.g., 'a7f3k9x2m8...' from .env

// ---- Helper: Generate JWT ----
function generateToken(user) {
  return jwt.sign(
    { id: user._id, email: user.email, role: user.role },
    JWT_SECRET,
    { expiresIn: '7d' }
  );
}

// ---- SIGNUP ----
router.post('/signup', async (req, res) => {
  try {
    const { name, email, password } = req.body;

    // 1. Check if user already exists
    const existingUser = await User.findOne({ email });
    if (existingUser) {
      return res.status(400).json({ error: 'Email already registered' });
    }

    // 2. Hash the password (never store plain text!)
    const hashedPassword = await bcrypt.hash(password, 12);

    // 3. Create the user
    const user = await User.create({
      name,
      email,
      password: hashedPassword,
      role: 'user', // default role
    });

    // 4. Generate JWT and send it back
    const token = generateToken(user);
    res.status(201).json({
      token,
      user: { id: user._id, name: user.name, email: user.email },
    });
  } catch (error) {
    res.status(500).json({ error: 'Signup failed' });
  }
});

// ---- LOGIN ----
router.post('/login', async (req, res) => {
  try {
    const { email, password } = req.body;

    // 1. Find user by email
    const user = await User.findOne({ email });
    if (!user) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    // 2. Compare password with stored hash
    const isPasswordValid = await bcrypt.compare(password, user.password);
    if (!isPasswordValid) {
      return res.status(401).json({ error: 'Invalid credentials' });
    }

    // 3. Generate JWT and send it back
    const token = generateToken(user);
    res.json({
      token,
      user: { id: user._id, name: user.name, email: user.email },
    });
  } catch (error) {
    res.status(500).json({ error: 'Login failed' });
  }
});

module.exports = router;

// Note: We return the SAME generic error message for both
// "user not found" and "wrong password" — this prevents
// attackers from discovering which emails are registered.

Token Expiration & Refresh Tokens

Tokens should always expire. If a token is stolen and never expires, the attacker has permanent access to the user's account. Short expiration times limit the damage window.

The industry standard is a two-token strategy:

Access Token — Short-lived (15 minutes to 1 hour). Used on every API request in the Authorization header. If stolen, the attacker only has access for a brief period.

Refresh Token — Long-lived (7 to 30 days). Used only for one purpose: to get a new access token when the current one expires. Stored in an httpOnly, secure cookie (not localStorage!) to prevent XSS attacks from stealing it.

The refresh flow:

  1. User logs in, receives both an access token and a refresh token
  2. Client uses the access token for API requests
  3. Access token expires after 15 minutes
  4. Client sends the refresh token to /api/refresh
  5. Server verifies the refresh token, issues a new access token
  6. Client continues making requests with the new access token
  7. When the refresh token expires (e.g., after 7 days), the user must log in again

Why not just use a long-lived access token? Because access tokens are sent with every request and often stored in localStorage, making them more exposed. Refresh tokens are only sent to one specific endpoint and can be stored more securely in httpOnly cookies.

Token rotation is an additional security measure: every time a refresh token is used, the server issues a new refresh token and invalidates the old one. If an attacker steals a refresh token and the legitimate user also uses it, the server detects that the old token was reused and can revoke all tokens for that user.

Revoking tokens is the one downside of stateless JWTs. Since the server does not store session data, you cannot simply delete a session to log someone out. Common solutions: maintain a token blacklist (in Redis), use short-lived tokens so revocation is less critical, or store a tokenVersion in the user record and increment it to invalidate all existing tokens.

What are the three parts of a JWT?

Prêt à pratiquer ?

Crée ton compte gratuit pour accéder à l'éditeur de code interactif, lancer les défis et suivre ta progression.