Repository navigation
jwt.verify returns JSON-stringified payload (string) instead of parsed object — auth middleware breaks #927
Description
Repro (4 lines, no Fastify, no MySQL)
import jwt from "jsonwebtoken";
import { readFileSync } from "fs";
const PEM = readFileSync("/tmp/skelpo-jwt.pem", "utf8");
const TOK = jwt.sign({ sub: "u-001", acc: "a-001" }, PEM, { algorithm: "ES256", issuer: "z" });
const decoded = jwt.verify(TOK, PEM, { algorithms: ["ES256"], issuer: "z" }) as any;
console.log("typeof decoded:", typeof decoded);
console.log("decoded JSON:", JSON.stringify(decoded));
console.log("decoded.sub:", decoded.sub);Observed (perry 0.5.958)
[jwt-verify] success, claims={"acc":"a-001","sub":"u-001"} ← binding decoded correctly
typeof decoded: string ← but returned as STRING
decoded JSON: "{\"acc\":\"a-001\",\"sub\":\"u-001\"}" ← JSON-encoded-string-of-the-object
decoded.sub: undefined ← property access yields undefined
Expected (tsx / node — same source)
typeof decoded: object
decoded JSON: {"sub":"u-001","acc":"a-001","iat":..., "iss":"z"}
decoded.sub: u-001
Per spec / jsonwebtoken README, jwt.verify returns the decoded payload as an object (it's the JwtPayload type). Perry's binding decodes correctly internally (the [jwt-verify] success log shows the right shape) but the value returned to user code is the JSON-stringified object, not the parsed one.
How this breaks shop-admin
server/crypto/jwt.ts::verifyAccessToken does:
const decoded = jwt.verify(token, loadKey(), { algorithms: [ALG], issuer: ISSUER }) as jwt.JwtPayload;
if (typeof decoded.sub !== "string" || typeof decoded["acc"] !== "string" || ...) {
return null;
}
return { sub: decoded.sub, acc: decoded["acc"] as string, ... };Under perry: decoded is a string. decoded.sub is undefined. typeof undefined !== "string" is true → returns null. The auth middleware then early-returns without setting req.user, every authenticated route sees req.user === undefined, and requireUser(req) throws — which on its own triggers a second perry bug (issue to be filed separately): throw new Error/ApiError in an async route handler crashes the process instead of being caught by Fastify.
This is the residual blocker for the shop-admin native server after #859 and #915 closed; signup itself now works end-to-end (HTTP 201 with full JWT JSON, all 5 tables populate) but the very first authenticated request fails because of this jwt.verify return-shape bug.
Pointer
Likely in the perry-stdlib jsonwebtoken binding. The fix presumably is to return the parsed payload object directly to the JS side rather than the JSON-encoded representation; the js_jsonwebtoken_verify (or whatever wrapper) needs to construct a JS object from the verified payload rather than handing back the JSON text.
jwt.sign works correctly (verified by my #915 minimal repro and the standalone working).
Files: this repro is short enough that nothing's committed to the user repo for it; happy to PR a test fixture into test-files/ if useful.
Local checkout: clean at perry HEAD (v0.5.958), no source edits.
Activity
fix(codegen): #927 — jwt.verify returns parsed object, not JSON-strin…
fix: #927 — jwt.verify dispatches per algorithm (ES256/RS256), accept…
fix(jwt): #1074 — algorithm option resolves const-bound ident before …