jsonwebtoken HS256 and RS256: working versions (tested 2026-10-15)
Sign and verify JSON Web Tokens with jsonwebtoken: HS256 secrets plus RS256 RSA keys from node:crypto, with deterministic expiry via clockTimestamp, issuer and audience checks, and rejection of the none algorithm.
passed8 passed · 0 failed2026-10-15 22:58 UTC- Status
- passed
- Last passed
- 2026-10-15 22:58 UTC
- Tests
- 8 / 8
- Language
- TypeScript
- Runtime
- node v22.23.3
pinned versions4
jsonwebtoken@9.0.3@types/jsonwebtoken@9.0.10@types/node@26.6.5tsx@4.23.15
run it
npm install --no-audit --no-fund --ignore-scripts=false npm test
test environment
node v22.23.3 linux 6.17.0-1022-azure x64 runner: github-actions 38820570375
The code
Exactly the files that passed. Your agent can pull them with get_recipe("jsonwebtoken-hs256-rs256") (install), or see them on GitHub.
package.json
{
"name": "jsonwebtoken-hs256-rs256",
"private": true,
"type": "module",
"scripts": {
"test": "node --import tsx --test test/*.test.ts"
},
"dependencies": {
"jsonwebtoken": "9.0.3"
},
"devDependencies": {
"@types/jsonwebtoken": "9.0.10",
"@types/node": "26.6.5",
"tsx": "4.23.15"
}
}
src/tokens.ts
import jwt from "jsonwebtoken";
export const HS_SECRET = "provenpin-hs256-secret";
export const ISSUER = "https://issuer.provenpin.dev";
export const AUDIENCE = "provenpin-api";
export type Payload = { sub: string } & Record<string, unknown>;
export type VerifyOptions = {
issuer?: string;
audience?: string;
clockTimestamp?: number;
};
export function signHs256(payload: Payload, expiresIn: number | string = "1h"): string {
return jwt.sign(payload, HS_SECRET, {
algorithm: "HS256",
expiresIn,
issuer: ISSUER,
audience: AUDIENCE,
});
}
export function verifyHs256(token: string, options: VerifyOptions = {}): Payload {
return jwt.verify(token, HS_SECRET, {
algorithms: ["HS256"],
issuer: options.issuer ?? ISSUER,
audience: options.audience ?? AUDIENCE,
clockTimestamp: options.clockTimestamp,
}) as Payload;
}
export function signRs256(
payload: Payload,
privateKey: string,
expiresIn: number | string = "1h",
): string {
return jwt.sign(payload, privateKey, {
algorithm: "RS256",
expiresIn,
issuer: ISSUER,
audience: AUDIENCE,
});
}
export function verifyRs256(
token: string,
publicKey: string,
options: VerifyOptions = {},
): Payload {
return jwt.verify(token, publicKey, {
algorithms: ["RS256"],
issuer: options.issuer ?? ISSUER,
audience: options.audience ?? AUDIENCE,
clockTimestamp: options.clockTimestamp,
}) as Payload;
}
test/tokens.test.ts
import { test } from "node:test";
import assert from "node:assert/strict";
import { generateKeyPairSync } from "node:crypto";
import jwt from "jsonwebtoken";
import {
AUDIENCE,
ISSUER,
signHs256,
signRs256,
verifyHs256,
verifyRs256,
} from "../src/tokens.ts";
const { publicKey, privateKey } = generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" },
});
test("HS256: signs and verifies with expiresIn, issuer and audience", () => {
const token = signHs256({ sub: "user-1" });
const decoded = jwt.decode(token, { complete: true }) as {
header: { alg: string };
payload: { sub: string; iss: string; aud: string; iat: number; exp: number };
};
assert.equal(decoded.header.alg, "HS256");
assert.equal(decoded.payload.iss, ISSUER);
assert.equal(decoded.payload.aud, AUDIENCE);
assert.equal(decoded.payload.exp - decoded.payload.iat, 3600);
assert.equal(verifyHs256(token).sub, "user-1");
});
test("HS256: rejects a token issued for a different issuer", () => {
const token = signHs256({ sub: "user-iss" });
assert.throws(
() => verifyHs256(token, { issuer: "https://evil.example" }),
(err: unknown) => {
assert.ok(err instanceof jwt.JsonWebTokenError);
assert.match((err as Error).message, /issuer/);
return true;
},
);
});
test("HS256: rejects a token presented to the wrong audience", () => {
const token = signHs256({ sub: "user-aud" });
assert.throws(
() => verifyHs256(token, { audience: "someone-else" }),
(err: unknown) => {
assert.ok(err instanceof jwt.JsonWebTokenError);
assert.match((err as Error).message, /audience/);
return true;
},
);
});
test("HS256: detects TokenExpiredError deterministically via clockTimestamp", () => {
const issuedAt = 1_700_000_000;
const token = signHs256({ sub: "user-exp", iat: issuedAt }, 60);
assert.equal(verifyHs256(token, { clockTimestamp: issuedAt }).sub, "user-exp");
assert.throws(
() => verifyHs256(token, { clockTimestamp: issuedAt + 61 }),
(err: unknown) => {
assert.ok(err instanceof jwt.TokenExpiredError);
assert.equal(err.expiredAt.getTime(), (issuedAt + 60) * 1000);
return true;
},
);
});
test("HS256: rejects the 'none' algorithm", () => {
const encode = (value: object) =>
Buffer.from(JSON.stringify(value)).toString("base64url");
const token = `${encode({ alg: "none", typ: "JWT" })}.${encode({
sub: "attacker",
iss: ISSUER,
aud: AUDIENCE,
exp: Math.floor(Date.now() / 1000) + 3600,
})}.`;
assert.throws(
() => verifyHs256(token),
(err: unknown) => {
assert.ok(err instanceof jwt.JsonWebTokenError);
assert.ok(!(err instanceof jwt.TokenExpiredError));
return true;
},
);
});
test("RS256: signs with the private key and verifies with the public key", () => {
const token = signRs256({ sub: "user-rs" }, privateKey);
const decoded = jwt.decode(token, { complete: true }) as { header: { alg: string } };
assert.equal(decoded.header.alg, "RS256");
assert.equal(verifyRs256(token, publicKey).sub, "user-rs");
});
test("RS256: rejects an HS256 token presented as RS256", () => {
const hsToken = signHs256({ sub: "user-hs" });
assert.throws(
() => verifyRs256(hsToken, publicKey),
(err: unknown) => {
assert.ok(err instanceof jwt.JsonWebTokenError);
return true;
},
);
});
test("RS256: rejects a token signed by an unrelated key", () => {
const other = generateKeyPairSync("rsa", {
modulusLength: 2048,
publicKeyEncoding: { type: "spki", format: "pem" },
privateKeyEncoding: { type: "pkcs8", format: "pem" },
});
const token = signRs256({ sub: "user-other" }, other.privateKey);
assert.throws(
() => verifyRs256(token, publicKey),
(err: unknown) => {
assert.ok(err instanceof jwt.JsonWebTokenError);
assert.match((err as Error).message, /invalid signature/);
return true;
},
);
});
Connect to ProvenPin
Add the free MCP server so your agent can pull this recipe and others like it. Same setup as the docs.
terminal
claude mcp add --transport http provenpin https://mcp.provenpin.com/mcp
~/.cursor/mcp.json
{ "mcpServers": { "provenpin": { "url": "https://mcp.provenpin.com/mcp" } } }.vscode/mcp.json
{ "servers": { "provenpin": { "type": "http", "url": "https://mcp.provenpin.com/mcp" } } }~/.codeium/windsurf/mcp_config.json
{ "mcpServers": { "provenpin": { "serverUrl": "https://mcp.provenpin.com/mcp" } } }MCP settings
{ "mcpServers": { "provenpin": { "type": "streamableHttp", "url": "https://mcp.provenpin.com/mcp" } } }Any other MCP client: Streamable HTTP at https://mcp.provenpin.com/mcp. No API key, no signup.
Related stacks
Express 5 + jsonwebtoken auth middleware
Issue HS256 JWTs on login and protect routes with a bearer-token middleware.
Express · jsonwebtoken
Express 5 + Passport JWT strategy
Protect Express 5 routes with passport-jwt (bearer tokens signed by jsonwebtoken), stateless sessions and role checks.
Express · passport · passport-jwt · jsonwebtoken