Encodera

Thanks for reading!

Donate to the site — we work hard for you.

Contents

API authentication with JWT

Sessions with cookies suit a website in a browser, but a mobile app or another server finds a token easier to work with. A JWT (JSON Web Token) is a string in which the server writes data, such as the user id, and signs it with a secret key. The client sends the token with every request, and the server checks the signature to make sure it issued the token itself.

Installation

npm install jsonwebtoken

The secret key comes from the JWT_SECRET environment variable — there is a separate lesson about those. For now it is enough to know that the key must be a long random string.

Issuing a token

Add a login address to the API. The password check is the same User.verify() function from the login lesson:

// src/controllers/api/authController.js
import jwt from 'jsonwebtoken';
import * as User from '../../models/user.js';
 
export async function login(req, res) {
  const user = await User.verify(req.body?.email, req.body?.password);
 
  if (!user) {
    return res.status(401).json({ error: 'Wrong email or password' });
  }
 
  const token = jwt.sign({ userId: user.id }, process.env.JWT_SECRET, { expiresIn: '1h' });
  res.json({ token });
}

Add the route router.post('/login', authController.login) in src/routes/api/auth.js, and mount the router at /api/auth. Try it:

curl -X POST http://localhost:3000/api/auth/login -H "Content-Type: application/json" -d '{"email":"anna@example.com","password":"secret123"}'

The server returns {"token":"eyJhbGciOi..."} — a long string of three parts separated by dots. The expiresIn option limits the token's lifetime to an hour.

Checking the token

The client sends the token in the Authorization header, preceded by the word Bearer. Create the middleware src/middleware/requireToken.js:

// src/middleware/requireToken.js
import jwt from 'jsonwebtoken';
 
export function requireToken(req, res, next) {
  const header = req.get('Authorization') ?? '';
  const token = header.startsWith('Bearer ') ? header.slice(7) : null;
 
  if (!token) {
    return res.status(401).json({ error: 'A token is required' });
  }
 
  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET);
    req.userId = payload.userId;
    next();
  } catch {
    res.status(401).json({ error: 'The token is invalid or expired' });
  }
}

jwt.verify() checks the signature and the expiry and returns the data from the token. If anything is wrong, it throws.

Protecting addresses

Put the middleware before the handlers that change data:

// src/routes/api/posts.js
import { requireToken } from '../../middleware/requireToken.js';
 
router.post('/', requireToken, api.store);
router.patch('/:id', requireToken, api.update);
router.delete('/:id', requireToken, api.destroy);

Now deleting without a token returns 401, and with a token it goes through:

curl -X DELETE http://localhost:3000/api/posts/2 -H "Authorization: Bearer eyJhbGciOi..."

Common mistake

A JWT's content is not encrypted, only signed: anyone can decode the token and read the data inside. Do not put passwords, emails or other personal data into it — the user id is enough. And set a short expiresIn: a token already issued cannot be revoked before it expires.

Thanks for reading!

Donate to the site — we work hard for you.