How to Secure Your API with JWT Authentication

JSON Web Tokens (JWTs) are a compact, self-contained way to transmit authentication information between a client and a server. Once issued, a JWT carries all the information the server needs to verify a user's identity — no database lookup required on every request. This tutorial walks you through implementing JWT authentication from scratch.

What Is a JWT?

A JWT is a Base64URL-encoded string made of three parts separated by dots: header.payload.signature

  • Header: Specifies the token type (JWT) and the signing algorithm (e.g., HS256).
  • Payload: Contains claims — data like user ID, roles, and expiry time.
  • Signature: A cryptographic hash of the header and payload, signed with a secret key. This is what prevents tampering.

When a client sends a JWT in a request, the server re-computes the signature and compares it. If they match, the token is valid.

Step 1: Issue a Token at Login

When a user logs in with valid credentials, your server generates and returns a JWT:


const jwt = require('jsonwebtoken');

// After verifying username/password...
const payload = {
  sub: user.id,
  email: user.email,
  roles: user.roles,
  iat: Math.floor(Date.now() / 1000),
};

const token = jwt.sign(payload, process.env.JWT_SECRET, {
  expiresIn: '15m', // short-lived access token
});

res.json({ access_token: token });

Keep your JWT_SECRET in an environment variable — it should be a long, random string and never committed to source control.

Step 2: Send the Token in Requests

The client stores the token (typically in memory or a secure HTTP-only cookie) and sends it in the Authorization header on subsequent requests:

Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...

Step 3: Verify the Token in Middleware

Create an authentication middleware that validates the token before allowing access to protected routes:


function authenticateToken(req, res, next) {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1];

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

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (err) {
    return res.status(403).json({ error: 'Invalid or expired token' });
  }
}

// Apply to protected routes
app.get('/api/profile', authenticateToken, (req, res) => {
  res.json({ user: req.user });
});

Step 4: Implement Refresh Tokens

Access tokens should be short-lived (15 minutes is common). To avoid making users log in repeatedly, issue a long-lived refresh token alongside the access token. Store refresh tokens securely (in an HTTP-only cookie or your database) and create a /auth/refresh endpoint that issues a new access token in exchange for a valid refresh token.

Security Best Practices

  • Never store JWTs in localStorage — it's vulnerable to XSS attacks. Prefer HTTP-only cookies or in-memory storage.
  • Set short expiry times on access tokens. Rotate refresh tokens on each use.
  • Use HTTPS exclusively — JWTs sent over plain HTTP can be intercepted.
  • Validate all claims — check exp (expiry), iss (issuer), and aud (audience) as appropriate.
  • Implement a token blocklist if you need immediate revocation capability (e.g., on logout).

Common Pitfalls to Avoid

  1. Using a weak or hardcoded JWT_SECRET
  2. Putting sensitive data (passwords, PII) in the payload — remember, the payload is only Base64-encoded, not encrypted
  3. Setting excessively long expiry times on access tokens
  4. Skipping signature verification and trusting the payload blindly

JWT authentication, when implemented correctly, provides a stateless, scalable security layer for your API. Combine it with HTTPS, proper secret management, and refresh token rotation for a production-ready authentication system.