JWT Authentication Implementation for Web Application

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Implementing JWT Authentication for Web Applications

JWT (JSON Web Token) is a compact token containing a signed set of claims. Unlike session-based approaches, the server doesn't maintain state—everything needed is embedded in the token itself. Horizontal scalability without sticky sessions, no database roundtrips on every request.

JWT Structure

A token consists of three base64url-encoded parts separated by a dot:

header.payload.signature

// Decoded header:
{ "alg": "RS256", "typ": "JWT" }

// Decoded payload:
{
  "sub": "user_42",        // subject — user ID
  "iss": "api.example.com", // issuer
  "aud": "app.example.com", // audience
  "iat": 1735600000,        // issued at
  "exp": 1735686400,        // expires at (24 hours)
  "jti": "uuid-v4",         // JWT ID for revocation
  "roles": ["admin"],
  "plan": "pro"
}

Signature—HMAC-SHA256 (HS256) or RSA/ECDSA (RS256/ES256). RS256 is preferable: private key only on the issuing server, public key can be distributed to any resource server for verification.

Implementation (Node.js + jose)

import { SignJWT, jwtVerify, generateKeyPair } from 'jose';

// Generate key pair (once, store in secrets)
const { privateKey, publicKey } = await generateKeyPair('RS256');

// Issue tokens
async function issueTokens(userId: string, roles: string[]) {
  const now = Math.floor(Date.now() / 1000);

  const accessToken = await new SignJWT({ roles, plan: 'pro' })
    .setProtectedHeader({ alg: 'RS256' })
    .setSubject(userId)
    .setIssuedAt(now)
    .setExpirationTime('15m')  // short-lived
    .setJti(crypto.randomUUID())
    .sign(privateKey);

  const refreshToken = await new SignJWT({})
    .setProtectedHeader({ alg: 'RS256' })
    .setSubject(userId)
    .setIssuedAt(now)
    .setExpirationTime('30d')  // long-lived
    .setJti(crypto.randomUUID())
    .sign(privateKey);

  return { accessToken, refreshToken };
}

// Verification
async function verifyToken(token: string) {
  const { payload } = await jwtVerify(token, publicKey, {
    issuer: 'api.example.com',
    audience: 'app.example.com',
  });
  return payload;
}

Access Token + Refresh Token Strategy

Access token lives 15 minutes—minimal window during compromise. Refresh token—30 days, stored securely:

// On login—set refresh token in httpOnly cookie
res.cookie('refresh_token', refreshToken, {
  httpOnly: true,        // inaccessible to JS
  secure: true,          // HTTPS only
  sameSite: 'strict',    // CSRF protection
  maxAge: 30 * 24 * 3600 * 1000,
  path: '/api/auth/refresh', // cookie sent ONLY to /refresh
});

// Access token—in memory (React state or module variable), NOT in localStorage
// localStorage is vulnerable to XSS

Refresh endpoint:

app.post('/api/auth/refresh', async (req, res) => {
  const refreshToken = req.cookies.refresh_token;
  if (!refreshToken) return res.status(401).json({ error: 'No refresh token' });

  const payload = await verifyToken(refreshToken);

  // Check if token is revoked (by jti)
  const isRevoked = await redis.get(`revoked:${payload.jti}`);
  if (isRevoked) return res.status(401).json({ error: 'Token revoked' });

  const user = await db.user.findUnique({ where: { id: payload.sub } });
  const tokens = await issueTokens(user.id, user.roles);

  // Rotate refresh token
  await redis.setex(`revoked:${payload.jti}`, 30 * 24 * 3600, '1');
  res.cookie('refresh_token', tokens.refreshToken, { /* ... */ });
  res.json({ access_token: tokens.accessToken });
});

Revocation via Redis Blocklist

JWT is stateless—you can't "revoke" without additional storage. Solution—blocklist with TTL:

// Logout
app.post('/api/auth/logout', authenticate, async (req, res) => {
  const { jti, exp } = req.jwtPayload;
  const ttl = exp - Math.floor(Date.now() / 1000);

  if (ttl > 0) {
    await redis.setex(`revoked:${jti}`, ttl, '1');
  }

  res.clearCookie('refresh_token', { path: '/api/auth/refresh' });
  res.json({ success: true });
});

// Verification middleware with blocklist check
async function authenticate(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  const payload = await verifyToken(token);

  const isRevoked = await redis.get(`revoked:${payload.jti}`);
  if (isRevoked) return res.status(401).json({ error: 'Token revoked' });

  req.jwtPayload = payload;
  next();
}

Laravel—Implementation via tymon/jwt-auth

composer require tymon/jwt-auth
php artisan jwt:secret
// config/auth.php
'guards' => [
    'api' => ['driver' => 'jwt', 'provider' => 'users'],
],

// AuthController
public function login(LoginRequest $request)
{
    $credentials = $request->only(['email', 'password']);

    if (!$token = auth('api')->attempt($credentials)) {
        return response()->json(['error' => 'Invalid credentials'], 401);
    }

    return response()->json([
        'access_token' => $token,
        'token_type'   => 'bearer',
        'expires_in'   => auth('api')->factory()->getTTL() * 60,
    ]);
}

public function refresh()
{
    return response()->json([
        'access_token' => auth('api')->refresh(),
    ]);
}

What NOT to Put in JWT

JWT is visible to anyone who intercepts the token (before signature verification)—the signature guarantees integrity, not confidentiality. Don't put in payload:

  • Passwords, secrets
  • Payment data
  • Personal data beyond necessity (GDPR—data minimization)

Optimal payload: sub (user_id), roles, plan, jti, standard claims.

Timeline

JWT auth with RS256, access+refresh tokens, httpOnly cookie for refresh, Redis revocation, logout: 3–5 days. With auto-refresh in React (silent refresh), multi-device support, session audit log: 1–2 weeks.