auth0/node-jsonwebtoken · error
"iat"/"exp"/"nbf" should be a number of seconds
Error message
"iat"/"exp"/"nbf" should be a number of seconds
What it means
The claim validators for iat, exp, and nbf require numbers (seconds since epoch). The schema maps each claim to an isNumber check with a message like '"exp" should be a number of seconds'; passing a string, Date, or non-finite value throws this validator message at sign time.
Source
Thrown at sign.js:56
exp: { isValid: isNumber, message: '"exp" should be a number of seconds' },
nbf: { isValid: isNumber, message: '"nbf" should be a number of seconds' }
};
function validate(schema, allowUnknown, object, parameterName) {
if (!isPlainObject(object)) {
throw new Error('Expected "' + parameterName + '" to be a plain object.');
}
Object.keys(object)
.forEach(function(key) {
const validator = schema[key];
if (!validator) {
if (!allowUnknown) {
throw new Error('"' + key + '" is not allowed in "' + parameterName + '"');
}
return;
}
if (!validator.isValid(object[key])) {
throw new Error(validator.message);
}
});
}
function validateOptions(options) {
return validate(sign_options_schema, false, options, 'options');
}
function validatePayload(payload) {
return validate(registered_claims_schema, true, payload, 'payload');
}
const options_to_payload = {
'audience': 'aud',
'issuer': 'iss',
'subject': 'sub',
'jwtid': 'jti'
};View on GitHub (pinned to b924272f29)
Solutions
- Convert to epoch seconds: Math.floor(new Date(value).getTime() / 1000)
- Use Number(value)/parseInt on any string coming from env or query input
- Prefer the expiresIn option with a string like '1h' and let the library compute exp
- Ensure iat/nbf are also plain numbers if you set them manually
Example fix
// before
jwt.sign({ ...payload, exp: process.env.EXP }, secret);
// after
jwt.sign({ ...payload, exp: Number(process.env.EXP) }, secret); Defensive patterns
Strategy: type-guard
Validate before calling
function toEpochSeconds(v) {
if (v instanceof Date) return Math.floor(v.getTime() / 1000);
const n = Number(v);
if (!Number.isFinite(n)) throw new TypeError('time claims must be epoch-seconds numbers');
return n;
}
['exp','nbf','iat'].forEach(c => { if (payload[c] !== undefined) payload[c] = toEpochSeconds(payload[c]); }); Type guard
function isEpochSeconds(v) {
return typeof v === 'number' && Number.isFinite(v) && Number.isInteger(v);
} Try / catch
try {
return jwt.sign(payload, secret, options);
} catch (err) {
if (/should be a number of seconds/.test(err.message)) {
const claim = err.message.match(/"(\w+)"/)[1];
payload = { ...payload, [claim]: Math.floor(new Date(payload[claim]).getTime() / 1000) };
return jwt.sign(payload, secret, options);
}
throw err;
} Prevention
- Coerce all time inputs through one toEpochSeconds helper at ingestion
- Prefer expiresIn option strings ('1h') over manual exp computation
- Never pass Date, moment, or dayjs objects directly as claims
- Number()-convert values from env vars and query strings before assigning claims
When it happens
Trigger: jwt.sign({ exp: '2030-01-01' }, secret) or jwt.sign(payload, secret, { expiresIn: 3600 }) is fine, but directly setting exp/nbf/iat to strings, Dates, or numbers-as-strings in the payload fails validation.
Common situations: Reading expiration values from env vars or HTTP input as strings without Number() conversion; passing Date objects instead of Math.floor(date.getTime()/1000); setting exp with a dayjs/moment object.
Related errors
- Expected "${parameterName}" to be a plain object.
- Unknown key type "${keyType}".
- "alg" parameter for "${keyType}" key type must be one of: ${
- "alg" parameter "${algorithm}" requires curve "${allowedCurv
- Invalid key for this operation, its RSA-PSS parameters do no
AI-assisted analysis of auth0/node-jsonwebtoken@b924272f29 (2026-09-02).
Data as JSON: /api/errors/b8352d9363fd4bc5.
Report an issue: GitHub.