jsonwebtoken は、jwt.sign()、jwt.verify()、jwt.decode() の 3 つの呼び出しで JSON Web Token の署名と検証を行います。JWS の層は jws パッケージと jwa パッケージで処理し、暗号処理は node:crypto で行うため、鍵には PEM 文字列、バッファー、KeyObject インスタンスを使えます。jwt.decode() はトークンを解析するだけで、署名は一切チェックしません。そのため、その結果からクレームを読み取るコードは、送信者が書いた内容を何でもそのまま信頼することになります。
バージョン 9 では典型的なアルゴリズム混同の経路が塞がれており、チェックでも 9.0.3 でそれを確認できました。algorithms オプションを指定しない場合、許可リストは鍵の種類から決まり、公開鍵のテキストを鍵とした HS256 トークンは拒否されます。また、alg: none が通るのは、none を列挙したうえで鍵をまったく渡さない場合だけです。それでも algorithms は指定してください。この指定は前提をコード上で明文化し、鍵の種類が将来変わっても機能し続けます。
対応するアルゴリズムは RFC 7518 の範囲までで、EdDSA、Ed25519、ES256K はありません。Ed25519 で署名する発行者(jose、Go、Rust のスタックでよく見られます)のトークンはここでは検証できないため、代わりに jose を使ってください。また、ES256 には P-256 の鍵が必要です。その名前で P-384 の鍵を使って署名すると、ほかのライブラリが拒否するようなトークンを生成するのではなく、例外がスローされます。
トークン内の ECDSA 署名は、JWS が義務付ける 64 バイトの生の r‖s 形式です。openssl dgst、Java の SHA256withECDSA、クラウドの KMS を使ってトークンを手作業で組み立てた場合、署名は DER になり、検証は "ES256" signatures must be "64" bytes で失敗します。トークンを JWT デバッガーに貼り付けて署名の長さを確認し、DER ⇄ 生形式の変換ツールで修正してください。
多くのチームが意外に思うデフォルトが 2 つあります。1 つは想定より厳しく、もう 1 つは想定より緩いものです。clockTolerance はゼロなので、サーバー間で時刻が数秒ずれただけで jwt expired になります。また、exp は任意なので、これを含めずに発行されたトークンは決して期限切れになりません。署名時には expiresIn を設定し、検証時には maxAge を設定するか exp を明示的に必須にしてください。HMAC シークレットは JWT シークレットチェッカーで確認してください。