Skip to main content
interlace
Plugin: jwtRules

require-issued-at

This rule ensures tokens have the iat claim for freshness validation

Require iat (issued at) claim for token freshness validation

Severity: 🟡 Medium
CWE: CWE-294

Rule Details

This rule ensures tokens have the iat claim for freshness validation. Most libraries add iat automatically unless explicitly disabled.

Examples

❌ Incorrect

// A noTimestamp chosen at runtime may drop iat on some path
jwt.sign(payload, secret, { noTimestamp: process.env.LEGACY === '1' });

// jose does NOT add iat unless asked
await new SignJWT({ sub })
  .setProtectedHeader({ alg: 'ES256' })
  .setExpirationTime('1h')
  .sign(key);

✅ Correct

// Default - iat is added automatically
jwt.sign(payload, secret);

// Explicit iat in payload
jwt.sign({ sub: 'user', iat: Math.floor(Date.now() / 1000) }, secret);

// noTimestamp: false keeps iat (it is the default, made explicit)
jwt.sign(payload, secret, { noTimestamp: false });

// noTimestamp: true is reported by no-timestamp-manipulation, not here —
// one defect, one finding
jwt.sign(payload, secret, { noTimestamp: true });

// jose with setIssuedAt()
await new SignJWT({ sub })
  .setProtectedHeader({ alg: 'ES256' })
  .setIssuedAt()
  .sign(key);

Options

OptionTypeDefaultDescription
trustedSanitizersstring[][]No effect — accepted so existing configs stay valid; the rule does not read it yet.
trustedAnnotationsstring[][]No effect — accepted so existing configs stay valid; the rule does not read it yet.
strictModebooleanfalseNo effect — accepted so existing configs stay valid; the rule does not read it yet.

Known False Negatives

The following patterns are not detected due to static analysis limitations:

Options from Variables, Casts and Spreads

What is read: an inline object, a same-file const, an as / satisfies cast, and a spread of such a const. Options from a parameter, an import or a call cannot be seen.

// ⚠️ NOT DETECTED - options built elsewhere
jwt.sign(payload, secret, getSignOptions());

Payload Without iat When noTimestamp Is Not Used

Why: jsonwebtoken adds iat unless noTimestamp is set, so for jsonwebtoken the rule only asks about a noTimestamp chosen at runtime (no-timestamp-manipulation owns the literal true).

// ⚠️ EDGE CASE - Most libraries auto-add iat, so this is usually safe
jwt.sign({ sub: 'user' }, secret); // iat added automatically

Mitigation: Rely on library defaults. Use require-max-age for verification-side checks.

Library Without Auto-iat

Why: Some JWT libraries don't auto-add iat, which the rule assumes.

// ❌ NOT DETECTED - Library doesn't auto-add iat
import { sign } from 'minimal-jwt-lib';
sign({ sub: 'user' }, secret); // No iat and none added by library

Mitigation: Verify library behavior. Add explicit iat in payload.

Spread of an Unresolvable Value

// ⚠️ NOT DETECTED - the spread source is a call
jwt.sign(payload, secret, { ...getSignOptions() });

Further Reading

Use this rule

npm install -D oxlint-plugin-jwt-security
.oxlintrc.json
{  "jsPlugins": ["oxlint-plugin-jwt-security"],  "rules": {    "jwt-security/require-issued-at": "error"  }}
vite.config.ts
import { defineConfig } from 'vite-plus';export default defineConfig({  lint: {    jsPlugins: ['oxlint-plugin-jwt-security'],    rules: {      'jwt-security/require-issued-at': 'error',    },  },});

oxlint-plugin-jwt-security re-exports eslint-plugin-jwt-security/oxlint: the same rules under the same IDs, and no ESLint install. Why oxlint.

Did this rule catch something? Star the repo to get new CWE coverage as we ship it — or follow the AI-code-security benchmarks behind these rules.