X Xerobit

JWT Standard Claims — iss, sub, aud, exp, nbf, iat, jti Explained

JWT registered claims define standard metadata for tokens: issuer (iss), subject (sub), audience (aud), expiration (exp), not-before (nbf), issued-at (iat), and JWT ID (jti)....

Mian Ali Khalid · · 4 min read
Use the tool
JWT Decoder
Decode and inspect JSON Web Tokens. Local-only — tokens never leave your browser.
Open JWT Decoder →

JWT registered claims are standardized fields in the payload. They’re optional but widely recognized — libraries like jsonwebtoken validate them automatically.

Decode and inspect JWT claims with the JWT Decoder.

The seven registered claims (RFC 7519)

{
  "iss": "https://auth.example.com",
  "sub": "user:123456",
  "aud": ["https://api.example.com", "https://app.example.com"],
  "exp": 1747070400,
  "nbf": 1747066800,
  "iat": 1747066800,
  "jti": "550e8400-e29b-41d4-a716-446655440000"
}

iss — Issuer

// Who created and signed the token
// Value: usually the authentication server URL

jwt.sign({ sub: userId }, secret, {
  issuer: 'https://auth.example.com',
  // Adds: iss: "https://auth.example.com"
});

// Verify: rejects tokens from unexpected issuers
jwt.verify(token, secret, {
  issuer: 'https://auth.example.com',
});
// Throws if iss ≠ "https://auth.example.com"

sub — Subject

// Identifies the principal the token is about
// Usually the user ID — must be unique within the issuer

jwt.sign({
  sub: 'user:123456',    // User ID as string
  name: 'Alice Johnson',
  email: 'alice@example.com',
}, secret);

// Access it after verification:
const { sub } = jwt.verify(token, secret);
const userId = sub;

aud — Audience

// Who is the token intended for
// Validates that the token is being used by the right service

// Single audience:
jwt.sign({ sub: 'user:123' }, secret, {
  audience: 'https://api.example.com',
});

// Multiple audiences:
jwt.sign({ sub: 'user:123' }, secret, {
  audience: ['https://api.example.com', 'https://mobile.example.com'],
});

// Verify — must match at least one audience:
jwt.verify(token, secret, {
  audience: 'https://api.example.com',
});
// Throws if "https://api.example.com" not in aud

exp — Expiration Time

// Unix timestamp after which the token must not be accepted
// All JWT libraries validate this automatically

jwt.sign({ sub: 'user:123' }, secret, {
  expiresIn: '15m',    // 15 minutes
  // expiresIn: 900,   // 900 seconds
  // expiresIn: '1d',  // 1 day
  // Adds: exp: Math.floor(Date.now()/1000) + 900
});

// When token is expired:
jwt.verify(expiredToken, secret);
// Throws: TokenExpiredError: jwt expired

// Check expiry without throwing:
const decoded = jwt.decode(token);
const isExpired = decoded.exp < Math.floor(Date.now() / 1000);

// Allow some clock skew (servers may have slightly different times):
jwt.verify(token, secret, {
  clockTolerance: 30,  // Allow 30 seconds of clock difference
});

nbf — Not Before

// Unix timestamp before which the token must not be accepted
// Useful for "issued in advance" tokens (e.g., scheduled emails)

const futureTime = Math.floor(Date.now() / 1000) + 3600; // 1 hour from now

jwt.sign({ sub: 'user:123' }, secret, {
  notBefore: '1h',     // Not valid for 1 hour
  // or: notBefore: 3600
});

// Throws: NotBeforeError: jwt not active
jwt.verify(notYetActiveToken, secret);

iat — Issued At

// Unix timestamp of when the token was issued
// Added automatically by most libraries

jwt.sign({ sub: 'user:123' }, secret);
// Payload includes: iat: 1747066800

// Useful for:
// - Detecting tokens issued before a password change
// - Logging when tokens were created
// - Computing token age

const decoded = jwt.verify(token, secret);
const tokenAgeSeconds = Math.floor(Date.now() / 1000) - decoded.iat;

// Reject tokens older than X (manual check):
if (tokenAgeSeconds > 86400) throw new Error('Token too old');

// Or use issuedAt validation:
jwt.sign({ sub: 'user:123' }, secret, {
  // No direct option — check iat manually after verify
});

jti — JWT ID

// Unique identifier for the token
// Enables token revocation and replay attack prevention
import { v4 as uuidv4 } from 'uuid';

// Create token with unique ID:
const jti = uuidv4();
jwt.sign({ sub: 'user:123', jti }, secret, { expiresIn: '15m' });

// Revoke a specific token (server-side blocklist):
const revokedTokens = new Set(); // Use Redis in production

app.post('/auth/logout', (req, res) => {
  const decoded = jwt.verify(req.headers.authorization.split(' ')[1], secret);
  revokedTokens.add(decoded.jti);
  res.json({ message: 'Logged out' });
});

// Check blocklist on every request:
function authMiddleware(req, res, next) {
  const token = req.headers.authorization?.split(' ')[1];
  const decoded = jwt.verify(token, secret);
  
  if (revokedTokens.has(decoded.jti)) {
    return res.status(401).json({ error: 'Token revoked' });
  }
  
  req.user = decoded;
  next();
}

Complete token with all claims

import { v4 as uuidv4 } from 'uuid';

function createAccessToken(userId, userRoles) {
  return jwt.sign({
    // Standard claims:
    iss: 'https://auth.example.com',
    aud: 'https://api.example.com',
    sub: `user:${userId}`,
    jti: uuidv4(),
    
    // Custom claims (use namespaced keys to avoid conflicts):
    'https://example.com/roles': userRoles,
    'https://example.com/version': 1,
  }, ACCESS_SECRET, {
    expiresIn: '15m',
    // iat and exp added automatically
  });
}

Related posts

Related tool

JWT Decoder

Decode and inspect JSON Web Tokens. Local-only — tokens never leave your browser.

Written by Mian Ali Khalid. Part of the Encoding & Crypto pillar.