DEV Community

Shrijith Venkatramana
Shrijith Venkatramana

Posted on

5 Lesser Known Ways to Use JSON Web Tokens

Hello, I'm Shrijith Venkatramana. I’m building LiveReview, a private AI code review tool that runs on your LLM key (OpenAI, Gemini, etc.) with highly competitive pricing -- built for small teams. Do check it out and give it a try!

JSON Web Tokens (JWT) provide a compact way to securely transmit information between parties. They consist of three parts: a header, a payload, and a signature, separated by dots. The header defines the token type and signing algorithm, the payload holds claims like user data, and the signature verifies integrity.

JWTs are self-contained, meaning servers don't need to store session data. This makes them ideal for distributed systems. To get started, you'll often use libraries like jsonwebtoken in Node.js or similar in other languages.

Before diving into the ways, note that JWTs should always be transmitted over HTTPS to prevent interception.

Decoding the Basics of JWT Structure

A JWT looks like this: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c.

  • Header: Base64-encoded JSON with algorithm (e.g., HS256) and type (JWT).
  • Payload: Base64-encoded JSON with claims. Standard claims include iss (issuer), exp (expiration), sub (subject). Custom claims can add app-specific data.
  • Signature: Created by hashing header + payload with a secret key.

To create one manually in code, install jsonwebtoken via npm: npm install jsonwebtoken.

Here's a basic example to generate and verify a JWT:

const jwt = require('jsonwebtoken');

// Secret key for signing (keep this secure in production)
const secret = 'your-256-bit-secret';

// Payload with claims
const payload = {
  sub: '1234567890',
  name: 'John Doe',
  iat: 1516239022  // Issued at timestamp
};

// Generate JWT with 1-hour expiration
const token = jwt.sign(payload, secret, { expiresIn: '1h' });
console.log('Generated Token:', token);

// Verify the token
try {
  const decoded = jwt.verify(token, secret);
  console.log('Decoded Payload:', decoded);
  // Output: Decoded Payload: { sub: '1234567890', name: 'John Doe', iat: 1516239022, exp: 1516242622 }
} catch (err) {
  console.error('Verification failed:', err.message);
}
Enter fullscreen mode Exit fullscreen mode

This code runs in a Node.js environment and outputs the token and decoded payload if valid.

For more on JWT claims, check the official spec: RFC 7519.

Way 1: Implementing Stateless Authentication

Use JWT for authentication to avoid server-side sessions. After login, the server issues a JWT containing user ID and roles. The client sends it in headers for subsequent requests.

Key benefits: Scalable for microservices, no database lookups needed for validation.

In practice, on login:

  1. Validate credentials.
  2. Generate JWT with user data.
  3. Return it to client.

Client stores it (e.g., in localStorage) and includes in Authorization: Bearer <token>.

Example server-side code (Express.js app):

const express = require('express');
const jwt = require('jsonwebtoken');
const bodyParser = require('body-parser');

const app = express();
app.use(bodyParser.json());

const secret = 'your-256-bit-secret';
const users = [{ id: 1, username: 'user', password: 'pass' }];  // Mock DB

// Login route
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username && u.password === password);
  if (!user) return res.status(401).send('Invalid credentials');

  const token = jwt.sign({ sub: user.id, username: user.username }, secret, { expiresIn: '1h' });
  res.json({ token });
});

// Protected route
app.get('/protected', (req, res) => {
  const authHeader = req.headers.authorization;
  if (!authHeader) return res.status(401).send('No token');

  const token = authHeader.split(' ')[1];
  try {
    const decoded = jwt.verify(token, secret);
    res.json({ message: 'Access granted', user: decoded });
    // Output in response: { "message": "Access granted", "user": { "sub": 1, "username": "user", "iat": 1694080000, "exp": 1694083600 } }
  } catch (err) {
    res.status(403).send('Invalid token');
  }
});

app.listen(3000, () => console.log('Server running on port 3000'));
Enter fullscreen mode Exit fullscreen mode

Run this with node app.js, POST to /login with {"username":"user","password":"pass"}, get token, then GET /protected with Authorization header.

Way 2: Enforcing Role-Based Authorization

Extend JWT for authorization by adding roles or permissions in the payload. On verification, check if the user has the required role.

Bold point: This keeps logic simple—decode token and compare claims.

For example, add a roles array: ["admin", "user"].

Table of common claims for auth:

Claim Description Example Value
sub User ID "user123"
roles User roles ["admin"]
scope Permissions "read:write"

Code to check roles in a middleware:

const jwt = require('jsonwebtoken');

function authMiddleware(requiredRole) {
  return (req, res, next) => {
    const authHeader = req.headers.authorization;
    if (!authHeader) return res.status(401).send('No token');

    const token = authHeader.split(' ')[1];
    try {
      const decoded = jwt.verify(token, 'your-256-bit-secret');
      if (!decoded.roles || !decoded.roles.includes(requiredRole)) {
        return res.status(403).send('Insufficient permissions');
      }
      req.user = decoded;
      next();
    } catch (err) {
      res.status(403).send('Invalid token');
    }
  };
}

// Usage in Express route
app.get('/admin', authMiddleware('admin'), (req, res) => {
  res.json({ message: 'Admin access', user: req.user });
  // Output: { "message": "Admin access", "user": { "sub": "user123", "roles": ["admin"], ... } }
});
Enter fullscreen mode Exit fullscreen mode

This middleware can be reused across routes.

Learn more about role-based access: Auth0 Guide.

Way 3: Enabling Single Sign-On Across Services

JWT facilitates SSO by allowing one authentication to work across multiple apps. A central auth server issues the token, and services validate it using a shared secret or public key.

Key setup: Use asymmetric keys (RS256) for better security in distributed setups.

Process:

  • User logs in to auth service.
  • Gets JWT.
  • Sends to other services, which verify signature.

Example with public/private keys (generate with openssl):

First, generate keys:

openssl genrsa -out private.pem 2048
openssl rsa -in private.pem -outform PEM -pubout -out public.pem
Enter fullscreen mode Exit fullscreen mode

Node.js code for auth server:

const jwt = require('jsonwebtoken');
const fs = require('fs');

const privateKey = fs.readFileSync('private.pem', 'utf8');

// Sign JWT
const token = jwt.sign({ sub: 'user123' }, privateKey, { algorithm: 'RS256', expiresIn: '1h' });
console.log('Token:', token);
Enter fullscreen mode Exit fullscreen mode

Verification in another service:

const publicKey = fs.readFileSync('public.pem', 'utf8');

try {
  const decoded = jwt.verify(token, publicKey, { algorithms: ['RS256'] });
  console.log('Decoded:', decoded);
  // Output: Decoded: { sub: 'user123', iat: 1694080000, exp: 1694083600 }
} catch (err) {
  console.error('Invalid:', err.message);
}
Enter fullscreen mode Exit fullscreen mode

This ensures trust without sharing secrets.

Way 4: Securing API Rate Limiting

Incorporate JWT for rate limiting by embedding usage data or identifiers. Track requests per token without sessions.

Approach: Use sub for user ID, and on each request, check against a rate limiter (e.g., Redis-based).

Table for rate limit strategies:

Strategy How It Works Pros
Fixed Window Count in time windows Simple
Token Bucket Refill tokens over time Flexible

Example with express-rate-limit:

Install: npm install express-rate-limit.

const express = require('express');
const jwt = require('jsonwebtoken');
const rateLimit = require('express-rate-limit');

const app = express();
const secret = 'your-256-bit-secret';

// Middleware to extract user from JWT
function getUserFromToken(req) {
  const token = req.headers.authorization?.split(' ')[1];
  if (!token) return 'anonymous';
  try {
    const decoded = jwt.verify(token, secret);
    return decoded.sub;
  } catch {
    return 'anonymous';
  }
}

// Rate limiter keyed by user
const limiter = rateLimit({
  windowMs: 15 * 60 * 1000,  // 15 minutes
  max: 100,  // Limit each user to 100 requests
  keyGenerator: getUserFromToken
});

app.use(limiter);

app.get('/api', (req, res) => {
  res.send('Request successful');
  // If limit exceeded, responds with 429 Too Many Requests
});

app.listen(3000);
Enter fullscreen mode Exit fullscreen mode

This limits based on JWT's subject.

For advanced limiting: Redis Rate Limiting.

Way 5: Facilitating Secure Data Exchange Between Microservices

JWT can carry data between services securely. Service A signs a JWT with payload, Service B verifies and uses it.

Advantage: No need for repeated API calls; data is embedded.

Example: Service A generates token with order details.

const jwt = require('jsonwebtoken');
const secret = 'shared-secret-between-services';  // Or use public/private keys

// Service A: Generate token
const payload = {
  orderId: 'abc123',
  userId: 'user456',
  items: ['item1', 'item2']
};
const token = jwt.sign(payload, secret, { expiresIn: '5m' });
console.log('Token for Service B:', token);

// Service B: Verify and process
try {
  const decoded = jwt.verify(token, secret);
  console.log('Received data:', decoded);
  // Process order: { orderId: 'abc123', userId: 'user456', items: [ 'item1', 'item2' ], iat: ..., exp: ... }
} catch (err) {
  console.error('Invalid data:', err.message);
}
Enter fullscreen mode Exit fullscreen mode

This ensures data integrity during transfer.

Adopting JWT Best Practices

To use JWT effectively:

  • Set short expirations: Use exp claim, refresh tokens for long sessions.
  • Validate all claims: Check iss, aud (audience) to prevent misuse.
  • Use secure algorithms: Prefer RS256 over HS256 for public verifiability.
  • Store securely: Clients should use HttpOnly cookies if possible, avoid localStorage for XSS risks.
  • Handle revocation: For blacklisting, maintain a list or use short-lived tokens.

Implement token refresh:

// Refresh endpoint
app.post('/refresh', (req, res) => {
  const refreshToken = req.body.refreshToken;  // Assume stored securely
  if (refreshToken !== 'valid-refresh') return res.status(403).send('Invalid');

  const newToken = jwt.sign({ sub: 'user123' }, secret, { expiresIn: '1h' });
  res.json({ token: newToken });
});
Enter fullscreen mode Exit fullscreen mode

Avoiding Common JWT Mistakes

Watch for these issues:

  • Secret exposure: Never commit secrets to git; use env vars.
  • No encryption: JWT is signed, not encrypted—don't put sensitive data in payload.
  • Infinite tokens: Always include exp to limit lifetime.
  • Algorithm none: Force specific algorithms in verify options.

Example of secure verify:

jwt.verify(token, secret, { algorithms: ['HS256'] });  // Restrict to HS256
Enter fullscreen mode Exit fullscreen mode

By addressing these, your JWT usage stays robust.

JWT offers flexibility across authentication, authorization, and more. Choose the right way based on your app's needs—start with basics and scale up. Experiment with these examples in your projects to see the impact.

Top comments (0)