{"record":{"id":"b8352d9363fd4bc5","repo":"auth0/node-jsonwebtoken","slug":"iat-exp-nbf-should-be-a-number-of-seconds","errorCode":null,"errorMessage":"\"iat\"/\"exp\"/\"nbf\" should be a number of seconds","messagePattern":"\"iat\"/\"exp\"/\"nbf\" should be a number of seconds","errorType":"validation","errorClass":null,"httpStatus":null,"severity":"error","filePath":"sign.js","lineNumber":56,"sourceCode":"  exp: { isValid: isNumber, message: '\"exp\" should be a number of seconds' },\n  nbf: { isValid: isNumber, message: '\"nbf\" should be a number of seconds' }\n};\n\nfunction validate(schema, allowUnknown, object, parameterName) {\n  if (!isPlainObject(object)) {\n    throw new Error('Expected \"' + parameterName + '\" to be a plain object.');\n  }\n  Object.keys(object)\n    .forEach(function(key) {\n      const validator = schema[key];\n      if (!validator) {\n        if (!allowUnknown) {\n          throw new Error('\"' + key + '\" is not allowed in \"' + parameterName + '\"');\n        }\n        return;\n      }\n      if (!validator.isValid(object[key])) {\n        throw new Error(validator.message);\n      }\n    });\n}\n\nfunction validateOptions(options) {\n  return validate(sign_options_schema, false, options, 'options');\n}\n\nfunction validatePayload(payload) {\n  return validate(registered_claims_schema, true, payload, 'payload');\n}\n\nconst options_to_payload = {\n  'audience': 'aud',\n  'issuer': 'iss',\n  'subject': 'sub',\n  'jwtid': 'jti'\n};","sourceCodeStart":38,"sourceCodeEnd":74,"githubUrl":"https://github.com/auth0/node-jsonwebtoken/blob/b924272f29192e12926b5414546f7c5bfcc9579d/sign.js#L38-L74","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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"],"exampleFix":"// before\njwt.sign({ ...payload, exp: process.env.EXP }, secret);\n// after\njwt.sign({ ...payload, exp: Number(process.env.EXP) }, secret);","handlingStrategy":"type-guard","validationCode":"function toEpochSeconds(v) {\n  if (v instanceof Date) return Math.floor(v.getTime() / 1000);\n  const n = Number(v);\n  if (!Number.isFinite(n)) throw new TypeError('time claims must be epoch-seconds numbers');\n  return n;\n}\n['exp','nbf','iat'].forEach(c => { if (payload[c] !== undefined) payload[c] = toEpochSeconds(payload[c]); });","typeGuard":"function isEpochSeconds(v) {\n  return typeof v === 'number' && Number.isFinite(v) && Number.isInteger(v);\n}","tryCatchPattern":"try {\n  return jwt.sign(payload, secret, options);\n} catch (err) {\n  if (/should be a number of seconds/.test(err.message)) {\n    const claim = err.message.match(/\"(\\w+)\"/)[1];\n    payload = { ...payload, [claim]: Math.floor(new Date(payload[claim]).getTime() / 1000) };\n    return jwt.sign(payload, secret, options);\n  }\n  throw err;\n}","preventionTips":["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"],"tags":["jwt","claim-validation","type-error"],"backgroundTag":"jwt-invalid-payload-type","analyzedSha":"b924272f29192e12926b5414546f7c5bfcc9579d","analyzedAt":"2026-09-02T21:29:06.876Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-10T02:17:09.455Z"}