Review: JWT Verification Middleware Written by an AI Assistant
Your team asked an AI coding assistant to replace a third-party JWT library with a small in-house helper and an Express middleware for the customer support API. It opened this pull request. You are the reviewer, and nothing merges without your approval.
The assistant's description:
Removes the JWT dependency.verifyJwtchecks the signature, issuer and expiry and supports both HS and RS algorithms, so we are ready if the identity provider changes how it signs.requireAuth(["admin"])protects routes by role. No behaviour change for callers.
Where it runs: Node 20 and Express 4. The company identity provider signs every token with RS256, using one key pair for all internal apps; each app gets its own aud value, and roles mean different things in different apps. The provider's public key is published on the intranet. Logs go to a shared platform that most engineers can read.
Your task
- Flag every problem you would block the merge for. Click a line number to comment, and Shift-click to cover a range.
- For each one, choose the issue type and severity, and explain what fails and how to fix it.
- You are scored on the problems you catch, the right issue type and a clear explanation. Missing a critical problem fails the round.
Aim for 15 minutes, as in a real review round.
The algorithm comes from the token header, which the sender writes. What key is used when the header says HS256, and who already has that key?
What happens if IDP_PUBLIC_KEY is missing from the environment?
A token issued by the same provider for the staff canteen app arrives here. Which check stops it?
Compare how the HMAC signature is checked with how you would check a password hash.
Some tokens may not carry exp. How long do they last? And what ends up in the logs when a token is rejected?
A strong reviewer blocks this immediately: anyone can mint an admin token with the public key, because the code lets the token choose its own algorithm.
- Algorithm confusion (jwt.ts lines 37–38, critical). The algorithm is taken from the attacker-controlled header, and the same
keyis used for both branches. An attacker sets"alg": "HS256"and signs the token with HMAC, using the identity provider's public key PEM as the secret. The server computes the same HMAC and accepts it. Pin the allowed algorithm (RS256) per key and reject everything else, or usecrypto.createPublicKeyso the key type decides the algorithm. - Empty key fallback (requireAuth.ts line 6, major). If the environment variable is missing, the key is
"". Combined with the HS branch, a token signed with an empty HMAC secret is then valid. Fail at start-up when the key is not set. - Audience never checked (jwt.ts line 47, major). All internal apps share the provider's signing key, so a valid token issued for any other app is accepted here, and its
roleclaim, which means something else in that app, is trusted. Check thataudcontains this API's identifier. - Timing-unsafe signature comparison (jwt.ts line 39, major).
===stops at the first differing character, so response timing can leak how much of a forged HMAC is correct. Compare withcrypto.timingSafeEqualon equal-length buffers. - Tokens without
expnever expire (jwt.ts line 50, minor). A missingexpskips the check, so such a token is valid for ever. Requireexp, and allow a small clock skew. - Bearer tokens written to shared logs (requireAuth.ts line 29, minor). A rejected token may still be valid elsewhere (for another audience, or a few seconds of clock skew), and the logs are widely readable. Log the reason and perhaps the
subor a hash, never the token.