Skip to content

jwt.verify returns JSON-stringified payload (string) instead of parsed object — auth middleware breaks #927

@proggeramlug

Description

@proggeramlug

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions