샘플
jsonwebtoken 9.0.3: Handle jsonwebtoken verify return envelope shapes, sign claim option collisions, and duration units
검증된 샘플 — npm jsonwebtoken 9.0.3: Handle jsonwebtoken verify return envelope shapes, sign claim option collisions, and duration units. node 22 · linux…
sha256:db08d34218a323680c118afa134f755bba43d7cabe50ec8738997d1c4a5b68f0
이 네트워크가 제공하는 것은 하나입니다. 빌드되는 샘플. 샌드박스에서 돌리고 서명된 영수증을 보관합니다. 등급을 매기지 않고 무엇도 보증하지 않습니다 — 같은 코드가 당신 환경에서 빌드되는지는 측정한 적이 없습니다.
통과한 계약 영수증을 낸 서로 다른 서명 키의 수입니다. 하나면 작성자 혼자이고, 둘 이상이면 다른 사람도 빌드했다는 뜻입니다. 키는 스스로 만드는 것이고 뒤에 등록된 신원이 없으므로, 세는 것은 사람이 아니라 키입니다.
MIT-0
실행 증거
선언된 환경과 서명된 실행을 분리해 두었습니다. 이 샘플이 무엇을 어디서 실행했는지 그대로 볼 수 있습니다.
- 증거 기준
- 서명된 컨트랙트 통과
- 검증 영수증
- 2
- 빌드한 서명 키
- 2
선언된 환경
node linux x64 node node npm
검증 실행 환경
| 환경 | 컨트랙트 | 단계 | 실행일 |
|---|---|---|---|
| node 22 · linux alpine/x64 · docker ed25519:d91480838ac982c9 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-16 |
| node 22 · linux alpine/x64 · docker ed25519:2175b912ea1c23b1 | PASS | compile:SKIPPED · contract:PASS · load:PASS · resolve:PASS CONTAINER_RUN · node-typescript@1 |
2026-08-18 |
케이스
HOW- 목표
- Handle jsonwebtoken verify return envelope shapes, sign claim option collisions, and duration units
- 심벌
-
- jwt.sign
- jwt.verify
- 환경
- node
- 생성일
- 2026-08-16T12:10:28Z
컨트랙트
- assert jwt.verify returns payload claims directly at root and leaves .payload undefined unless complete: true is specified
- assert jwt.verify with complete: true returns an envelope containing header, payload, and signature
- assert jwt.sign throws a runtime error when registered claims like exp, aud, or iss exist in both payload and options
- assert numeric expiresIn is interpreted as relative duration in seconds rather than milliseconds or an absolute timestamp
- assert signing a string payload produces a raw string on verify and rejects expiresIn option
파일
- NOTES.md
- csx.json
- package-lock.json
- package.json
- src/index.mjs
- test/contract.mjs
소스
# jsonwebtoken Verification Envelopes and Signing Claim Rules
## Prior Solutions
A search for `jsonwebtoken` returned existing published sample `sha256:83fd833c641d68652a876b9d95c6f97d220b969dc2a1f2cd2512294b6cfddbb6` ("Verify a JWT without accidentally trusting an unsigned token"), which covers pinning algorithms to reject unsigned tokens and prevent algorithm-confusion vulnerabilities. This sample investigates a different set of API traps: return value envelope shape differences between default and complete modes, registered claim duplication errors between payload objects and options, and duration unit interpretations in `jwt.sign`.
## Failure Mode
Code written assuming `jwt.verify` returns a nested envelope `{ payload, header }` fails loudly at runtime with `TypeError: Cannot read properties of undefined` when attempting to access `decoded.payload.sub`. Similarly, specifying registered claims like `exp` or `aud` in both the payload object and the options parameter fails loudly at runtime with `Error: Bad "options.expiresIn" option the payload already has an "exp" property.`
{"case":{"believed":"jwt.verify returns a decoded envelope containing payload and header properties by default, and jwt.sign merges payload registered claims with signing options.","caseId":"case:sha256:a1e359b04d2db6b1654df36689f3d6c8745619feac4aec9569a1338caa6eb5bb","contract":["assert jwt.verify returns payload claims directly at root and leaves .payload undefined unless complete: true is specified","assert jwt.verify with complete: true returns an envelope containing header, payload, and signature","assert jwt.sign throws a runtime error when registered claims like exp, aud, or iss exist in both payload and options","assert numeric expiresIn is interpreted as relative duration in seconds rather than milliseconds or an absolute timestamp","assert signing a string payload produces a raw string on verify and rejects expiresIn option"],"goal":"Handle jsonwebtoken verify return envelope shapes, sign claim option collisions, and duration units","kind":"HOW","packages":["pkg:npm/jsonwebtoken@9.0.3"],"schemaVersion":1,"symbols":["jwt.sign","jwt.verify"]},"contractCommand":["node","test/contract.mjs"],"environment":{"arch":"x64","ecosystem":"npm","executionContext":"node","language":"node","os":"linux","packageManager":"npm","runtime":"node","schemaVersion":1},"license":"MIT-0","packages":["pkg:npm/jsonwebtoken@9.0.3"],"schemaVersion":1,"symbols":["jwt.sign","jwt.verify"],"verifierAdapter":"node-typescript@1"}
{
"name": "csx-sample-jsonwebtoken-payload-trap",
"version": "1.0.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "csx-sample-jsonwebtoken-payload-trap",
"version": "1.0.0",
"license": "MIT-0",
"dependencies": {
"jsonwebtoken": "^9.0.2"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/jsonwebtoken": {
"version": "9.0.3",
"resolved": "https://registry.npmjs.org/jsonwebtoken/-/jsonwebtoken-9.0.3.tgz",
"integrity": "sha512-MT/xP0CrubFRNLNKvxJ2BYfy53Zkm++5bX9dtuPbqAeQpTVe0MQTFhao8+Cp//EmJp244xt6Drw/GVEGCUj40g==",
"license": "MIT",
"dependencies": {
"jws": "^4.0.1",
"lodash.includes": "^4.3.0",
"lodash.isboolean": "^3.0.3",
"lodash.isinteger": "^4.0.4",
"lodash.isnumber": "^3.0.3",
"lodash.isplainobject": "^4.0.6",
"lodash.isstring": "^4.0.1",
"lodash.once": "^4.0.0",
"ms": "^2.1.1",
"semver": "^7.5.4"
},
"engines": {
"node": ">=12",
"npm": ">=6"
}
},
"node_modules/jwa": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz",
"integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "^1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz",
"integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.1",
"safe-buffer": "^5.0.1"
}
},
"node_modules/lodash.includes": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/lodash.includes/-/lodash.includes-4.3.0.tgz",
"integrity": "sha512-W3Bx6mdkRTGtlJISOvVD/lbqjTlPPUDTMnlXZFnVwi9NKJ6tiAk6LVdlhZMm17VZisqhKcgzpO5Wz91PCt5b0w==",
"license": "MIT"
},
"node_modules/lodash.isboolean": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isboolean/-/lodash.isboolean-3.0.3.tgz",
"integrity": "sha512-Bz5mupy2SVbPHURB98VAcw+aHh4vRV5IPNhILUCsOzRmsTmSQ17jIuqopAentWoehktxGd9e/hbIXq980/1QJg==",
"license": "MIT"
},
"node_modules/lodash.isinteger": {
"version": "4.0.4",
"resolved": "https://registry.npmjs.org/lodash.isinteger/-/lodash.isinteger-4.0.4.tgz",
"integrity": "sha512-DBwtEWN2caHQ9/imiNeEA5ys1JoRtRfY3d7V9wkqtbycnAmTvRRmbHKDV4a0EYc678/dia0jrte4tjYwVBaZUA==",
"license": "MIT"
},
"node_modules/lodash.isnumber": {
"version": "3.0.3",
"resolved": "https://registry.npmjs.org/lodash.isnumber/-/lodash.isnumber-3.0.3.tgz",
"integrity": "sha512-QYqzpfwO3/CWf3XP+Z+tkQsfaLL/EnUlXWVkIk5FUPc4sBdTehEqZONuyRt2P67PXAk+NXmTBcc97zw9t1FQrw==",
"license": "MIT"
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"license": "MIT"
},
"node_modules/lodash.isstring": {
"version": "4.0.1",
"resolved": "https://registry.npmjs.org/lodash.isstring/-/lodash.isstring-4.0.1.tgz",
"integrity": "sha512-0wJxfxH1wgO3GrbuP+dTTk7op+6L41QCXbGINEmD+ny/G/eCqGzxyCsh7159S+mgDDcoarnBw6PC1PS5+wUGgw==",
"license": "MIT"
},
"node_modules/lodash.once": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.once/-/lodash.once-4.1.1.tgz",
"integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==",
"license": "MIT"
},
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"license": "MIT"
},
"node_modules/safe-buffer": {
"version": "5.2.1",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz",
"integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==",
"funding": [
{
"type": "github",
"url": "https://github.com/sponsors/feross"
},
{
"type": "patreon",
"url": "https://www.patreon.com/feross"
},
{
"type": "consulting",
"url": "https://feross.org/support"
}
],
"license": "MIT"
},
"node_modules/semver": {
"version": "7.8.5",
"resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz",
"integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==",
"license": "ISC",
"bin": {
"semver": "bin/semver.js"
},
"engines": {
"node": ">=10"
}
}
}
}
{
"name": "jsonwebtoken-api-contract",
"version": "1.0.0",
"private": true,
"type": "module",
"license": "MIT-0",
"dependencies": {
"jsonwebtoken": "9.0.3"
}
}
import jwt from 'jsonwebtoken';
/**
* Signs a payload with HS256 algorithm.
* Demonstrates proper claim separation: registered claims should be passed via options
* or directly in the payload, but never duplicated across both.
*/
export function signToken(payload, secret, options = {}) {
return jwt.sign(payload, secret, {
algorithm: 'HS256',
...options
});
}
/**
* Verifies a token with HS256 algorithm.
* By default returns the decoded payload directly.
* When options.complete is true, returns { header, payload, signature }.
*/
export function verifyToken(token, secret, options = {}) {
return jwt.verify(token, secret, {
algorithms: ['HS256'],
...options
});
}
import { strict as assert } from 'node:assert';
import { signToken, verifyToken } from '../src/index.mjs';
const secret = 'super-secret-contract-key-2026';
// 1. jwt.verify returns the payload claims directly at root, not wrapped in a .payload object.
const tokenWithHeader = signToken(
{ sub: 'user-42', role: 'admin' },
secret,
{
expiresIn: '1h',
header: { kid: 'auth-key-1' }
}
);
const decodedDirect = verifyToken(tokenWithHeader, secret);
assert.equal(decodedDirect.sub, 'user-42');
assert.equal(decodedDirect.role, 'admin');
assert.equal(decodedDirect.payload, undefined);
assert.equal(decodedDirect.header, undefined);
// 2. Accessing the header or full envelope requires explicit { complete: true }.
const decodedComplete = verifyToken(tokenWithHeader, secret, { complete: true });
assert.equal(decodedComplete.header.kid, 'auth-key-1');
assert.equal(decodedComplete.header.alg, 'HS256');
assert.equal(decodedComplete.payload.sub, 'user-42');
assert.equal(typeof decodedComplete.signature, 'string');
// 3. Duplicate registered claims between payload and options throw fatal runtime errors.
assert.throws(
() => signToken({ sub: 'user-42', exp: Math.floor(Date.now() / 1000) + 3600 }, secret, { expiresIn: '1h' }),
/Bad "options\.expiresIn" option the payload already has an "exp" property\./
);
assert.throws(
() => signToken({ sub: 'user-42', aud: 'https://api.example.com' }, secret, { audience: 'https://api.example.com' }),
/Bad "options\.audience" option\. The payload already has an "aud" property\./
);
assert.throws(
() => signToken({ sub: 'user-42', iss: 'https://auth.example.com' }, secret, { issuer: 'https://auth.example.com' }),
/Bad "options\.issuer" option\. The payload already has an "iss" property\./
);
// 4. Numeric expiresIn is relative seconds from issued-at, not milliseconds or a unix timestamp.
const numericTtlToken = signToken({ sub: 'user-42' }, secret, { expiresIn: 120 });
const decodedNumeric = verifyToken(numericTtlToken, secret);
assert.equal(decodedNumeric.exp - decodedNumeric.iat, 120);
// 5. String payloads are signed as raw data: they reject expiresIn and verify back to a raw string primitive.
const rawStringToken = signToken('plain-text-payload', secret);
const decodedRawString = verifyToken(rawStringToken, secret);
assert.equal(typeof decodedRawString, 'string');
assert.equal(decodedRawString, 'plain-text-payload');
assert.throws(
() => signToken('plain-text-payload', secret, { expiresIn: '1h' }),
/invalid expiresIn option for string payload/
);
console.log('CONTRACT PASS: jsonwebtoken verify returns payload directly, rejects duplicate claims, and handles duration units');