secretorprivatekey must be an asymmetric key when using rs256
A fix for the jsonwebtoken error 'secretOrPrivateKey must be an asymmetric key when using RS256': why RS256 needs a PEM key pair instead of a string secret, how to generate the pair with openssl, and how to pass the private key to sign and the public key to verify. Use when switching a Node app from HS256 to RS256, when signing JWTs fails at startup, or when verify throws this error. Triggers: 'must be an asymmetric key', 'RS256'. Not for: expired tokens, wrong audience, or key-rotation issues.
secretorprivatekey must be an asymmetric key when using rs256
TL;DR
You asked the jsonwebtoken library to use RS256, which is RSA asymmetric signing, but handed it a plain string secret, which only works with HS256. Generate an RSA key pair, sign with the private key, and verify with the public key. If you actually want a shared string secret, switch the algorithm back to HS256 explicitly instead.
secretorprivatekey must be an asymmetric key when using rs256Use this when
- Node throws this error from jwt.sign or jwt.verify
- You are migrating an app from HS256 to RS256
- A tutorial's RS256 example fails because the key material is a string
- Verify works in one service and throws this in another (mismatched key material)
Not for this skill when
- Tokens fail with expiry or audience errors (the keys are fine there)
- You are debugging key rotation or JWKS fetching
- The error is about PEM formatting rather than key type
Steps
Generate an RSA key pair with openssl:
openssl genrsa -out private.pem 2048 openssl rsa -in private.pem -pubout -out public.pemExpected: two files, private.pem starting with a PRIVATE KEY header and public.pem with a PUBLIC KEY header.
Sign with the private key and verify with the public key:
const fs = require('fs'); const jwt = require('jsonwebtoken'); const signWith = fs.readFileSync('private.pem'); const verifyWith = fs.readFileSync('public.pem'); const signed = jwt.sign({ sub: '[user id]' }, signWith, { algorithm: 'RS256', expiresIn: '15m' }); const decoded = jwt.verify(signed, verifyWith, { algorithms: ['RS256'] });Expected: verify returns the payload object with no throw. Note the explicit
algorithmslist on verify, which pins verification to RS256.Store the keys properly. The private key belongs in a secret manager or a file with tight permissions, loaded at startup. The public key is not sensitive and can live in config or be published at a JWKS endpoint. Expected: the app boots, signs, and verifies with no key-type errors, and the private key is not in source control.
If you truly want symmetric signing, be explicit. Change both sign and verify to
HS256and use your string secret on both sides. Do not mix: signing HS256 and verifying RS256 (or the reverse) fails. Expected: consistent algorithm on both ends, and you have consciously accepted that one shared secret signs and verifies.
Variant: verifying RS256 with the private key
It technically works, since the private key contains the public parts, but it is wrong in spirit: verifiers should only ever hold the public key. Pass the public key to verify.
Variant: "error:0909006C:PEM routines" after fixing the key type
You cleared the type error and now the PEM itself is malformed. Check for truncated files, Windows line endings, or a key pasted with missing header and footer lines.
Variant: passphrase-protected private key
openssl can encrypt the private key. Then you must supply the passphrase when loading it, or the sign call fails. For services, prefer an unencrypted key file with strict file permissions over a passphrase in config.
Variant: same error from a different library
Every JWT library enforces the same rule: asymmetric algorithms need asymmetric keys. The fix is identical, only the option names differ.
Why this happens
RS256 signs with an RSA private key and verifies with the matching public key, so the signer and verifier do not have to share a secret. The library refuses a string secret for RS256 on purpose: silently accepting it would either produce broken signatures or, worse, tempt a downgrade to symmetric verification where anyone holding the public key could forge tokens.
Edge cases and pitfalls
- Key files committed to git are compromised the moment they are pushed. Rotate and move them to a secret manager.
- 2048-bit RSA is the sane minimum. Larger keys slow signing with little benefit for session tokens.
- Reading the key on every request instead of once at startup is a needless performance hit and a file-handle leak risk.
- If verification must happen in another service or language, export the public key in a standard format (PEM or JWK) rather than inventing your own serialization.
Provenance
Resolved from the public thread: https://vectle.com/posts/pst_cvFPxFJltFyvy1Npzh2OcQ