jsonwebtoken signs and verifies JSON Web Tokens with three calls: jwt.sign(), jwt.verify() and jwt.decode(). It handles the JWS layer through the jws and jwa packages and does the cryptography with node:crypto, so keys can be PEM strings, buffers or KeyObject instances. jwt.decode() only parses the token — it never checks the signature, and code that reads claims from it trusts whatever the sender wrote.
Version 9 closed the classic algorithm-confusion paths, and the check confirmed it for 9.0.3: without an algorithms option the allowed list comes from the key type, an HS256 token keyed with the text of a public key is refused, and alg: none passes only when you list none and supply no key at all. Pass algorithms anyway — it documents the contract and keeps working if the key type ever changes.
The algorithm list stops at the RFC 7518 set: there is no EdDSA, Ed25519 or ES256K. A token from an issuer that signs with Ed25519 — common with jose, Go and Rust stacks — cannot be verified here; use jose instead. ES256 also requires a P-256 key, and signing with a P-384 key under that name throws rather than producing a token other libraries would reject.
ECDSA signatures inside the token are the 64-byte raw r‖s form that JWS mandates. When a token was assembled by hand from openssl dgst, Java's SHA256withECDSA or a cloud KMS, its signature is DER and verification fails with "ES256" signatures must be "64" bytes — paste the token into the JWT debugger to see the signature length, and fix it with the DER ⇄ raw converter.
Two defaults surprise many teams, one stricter and one looser than they assume: clockTolerance is zero, so a few seconds of clock drift between servers already yields jwt expired, and exp is optional, so a token issued without it never expires. Set expiresIn when signing and maxAge or an explicit exp requirement when verifying; check HMAC secrets with the JWT secret checker.